@@ -7,7 +7,7 @@ import type {
77} from ' ../types'
88import { resolveLabels } from ' ../labels'
99import { useCompact } from ' ../composables/useCompact'
10- const isCompact = useCompact ()
10+ import { useAttachments } from ' ../composables/useAttachments '
1111import MessageList from ' ./MessageList.vue'
1212import RichInput from ' ./RichInput.vue'
1313import ToolApprovalBar from ' ./ToolApprovalBar.vue'
@@ -16,7 +16,7 @@ import AskForm from './AskForm.vue'
1616const props = withDefaults (defineProps <{
1717 messages: StoredMessage []
1818 isStreaming: boolean
19- streamingContent: string | any []
19+ streamingContent: DisplayContent
2020 queuedMessages? : DisplayContent []
2121 pendingToolCall: ToolCallEvent | null
2222 pendingAsk: AskEvent | null
@@ -38,20 +38,21 @@ const emit = defineEmits<{
3838}>()
3939
4040const L = computed (() => resolveLabels (props .labels ))
41+ const isCompact = useCompact ()
4142
4243const richInputRef = ref <InstanceType <typeof RichInput >>()
4344const messagesEl = ref <HTMLElement | null >(null )
44- const attachments = ref <Attachment []>([])
4545const fileInputEl = ref <HTMLInputElement | null >(null )
4646const isDragging = ref (false )
4747let dragLeaveTimer: ReturnType <typeof setTimeout > | null = null
4848
49+ const { attachments, add : addFiles, remove : removeAttachment, drain : drainAttachments, isImageMime } = useAttachments ()
50+
4951// ── Scrolling ──
5052
5153function isAtBottom(): boolean {
52- if (! messagesEl .value ) return true
5354 const el = messagesEl .value
54- return el .scrollHeight - el .scrollTop - el .clientHeight < 60
55+ return ! el || el .scrollHeight - el .scrollTop - el .clientHeight < 60
5556}
5657
5758function scrollToBottom(force = false ) {
@@ -61,63 +62,20 @@ function scrollToBottom(force = false) {
6162}
6263
6364watch (() => props .messages .length , async () => {
64- await nextTick ()
65- scrollToBottom ()
65+ await nextTick (); scrollToBottom ()
6666})
67-
6867watch (() => props .streamingContent , async () => {
69- await nextTick ()
70- scrollToBottom (true )
68+ await nextTick (); scrollToBottom (true )
7169})
7270
7371// ── Attachments ──
7472
75- function pickFile() {
76- fileInputEl .value ?.click ()
77- }
78-
79- function isTextMime(type : string ) {
80- return type .startsWith (' text/' ) ||
81- type === ' application/json' ||
82- type === ' application/xml' ||
83- type === ' application/javascript' ||
84- type === ' application/xhtml+xml'
85- }
86-
87- function readFile(file : File ): Promise <Attachment > {
88- return new Promise ((resolve ) => {
89- const reader = new FileReader ()
90- if (isTextMime (file .type )) {
91- reader .onload = () => resolve ({ name: file .name , type: file .type , content: reader .result as string })
92- reader .readAsText (file )
93- } else {
94- reader .onload = () => resolve ({ name: file .name , type: file .type , dataUrl: reader .result as string })
95- reader .readAsDataURL (file )
96- }
97- })
98- }
99-
100- async function addFiles(files : File []) {
101- for (const file of files ) {
102- if (attachments .value .find (a => a .name === file .name )) continue
103- const att = await readFile (file )
104- attachments .value .push (att )
105- }
106- }
73+ const pickFile = () => fileInputEl .value ?.click ()
10774
10875async function onFileChange(e : Event ) {
109- const files = (e .target as HTMLInputElement ).files
110- if (! files ) return
111- await addFiles (Array .from (files ))
112- ;(e .target as HTMLInputElement ).value = ' '
113- }
114-
115- function removeAttachment(idx : number ) {
116- attachments .value .splice (idx , 1 )
117- }
118-
119- function isImage(att : Attachment ) {
120- return att .type .startsWith (' image/' )
76+ const input = e .target as HTMLInputElement
77+ if (input .files ) await addFiles (Array .from (input .files ))
78+ input .value = ' '
12179}
12280
12381function onInputBarDragOver(e : DragEvent ) {
@@ -136,23 +94,22 @@ function onInputBarDrop(e: DragEvent) {
13694 isDragging .value = false
13795 e .preventDefault ()
13896 if (! e .dataTransfer ?.files ?.length ) return
139- const nonImageFiles = Array .from (e .dataTransfer .files ).filter (f => ! f .type .startsWith (' image/' ))
140- if (nonImageFiles .length > 0 ) addFiles (nonImageFiles )
141- }
142-
143- function onFilesFromEditor(files : File []) {
144- addFiles (files )
97+ // Images dropped on the input bar are handled by the editor's own drop handler; only non-images
98+ // are surfaced as attachment chips here.
99+ const nonImages = Array .from (e .dataTransfer .files ).filter (f => ! isImageMime (f .type ))
100+ if (nonImages .length > 0 ) addFiles (nonImages )
145101}
146102
147103// ── Send ──
148104
149105function send() {
150- if (! richInputRef .value ) return
151- const { parts } = richInputRef .value .getContent ()
152- const fileAtts = attachments .value .splice (0 )
153- if (parts .length === 0 && fileAtts .length === 0 ) return
154- richInputRef .value .clear ()
155- emit (' send' , parts , fileAtts )
106+ const input = richInputRef .value
107+ if (! input ) return
108+ const { parts } = input .getContent ()
109+ const files = drainAttachments ()
110+ if (parts .length === 0 && files .length === 0 ) return
111+ input .clear ()
112+ emit (' send' , parts , files )
156113}
157114
158115onBeforeUnmount (() => {
@@ -211,7 +168,7 @@ defineExpose({ scrollToBottom })
211168 <div style =" flex :1 ;display :flex ;flex-direction :column ;gap :6px ;min-width :0 " >
212169 <div v-if =" attachments.length > 0" class =" chatui-attachments" >
213170 <div v-for =" (att, i) in attachments" :key =" att.name" class =" chatui-attachment-chip" >
214- <img v-if =" isImage (att) && att.dataUrl" :src =" att.dataUrl" class =" chatui-attachment-thumb" />
171+ <img v-if =" isImageMime (att.type ) && att.dataUrl" :src =" att.dataUrl" class =" chatui-attachment-thumb" />
215172 <span v-else class =" chatui-attachment-icon" >📄</span >
216173 <span class =" chatui-attachment-name" >{{ att.name }}</span >
217174 <button class =" chatui-attachment-remove" @click =" removeAttachment(i)" >×</button >
@@ -221,7 +178,7 @@ defineExpose({ scrollToBottom })
221178 ref="richInputRef"
222179 :placeholder =" L .inputPlaceholder "
223180 @submit =" send "
224- @files =" onFilesFromEditor "
181+ @files =" addFiles "
225182 />
226183 </div >
227184 <div class =" chatui-input-actions" >
0 commit comments