Skip to content

Commit 07a2c5c

Browse files
Merge pull request #292 from davidmyersdev/assistant-improvements
Add support for Vision in GPT-4 Turbo and GPT-4o
2 parents 8472fdc + a016af6 commit 07a2c5c

File tree

9 files changed

+294
-80
lines changed

9 files changed

+294
-80
lines changed

assets/css/tailwind.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,8 @@
313313
@apply bg-layer;
314314
@apply disabled:bg-layer-disabled ui-disabled:bg-layer-disabled;
315315
@apply disabled:text-layer-disabled ui-disabled:text-layer-disabled;
316-
@apply disabled:cursor-default ui-disabled:cursor-default;
316+
@apply disabled:hover:bg-layer-disabled;
317+
@apply disabled:cursor-not-allowed ui-disabled:cursor-not-allowed disabled-within:cursor-not-allowed;
317318
@apply disabled-within:bg-layer-disabled disabled-within:text-layer-disabled;
318319
@apply disabled-within:hover:bg-layer-disabled disabled-within:hover:text-layer-disabled;
319320
@apply hover:bg-layer-hover ui-active:bg-layer-hover;

components/Assistant.vue

Lines changed: 95 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script lang="ts">
22
import { Prec } from '@codemirror/state'
33
import { keymap } from '@codemirror/view'
4+
import { PhotoIcon as AddImagesIcon } from '@heroicons/vue/24/outline'
45
import { StopIcon as TokenIcon } from '@heroicons/vue/24/solid'
56
import { type Options } from 'ink-mde'
67
import { nanoid } from 'nanoid'
@@ -11,6 +12,7 @@ import { type SystemInstruction, useSystemInstructions } from '#root/composables
1112
1213
export default defineComponent({
1314
components: {
15+
AddImagesIcon,
1416
TokenIcon,
1517
},
1618
props: {
@@ -22,6 +24,7 @@ export default defineComponent({
2224
const { id } = useId()
2325
const { isDesktop, modKey } = useDevice()
2426
const chatId = computed(() => props.chatId || id())
27+
// Todo: Update types for `ChatMessage` in vellma...
2528
const { chatMessages } = useChatMessages({ chatId })
2629
const { addSystemInstruction, systemInstructions } = useSystemInstructions()
2730
const systemInstruction = ref<SystemInstruction>({
@@ -194,11 +197,17 @@ export default defineComponent({
194197
messages.push(chatFactory.value.system({ text: systemInstruction.value.text }))
195198
}
196199
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+
}))
199207
}
200208
201209
input.value = ''
210+
files.value = []
202211
203212
if (!messages.length) return
204213
@@ -229,18 +238,57 @@ export default defineComponent({
229238
})
230239
}
231240
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+
232277
return {
233278
CoreDivider,
234279
CoreLink,
280+
addFiles,
235281
apiKey,
236282
chatMessages,
237283
choosePrompt,
238284
clearSystemInstruction,
239285
examplePrompts,
286+
files,
240287
historyElement,
241288
input,
242289
inputElement,
243290
inputOptions,
291+
isAllowedToSend,
244292
isDesktop,
245293
isToBeSaved,
246294
isWaiting,
@@ -255,6 +303,7 @@ export default defineComponent({
255303
systemInstruction,
256304
systemInstructions,
257305
systemMessage,
306+
toFileUrl,
258307
tryAgain,
259308
}
260309
},
@@ -358,10 +407,13 @@ export default defineComponent({
358407
</CoreLayer>
359408
<div v-if="!chatMessages.length || systemMessage?.text" class="flex flex-col gap-2">
360409
<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>
365417
<CoreInput
366418
v-if="isToBeSaved || systemInstruction.id"
367419
v-model="systemInstruction.description"
@@ -384,7 +436,14 @@ export default defineComponent({
384436
</div>
385437
</div>
386438
<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+
/>
388447
<div class="h-4" />
389448
</template>
390449
<div v-else class="flex flex-col flex-grow gap-4 justify-end">
@@ -410,9 +469,11 @@ export default defineComponent({
410469
</div>
411470
</div>
412471
</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>
416477
</div>
417478
<CoreLayer as="section" class="bg-layer">
418479
<CoreDivider />
@@ -424,24 +485,35 @@ export default defineComponent({
424485
<CoreScrollable class="bg-layer rounded max-h-[40vh]">
425486
<div class="flex gap-2">
426487
<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">
429490
Try again
430491
</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...
440496
</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>
443510
</CoreLayer>
444511
</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>
445517
</CoreScrollable>
446518
</CoreLayer>
447519
</div>

components/AssistantChatMessage.vue

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ClipboardIcon } from '@heroicons/vue/24/outline'
33
import { readonly } from '#root/src/vendor/plugins/readonly'
44
import { useVue } from '#shared/composables'
55
6-
const props = defineProps<{ createdAt: Date, role: string, text: string }>()
6+
const props = defineProps<{ createdAt: Date, role: string, text: string, attachments?: { url: string }[] }>()
77
88
const { copy } = useClipboard()
99
const { addToast } = useToasts()
@@ -47,9 +47,12 @@ const copyMessage = async () => {
4747
<label class="bg-layer bg-opacity-75 block m-1 px-1 rounded sticky top-1 z-10" :class="{ 'self-end': isHuman, 'self-start': isAssistant }">
4848
<small>{{ name }}</small>
4949
</label>
50-
<div class="flex items-start">
51-
<CoreLayer>
52-
<CoreEditor v-if="isMounted" :model-value="text" :options="options" class="bg-layer" />
50+
<div class="flex items-start" :class="{ 'justify-end': isHuman, 'justify-start': isAssistant }">
51+
<CoreLayer class="bg-layer rounded">
52+
<CoreEditor v-if="isMounted && text" :model-value="text" :options="options" />
53+
<div v-if="attachments?.length" class="flex p-2" :class="{ 'justify-end': isHuman, 'justify-start': isAssistant }">
54+
<img v-for="attachment in attachments" :key="attachment.url" class="max-h-24 rounded" :src="attachment.url">
55+
</div>
5356
</CoreLayer>
5457
</div>
5558
</div>

components/CoreSwitch.vue

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<script lang="ts" setup>
2+
import { Switch } from '@headlessui/vue'
3+
4+
defineProps<{
5+
description?: string,
6+
label?: string,
7+
}>()
8+
9+
const modelValue = defineModel<boolean>()
10+
</script>
11+
12+
<template>
13+
<div>
14+
<label class="inline-flex items-center gap-2 cursor-pointer">
15+
<Switch
16+
v-model="modelValue"
17+
:class="modelValue ? 'bg-primary' : 'bg-layer'"
18+
class="inline-flex relative h-5 items-stretch justify-stretch shrink-0 grow-0 cursor-pointer rounded border border-layer transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2"
19+
>
20+
<span class="sr-only">Toggle</span>
21+
<span aria-hidden="true" class="pointer-events-none inline-flex aspect-[2/1] overflow-hidden">
22+
<span
23+
class="
24+
relative
25+
font-bold text-[0.4rem] leading-none
26+
inline-flex items-center gap-0
27+
border-2 border-transparent
28+
aspect-square w-1/2
29+
transform-gpu transition duration-200 ease-in-out
30+
"
31+
:class="{
32+
'translate-x-[100%]': modelValue,
33+
'translate-x-0': !modelValue,
34+
}"
35+
>
36+
<span class="absolute block right-full mr-1">ON</span>
37+
<span
38+
class="relative bg-current block rounded-sm h-full w-full"
39+
/>
40+
<span class="absolute block left-full ml-1">OFF</span>
41+
</span>
42+
</span>
43+
</Switch>
44+
<span v-if="label">{{ label }}</span>
45+
</label>
46+
<small v-if="description" class="block text-layer-muted">{{ description }}</small>
47+
</div>
48+
</template>

components/Key.vue

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,3 @@
1-
<template>
2-
<small class="key hidden md:flex justify-center rounded-sm uppercase leading-none text-xs p-1">
3-
<slot></slot>
4-
</small>
5-
</template>
6-
71
<script>
82
export default {
93
props: {
@@ -15,3 +9,9 @@ export default {
159
},
1610
}
1711
</script>
12+
13+
<template>
14+
<small class="key hidden md:flex justify-center rounded-sm uppercase leading-none text-xs p-0.5">
15+
<slot />
16+
</small>
17+
</template>

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
"overlayscrollbars": "^2.4.6",
5252
"pinia": "^2.1.7",
5353
"remarkable": "^2.0.1",
54-
"vellma": "^0.7.0",
54+
"vellma": "^0.8.1",
5555
"vue3-mq": "^3.1.3",
5656
"vuex": "^4.1.0"
5757
},

pages/dev/design-system/index.vue

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
<script lang="ts" setup>
2-
import { DesignSystemColors } from '~/lib/components'
32
</script>
43

54
<template>
65
<div class="flex flex-col gap-4 p-4">
7-
<DesignSystemColors />
6+
<CoreSwitch />
87
</div>
98
</template>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<script lang="ts" setup>
2+
import { DesignSystemColors } from '~/lib/components'
3+
</script>
4+
5+
<template>
6+
<div class="flex flex-col gap-4 p-4">
7+
<DesignSystemColors />
8+
</div>
9+
</template>

0 commit comments

Comments
 (0)