|
48 | 48 | throw new Error("Prompt attachment manager requires promptInput, inputWrap, and toolsLine."); |
49 | 49 | } |
50 | 50 |
|
51 | | - let attachment = null; |
| 51 | + let attachments = []; |
| 52 | + let nextAttachmentId = 0; |
52 | 53 | let previewPopup = null; |
53 | 54 | let previewImage = null; |
| 55 | + let previewAnchor = null; |
54 | 56 |
|
55 | 57 | function ensurePreviewPopup() { |
56 | 58 | if (previewPopup) { |
|
68 | 70 | if (!previewPopup) { |
69 | 71 | return; |
70 | 72 | } |
| 73 | + previewAnchor = null; |
71 | 74 | previewPopup.classList.remove("show"); |
72 | 75 | } |
73 | 76 |
|
|
101 | 104 | previewPopup.style.top = top + "px"; |
102 | 105 | } |
103 | 106 |
|
104 | | - function showPreview(anchor) { |
| 107 | + function showPreview(anchor, attachment) { |
105 | 108 | if (!attachment) { |
106 | 109 | return; |
107 | 110 | } |
108 | 111 |
|
109 | 112 | ensurePreviewPopup(); |
| 113 | + previewAnchor = anchor; |
110 | 114 | previewImage.src = attachment.dataUrl; |
111 | 115 | previewPopup.classList.add("show"); |
112 | 116 | updatePreviewPosition(anchor); |
113 | 117 | } |
114 | 118 |
|
115 | 119 | function emitChange() { |
116 | 120 | onAttachmentChange({ |
117 | | - hasAttachments: Boolean(attachment), |
118 | | - attachments: attachment ? [attachment] : [], |
| 121 | + hasAttachments: attachments.length > 0, |
| 122 | + attachments: attachments.slice(), |
119 | 123 | }); |
120 | 124 | } |
121 | 125 |
|
122 | 126 | function clear() { |
123 | | - attachment = null; |
| 127 | + attachments = []; |
124 | 128 | toolsLine.innerHTML = ""; |
125 | 129 | toolsLine.classList.remove("has-attachment"); |
126 | 130 | hidePreview(); |
127 | 131 | emitChange(); |
128 | 132 | } |
129 | 133 |
|
130 | | - function createAttachmentNode() { |
| 134 | + function removeAttachment(id) { |
| 135 | + const nextAttachments = attachments.filter((attachment) => attachment.id !== id); |
| 136 | + if (nextAttachments.length === attachments.length) { |
| 137 | + return; |
| 138 | + } |
| 139 | + attachments = nextAttachments; |
| 140 | + render(); |
| 141 | + emitChange(); |
| 142 | + } |
| 143 | + |
| 144 | + function createAttachmentNode(attachment) { |
131 | 145 | const wrapper = createElement("div", "chat-attached-context-attachment show-file-icons"); |
132 | 146 | wrapper.tabIndex = 0; |
133 | 147 | wrapper.setAttribute("role", "button"); |
134 | 148 | wrapper.setAttribute("aria-label", ATTACHMENT_LABEL + " (删除)"); |
| 149 | + wrapper.dataset.attachmentId = String(attachment.id); |
135 | 150 | wrapper.draggable = true; |
136 | 151 |
|
137 | 152 | const removeButton = createElement("a", "monaco-button codicon codicon-close"); |
|
143 | 158 | removeButton.addEventListener("click", (event) => { |
144 | 159 | event.preventDefault(); |
145 | 160 | event.stopPropagation(); |
146 | | - clear(); |
| 161 | + removeAttachment(attachment.id); |
147 | 162 | }); |
148 | 163 |
|
149 | 164 | const iconLabel = createElement("div", "monaco-icon-label"); |
|
166 | 181 | wrapper.appendChild(pill); |
167 | 182 | wrapper.appendChild(text); |
168 | 183 |
|
169 | | - const show = () => showPreview(wrapper); |
| 184 | + const show = () => showPreview(wrapper, attachment); |
170 | 185 | wrapper.addEventListener("mouseenter", show); |
171 | 186 | wrapper.addEventListener("focus", show); |
172 | 187 | wrapper.addEventListener("mouseleave", hidePreview); |
|
177 | 192 | wrapper.addEventListener("keydown", (event) => { |
178 | 193 | if (event.key === "Delete" || event.key === "Backspace") { |
179 | 194 | event.preventDefault(); |
180 | | - clear(); |
| 195 | + removeAttachment(attachment.id); |
181 | 196 | } |
182 | 197 | }); |
183 | 198 |
|
|
186 | 201 |
|
187 | 202 | function render() { |
188 | 203 | toolsLine.innerHTML = ""; |
189 | | - toolsLine.classList.toggle("has-attachment", Boolean(attachment)); |
190 | | - if (!attachment) { |
| 204 | + toolsLine.classList.toggle("has-attachment", attachments.length > 0); |
| 205 | + if (attachments.length === 0) { |
191 | 206 | hidePreview(); |
192 | 207 | return; |
193 | 208 | } |
194 | | - toolsLine.appendChild(createAttachmentNode()); |
| 209 | + for (const attachment of attachments) { |
| 210 | + toolsLine.appendChild(createAttachmentNode(attachment)); |
| 211 | + } |
| 212 | + if (previewAnchor && !toolsLine.contains(previewAnchor)) { |
| 213 | + hidePreview(); |
| 214 | + } |
195 | 215 | } |
196 | 216 |
|
197 | | - function setAttachmentData(data) { |
| 217 | + function addAttachmentData(data) { |
198 | 218 | if (!data?.dataUrl) { |
199 | 219 | return false; |
200 | 220 | } |
201 | 221 |
|
202 | | - attachment = { |
| 222 | + nextAttachmentId += 1; |
| 223 | + attachments.push({ |
| 224 | + id: nextAttachmentId, |
203 | 225 | name: data.name || ATTACHMENT_LABEL, |
204 | 226 | mimeType: data.mimeType || "image/png", |
205 | 227 | dataUrl: data.dataUrl, |
206 | 228 | label: ATTACHMENT_LABEL, |
207 | | - }; |
| 229 | + }); |
208 | 230 | render(); |
209 | 231 | emitChange(); |
210 | 232 | return true; |
211 | 233 | } |
212 | 234 |
|
213 | | - async function setAttachmentFromFile(file) { |
| 235 | + async function addAttachmentFromFile(file) { |
214 | 236 | if (!isImageFile(file)) { |
215 | 237 | return false; |
216 | 238 | } |
217 | 239 |
|
218 | 240 | const dataUrl = await readFileAsDataUrl(file); |
219 | | - return setAttachmentData({ |
| 241 | + return addAttachmentData({ |
220 | 242 | name: file.name || ATTACHMENT_LABEL, |
221 | 243 | mimeType: file.type || "image/png", |
222 | 244 | dataUrl, |
|
232 | 254 |
|
233 | 255 | event.preventDefault(); |
234 | 256 | try { |
235 | | - await setAttachmentFromFile(file); |
| 257 | + await addAttachmentFromFile(file); |
236 | 258 | } catch (error) { |
237 | 259 | console.error("Failed to attach pasted image.", error); |
238 | 260 | } |
|
241 | 263 | promptInput.addEventListener("paste", handlePaste); |
242 | 264 |
|
243 | 265 | window.addEventListener("resize", () => { |
244 | | - const attachmentNode = toolsLine.querySelector(".chat-attached-context-attachment"); |
245 | | - if (previewPopup?.classList.contains("show") && attachmentNode) { |
246 | | - updatePreviewPosition(attachmentNode); |
| 266 | + if (previewPopup?.classList.contains("show") && previewAnchor) { |
| 267 | + updatePreviewPosition(previewAnchor); |
247 | 268 | } |
248 | 269 | }); |
249 | 270 |
|
250 | 271 | window.addEventListener( |
251 | 272 | "scroll", |
252 | 273 | () => { |
253 | | - const attachmentNode = toolsLine.querySelector(".chat-attached-context-attachment"); |
254 | | - if (previewPopup?.classList.contains("show") && attachmentNode) { |
255 | | - updatePreviewPosition(attachmentNode); |
| 274 | + if (previewPopup?.classList.contains("show") && previewAnchor) { |
| 275 | + updatePreviewPosition(previewAnchor); |
256 | 276 | } |
257 | 277 | }, |
258 | 278 | true |
|
261 | 281 | return { |
262 | 282 | clear, |
263 | 283 | hasAttachments() { |
264 | | - return Boolean(attachment); |
| 284 | + return attachments.length > 0; |
265 | 285 | }, |
266 | 286 | getImageUrls() { |
267 | | - return attachment ? [attachment.dataUrl] : []; |
| 287 | + return attachments.map((attachment) => attachment.dataUrl); |
268 | 288 | }, |
269 | 289 | }; |
270 | 290 | } |
|
0 commit comments