diff --git a/app/app.config.ts b/app/app.config.ts index 32914ec..d82c423 100644 --- a/app/app.config.ts +++ b/app/app.config.ts @@ -3,6 +3,11 @@ export default defineAppConfig({ colors: { primary: 'blue', neutral: 'neutral' + }, + prose: { + img: { + base: 'size-64 mt-2' + } } } }) diff --git a/app/components/DragDropOverlay.vue b/app/components/DragDropOverlay.vue new file mode 100644 index 0000000..1d36c4c --- /dev/null +++ b/app/components/DragDropOverlay.vue @@ -0,0 +1,24 @@ + + + diff --git a/app/components/FilePreview.vue b/app/components/FilePreview.vue new file mode 100644 index 0000000..a99531c --- /dev/null +++ b/app/components/FilePreview.vue @@ -0,0 +1,68 @@ + + + diff --git a/app/components/FileUploadButton.vue b/app/components/FileUploadButton.vue new file mode 100644 index 0000000..6de9eaf --- /dev/null +++ b/app/components/FileUploadButton.vue @@ -0,0 +1,49 @@ + + + diff --git a/app/components/ModelSelect.vue b/app/components/ModelSelect.vue index 2913ea5..bbc3f25 100644 --- a/app/components/ModelSelect.vue +++ b/app/components/ModelSelect.vue @@ -16,8 +16,14 @@ const items = computed(() => models.map(model => ({ variant="ghost" value-key="value" class="hover:bg-default focus:bg-default data-[state=open]:bg-default" + :content="{ + align: 'start', + side: 'bottom', + sideOffset: 4 + }" :ui="{ - trailingIcon: 'group-data-[state=open]:rotate-180 transition-transform duration-200' + trailingIcon: 'group-data-[state=open]:rotate-180 transition-transform duration-200', + content: 'w-fit' }" /> diff --git a/app/components/tool/Weather.vue b/app/components/tool/Weather.vue new file mode 100644 index 0000000..dfe967a --- /dev/null +++ b/app/components/tool/Weather.vue @@ -0,0 +1,119 @@ + + + diff --git a/app/composables/useFileUploadWithStatus.ts b/app/composables/useFileUploadWithStatus.ts new file mode 100644 index 0000000..1347338 --- /dev/null +++ b/app/composables/useFileUploadWithStatus.ts @@ -0,0 +1,87 @@ +export function useFileUploadWithStatus(chatId: string) { + const files = ref([]) + + const { loggedIn } = useUserSession() + + async function uploadFiles(newFiles: File[]) { + if (!loggedIn.value) { + return + } + + const filesWithStatus: FileWithStatus[] = newFiles.map(file => ({ + file, + id: crypto.randomUUID(), + previewUrl: createObjectUrl(file), + status: 'uploading' as const + })) + + files.value = [...files.value, ...filesWithStatus] + + for (const fileWithStatus of filesWithStatus) { + try { + const response = await uploadFileToBlob(fileWithStatus.file, chatId) + + const index = files.value.findIndex(f => f.id === fileWithStatus.id) + if (index !== -1) { + files.value[index] = { + ...files.value[index]!, + status: 'uploaded', + uploadedUrl: response.url + } + } + } catch (error) { + const index = files.value.findIndex(f => f.id === fileWithStatus.id) + if (index !== -1) { + files.value[index] = { + ...files.value[index]!, + status: 'error', + error: error instanceof Error ? error.message : 'Upload failed' + } + } + } + } + } + + const { dropzoneRef, isDragging } = useFileUpload({ + accept: 'image/*,application/pdf', + multiple: true, + onUpdate: uploadFiles + }) + + const isUploading = computed(() => + files.value.some(f => f.status === 'uploading') + ) + + const uploadedFiles = computed(() => + files.value + .filter(f => f.status === 'uploaded' && f.uploadedUrl) + .map(f => ({ + type: 'file' as const, + mediaType: f.file.type, + url: f.uploadedUrl! + })) + ) + + function removeFile(id: string) { + const file = files.value.find(f => f.id === id) + if (file) { + URL.revokeObjectURL(file.previewUrl) + files.value = files.value.filter(f => f.id !== id) + } + } + + function clear() { + clearFiles(files) + } + + return { + dropzoneRef, + isDragging, + files, + isUploading, + uploadedFiles, + addFiles: uploadFiles, + removeFile, + clearFiles: clear + } +} diff --git a/app/pages/chat/[id].vue b/app/pages/chat/[id].vue index 1338f96..5fc4174 100644 --- a/app/pages/chat/[id].vue +++ b/app/pages/chat/[id].vue @@ -1,8 +1,8 @@