1
1
<script lang="ts">
2
2
import { Prec } from ' @codemirror/state'
3
3
import { keymap } from ' @codemirror/view'
4
+ import { PhotoIcon as AddImagesIcon } from ' @heroicons/vue/24/outline'
4
5
import { StopIcon as TokenIcon } from ' @heroicons/vue/24/solid'
5
6
import { type Options } from ' ink-mde'
6
7
import { nanoid } from ' nanoid'
@@ -11,6 +12,7 @@ import { type SystemInstruction, useSystemInstructions } from '#root/composables
11
12
12
13
export default defineComponent ({
13
14
components: {
15
+ AddImagesIcon ,
14
16
TokenIcon ,
15
17
},
16
18
props: {
@@ -22,6 +24,7 @@ export default defineComponent({
22
24
const { id } = useId ()
23
25
const { isDesktop, modKey } = useDevice ()
24
26
const chatId = computed (() => props .chatId || id ())
27
+ // Todo: Update types for `ChatMessage` in vellma...
25
28
const { chatMessages } = useChatMessages ({ chatId })
26
29
const { addSystemInstruction, systemInstructions } = useSystemInstructions ()
27
30
const systemInstruction = ref <SystemInstruction >({
@@ -194,11 +197,17 @@ export default defineComponent({
194
197
messages .push (chatFactory .value .system ({ text: systemInstruction .value .text }))
195
198
}
196
199
197
- if (input .value ) {
198
- messages .push (chatFactory .value .human ({ text: input .value }))
200
+ if (input .value || files .value .length > 0 ) {
201
+ const attachments = files .value .map ((file ) => ({ type: ' image' , url: file .dataUrl }))
202
+
203
+ messages .push (chatFactory .value .human ({
204
+ attachments ,
205
+ text: input .value ,
206
+ }))
199
207
}
200
208
201
209
input .value = ' '
210
+ files .value = []
202
211
203
212
if (! messages .length ) return
204
213
@@ -229,18 +238,57 @@ export default defineComponent({
229
238
})
230
239
}
231
240
241
+ const files = ref <{ name: string , blob: File , dataUrl: string }[]>([])
242
+
243
+ const addFiles = async (event : Event ) => {
244
+ if (event .target && ' files' in event .target && event .target .files ) {
245
+ files .value = []
246
+
247
+ for (const blob of Array .from (event .target .files as FileList )) {
248
+ files .value .push ({
249
+ blob ,
250
+ dataUrl: await toDataUrl (blob ),
251
+ name: blob .name ,
252
+ })
253
+ }
254
+ }
255
+ }
256
+
257
+ const isAllowedToSend = computed (() => !! input .value || !! files .value .length || !! systemInstruction .value .text )
258
+
259
+ const toFileUrl = (file : File ) => {
260
+ return URL .createObjectURL (file )
261
+ }
262
+
263
+ const toDataUrl = async (file : File ) => {
264
+ const fileReader = new FileReader ()
265
+
266
+ return new Promise <string >((resolve , reject ) => {
267
+ fileReader .onload = () => {
268
+ resolve (fileReader .result as string )
269
+ }
270
+
271
+ fileReader .onerror = reject
272
+
273
+ fileReader .readAsDataURL (file )
274
+ })
275
+ }
276
+
232
277
return {
233
278
CoreDivider ,
234
279
CoreLink ,
280
+ addFiles ,
235
281
apiKey ,
236
282
chatMessages ,
237
283
choosePrompt ,
238
284
clearSystemInstruction ,
239
285
examplePrompts ,
286
+ files ,
240
287
historyElement ,
241
288
input ,
242
289
inputElement ,
243
290
inputOptions ,
291
+ isAllowedToSend ,
244
292
isDesktop ,
245
293
isToBeSaved ,
246
294
isWaiting ,
@@ -255,6 +303,7 @@ export default defineComponent({
255
303
systemInstruction ,
256
304
systemInstructions ,
257
305
systemMessage ,
306
+ toFileUrl ,
258
307
tryAgain ,
259
308
}
260
309
},
@@ -358,10 +407,13 @@ export default defineComponent({
358
407
</CoreLayer >
359
408
<div v-if =" !chatMessages.length || systemMessage?.text" class =" flex flex-col gap-2" >
360
409
<div class =" flex flex-col gap-2" >
361
- <label v-if =" !systemInstruction.id && !chatMessages.length" class =" flex items-center cursor-pointer gap-2" >
362
- <input v-model =" isToBeSaved" type =" checkbox" class =" checkbox" >
363
- <span >Save Instructions</span >
364
- </label >
410
+ <div >
411
+ <CoreSwitch
412
+ v-if =" !systemInstruction.id && !chatMessages.length"
413
+ v-model =" isToBeSaved"
414
+ label =" Save Instructions"
415
+ />
416
+ </div >
365
417
<CoreInput
366
418
v-if =" isToBeSaved || systemInstruction.id"
367
419
v-model =" systemInstruction.description"
@@ -384,7 +436,14 @@ export default defineComponent({
384
436
</div >
385
437
</div >
386
438
<template v-if =" chatMessages .length " >
387
- <AssistantChatMessage v-for =" message in nonSystemMessages" :key =" message.id" :created-at =" message.createdAt" :role =" message.role" :text =" message.text" />
439
+ <AssistantChatMessage
440
+ v-for =" message in nonSystemMessages"
441
+ :key =" message.id"
442
+ :created-at =" message.createdAt"
443
+ :role =" message.role"
444
+ :text =" message.text"
445
+ :attachments =" message.attachments"
446
+ />
388
447
<div class =" h-4" />
389
448
</template >
390
449
<div v-else class =" flex flex-col flex-grow gap-4 justify-end" >
@@ -410,9 +469,11 @@ export default defineComponent({
410
469
</div >
411
470
</div >
412
471
</CoreScrollable >
413
- <CoreButton v-if =" showBackToTop" :layer =" 1" class =" absolute p-2 right-4 bottom-4" @click =" scrollToTop" >
414
- <AssetArrowUp class =" w-4" />
415
- </CoreButton >
472
+ <CoreLayer class =" absolute right-4 bottom-4" >
473
+ <CoreButton v-if =" showBackToTop" class =" p-2 bg-layer" @click =" scrollToTop" >
474
+ <AssetArrowUp class =" w-4" />
475
+ </CoreButton >
476
+ </CoreLayer >
416
477
</div >
417
478
<CoreLayer as =" section" class =" bg-layer" >
418
479
<CoreDivider />
@@ -424,24 +485,35 @@ export default defineComponent({
424
485
<CoreScrollable class =" bg-layer rounded max-h-[40vh]" >
425
486
<div class =" flex gap-2" >
426
487
<CoreEditor ref =" inputElement" v-model =" input" :layer =" 0" :options =" inputOptions" />
427
- <CoreLayer v-if =" apiKey" >
428
- <CoreButton v-if =" showTryAgainMessage" class = " m-1 self-start sticky top-1 " @click =" tryAgain" >
488
+ <CoreLayer v-if =" apiKey" class = " m-1 self-start sticky top-1 " >
489
+ <CoreButton v-if =" showTryAgainMessage" @click =" tryAgain" >
429
490
Try again
430
491
</CoreButton >
431
- <CoreButton v-else :disabled =" isWaiting || (!input && !systemInstruction.text)" class =" m-1 self-start sticky top-1" @click =" onSend" >
432
- <span v-if =" isWaiting" >
433
- Waiting for a reply...
434
- </span >
435
- <span v-else class =" flex items-center gap-2" >
436
- <span >Send</span >
437
- <span v-if =" isDesktop" class =" hidden md:flex text-layer-muted text-opacity-[inherit]" >
438
- <Key >{{ modKey }}</Key >
439
- <Key >⏎</Key >
492
+ <div v-else class =" flex gap-1" >
493
+ <CoreButton :disabled =" isWaiting || !isAllowedToSend" @click =" onSend" >
494
+ <span v-if =" isWaiting" >
495
+ Waiting for a reply...
440
496
</span >
441
- </span >
442
- </CoreButton >
497
+ <span v-else class =" flex items-center gap-2" >
498
+ <span >Send</span >
499
+ <span v-if =" isDesktop" class =" hidden md:flex text-layer-muted text-opacity-[inherit]" >
500
+ <Key >{{ modKey }}</Key >
501
+ <Key >⏎</Key >
502
+ </span >
503
+ </span >
504
+ </CoreButton >
505
+ <CoreButton as =" label" title =" Attach images" >
506
+ <AddImagesIcon class =" sq-5" />
507
+ <input class =" hidden" type =" file" accept =" image/*" multiple @change =" addFiles" >
508
+ </CoreButton >
509
+ </div >
443
510
</CoreLayer >
444
511
</div >
512
+ <div v-if =" files.length" class =" flex flex-wrap gap-1 p-1" >
513
+ <div v-for =" file in files" :key =" file.name" class =" relative border border-layer flex items-center justify-center sq-12 rounded overflow-hidden" >
514
+ <img class =" h-min w-min object-cover" :src =" file.dataUrl" :alt =" file.name" :title =" file.name" >
515
+ </div >
516
+ </div >
445
517
</CoreScrollable >
446
518
</CoreLayer >
447
519
</div >
0 commit comments