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 @@ + + + + + + + + Drop your files here + + + Supported formats: Images and PDFs + + + + 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 @@ + + + + + + + {{ invocation.output.temperature }}° + C + + + + {{ invocation.output.location }} + + + H:{{ invocation.output.temperatureHigh }}° L:{{ invocation.output.temperatureLow }}° + + + + + + + + + {{ invocation.output.condition.text }} + + + + + + + {{ invocation.output.humidity }}% + + + + {{ invocation.output.windSpeed }} km/h + + + + + + + + {{ forecast.day }} + + + + + {{ forecast.high }}° + + + {{ forecast.low }}° + + + + + + + + No forecast available + + + + + + + + + + Loading weather data... + + + + + + + + + + + Can't get weather data, please try again later + + + + + + + + + + + Getting weather... + + + + + 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 @@ - + - + + + + + @@ -103,7 +145,7 @@ onMounted(() => { label="Thinking..." variant="link" color="neutral" - class="p-0" + class="px-0" loading /> @@ -114,6 +156,19 @@ onMounted(() => { :components="components" :parser-options="{ highlight: false }" /> + + + + + + + + @@ -121,19 +176,27 @@ onMounted(() => { + + + - + + + + diff --git a/app/pages/index.vue b/app/pages/index.vue index 495ebe0..86d50e2 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -1,23 +1,49 @@ - + - + + How can I help you today? - - + + + + + - + + + + diff --git a/app/utils/file.ts b/app/utils/file.ts new file mode 100644 index 0000000..9a0c019 --- /dev/null +++ b/app/utils/file.ts @@ -0,0 +1,49 @@ +export interface FileWithStatus { + file: File + id: string + previewUrl: string + status: 'uploading' | 'uploaded' | 'error' + uploadedUrl?: string + error?: string +} + +export function createObjectUrl(file: File): string { + return URL.createObjectURL(file) +} + +export function clearFiles(files: Ref) { + files.value.forEach((fileWithStatus) => { + URL.revokeObjectURL(fileWithStatus.previewUrl) + }) + files.value = [] +} + +export async function uploadFileToBlob(file: File, chatId: string): Promise<{ + url: string + pathname: string + contentType: string + size: number +}> { + const formData = new FormData() + formData.append('file', file) + formData.append('chatId', chatId) + + return await $fetch('/api/upload', { + method: 'POST', + body: formData + }) +} + +export async function uploadFilesToBlob(files: File[], chatId: string) { + return Promise.all( + files.map(async (file) => { + const response = await uploadFileToBlob(file, chatId) + + return { + type: 'file' as const, + mediaType: response.contentType, + url: response.url + } + }) + ) +} diff --git a/nuxt.config.ts b/nuxt.config.ts index 370b850..7a4ce19 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -4,7 +4,8 @@ export default defineNuxtConfig({ '@nuxt/eslint', '@nuxt/ui', '@nuxtjs/mdc', - 'nuxt-auth-utils' + 'nuxt-auth-utils', + 'motion-v/nuxt' ], devtools: { @@ -14,6 +15,9 @@ export default defineNuxtConfig({ css: ['~/assets/css/main.css'], mdc: { + headings: { + anchorLinks: false + }, highlight: { // noApiRoute: true shikiEngine: 'javascript' diff --git a/package.json b/package.json index d008f15..7885d27 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,11 @@ "@iconify-json/simple-icons": "^1.2.54", "@nuxt/ui": "^4.0.1", "@nuxtjs/mdc": "^0.17.4", + "@vercel/blob": "^2.0.0", "ai": "^5.0.60", "date-fns": "^4.1.0", "drizzle-orm": "^0.44.6", + "motion-v": "^1.7.2", "nuxt": "^4.1.2", "nuxt-auth-utils": "^0.5.25", "pg": "^8.16.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 216ff99..8b4931d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,10 +28,13 @@ importers: version: 1.2.54 '@nuxt/ui': specifier: ^4.0.1 - version: 4.0.1(@babel/parser@7.28.4)(change-case@5.4.4)(db0@0.3.4(drizzle-orm@0.44.6(@opentelemetry/api@1.9.0)(pg@8.16.3)))(embla-carousel@8.6.0)(ioredis@5.8.0)(magicast@0.3.5)(typescript@5.9.3)(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))(zod@4.1.11) + version: 4.0.1(@babel/parser@7.28.4)(@vercel/blob@2.0.0)(change-case@5.4.4)(db0@0.3.4(drizzle-orm@0.44.6(@opentelemetry/api@1.9.0)(pg@8.16.3)))(embla-carousel@8.6.0)(ioredis@5.8.0)(magicast@0.3.5)(typescript@5.9.3)(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))(zod@4.1.11) '@nuxtjs/mdc': specifier: ^0.17.4 version: 0.17.4(magicast@0.3.5) + '@vercel/blob': + specifier: ^2.0.0 + version: 2.0.0 ai: specifier: ^5.0.60 version: 5.0.60(zod@4.1.11) @@ -41,9 +44,12 @@ importers: drizzle-orm: specifier: ^0.44.6 version: 0.44.6(@opentelemetry/api@1.9.0)(pg@8.16.3) + motion-v: + specifier: ^1.7.2 + version: 1.7.2(@vueuse/core@13.9.0(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)) nuxt: specifier: ^4.1.2 - version: 4.1.2(@parcel/watcher@2.5.1)(@types/node@24.7.0)(@vue/compiler-sfc@3.5.22)(db0@0.3.4(drizzle-orm@0.44.6(@opentelemetry/api@1.9.0)(pg@8.16.3)))(drizzle-orm@0.44.6(@opentelemetry/api@1.9.0)(pg@8.16.3))(eslint@9.37.0(jiti@2.6.1))(ioredis@5.8.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.52.4)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.1.0(typescript@5.9.3))(yaml@2.8.1) + version: 4.1.2(@parcel/watcher@2.5.1)(@types/node@24.7.0)(@vercel/blob@2.0.0)(@vue/compiler-sfc@3.5.22)(db0@0.3.4(drizzle-orm@0.44.6(@opentelemetry/api@1.9.0)(pg@8.16.3)))(drizzle-orm@0.44.6(@opentelemetry/api@1.9.0)(pg@8.16.3))(eslint@9.37.0(jiti@2.6.1))(ioredis@5.8.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.52.4)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.1.0(typescript@5.9.3))(yaml@2.8.1) nuxt-auth-utils: specifier: ^0.5.25 version: 0.5.25(magicast@0.3.5) @@ -637,6 +643,10 @@ packages: resolution: {integrity: sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@fastify/busboy@2.1.1': + resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} + engines: {node: '>=14'} + '@floating-ui/core@1.7.3': resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} @@ -1843,6 +1853,10 @@ packages: cpu: [x64] os: [win32] + '@vercel/blob@2.0.0': + resolution: {integrity: sha512-oAj7Pdy83YKSwIaMFoM7zFeLYWRc+qUpW3PiDSblxQMnGFb43qs4bmfq7dr/+JIfwhs6PTwe1o2YBwKhyjWxXw==} + engines: {node: '>=20.0.0'} + '@vercel/nft@0.30.2': resolution: {integrity: sha512-pquXF3XZFg/T3TBor08rUhIGgOhdSilbn7WQLVP/aVSSO+25Rs4H/m3nxNDQ2x3znX7Z3yYjryN8xaLwypcwQg==} engines: {node: '>=18'} @@ -1900,15 +1914,27 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@vue/compiler-core@3.5.21': + resolution: {integrity: sha512-8i+LZ0vf6ZgII5Z9XmUvrCyEzocvWT+TeR2VBUVlzIH6Tyv57E20mPZ1bCS+tbejgUgmjrEh7q/0F0bibskAmw==} + '@vue/compiler-core@3.5.22': resolution: {integrity: sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==} + '@vue/compiler-dom@3.5.21': + resolution: {integrity: sha512-jNtbu/u97wiyEBJlJ9kmdw7tAr5Vy0Aj5CgQmo+6pxWNQhXZDPsRr1UWPN4v3Zf82s2H3kF51IbzZ4jMWAgPlQ==} + '@vue/compiler-dom@3.5.22': resolution: {integrity: sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==} + '@vue/compiler-sfc@3.5.21': + resolution: {integrity: sha512-SXlyk6I5eUGBd2v8Ie7tF6ADHE9kCR6mBEuPyH1nUZ0h6Xx6nZI29i12sJKQmzbDyr2tUHMhhTt51Z6blbkTTQ==} + '@vue/compiler-sfc@3.5.22': resolution: {integrity: sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==} + '@vue/compiler-ssr@3.5.21': + resolution: {integrity: sha512-vKQ5olH5edFZdf5ZrlEgSO1j1DMA4u23TVK5XR1uMhvwnYvVdDF0nHXJUblL/GvzlShQbjhZZ2uvYmDlAbgo9w==} + '@vue/compiler-ssr@3.5.22': resolution: {integrity: sha512-GdgyLvg4R+7T8Nk2Mlighx7XGxq/fJf9jaVofc3IL0EPesTE86cP/8DD1lT3h1JeZr2ySBvyqKQJgbS54IX1Ww==} @@ -1948,6 +1974,9 @@ packages: peerDependencies: vue: 3.5.22 + '@vue/shared@3.5.21': + resolution: {integrity: sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw==} + '@vue/shared@3.5.22': resolution: {integrity: sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==} @@ -2079,8 +2108,8 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} - ansis@4.2.0: - resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + ansis@4.1.0: + resolution: {integrity: sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==} engines: {node: '>=14'} anymatch@3.1.3: @@ -2114,6 +2143,9 @@ packages: resolution: {integrity: sha512-3pYeLyDZ6nJew9QeBhS4Nly02269Dkdk32+zdbbKmL6n4ZuaGorwwA+xx12xgOciA8BF1w9x+dlH7oUkFTW91w==} engines: {node: '>=20.18.0'} + async-retry@1.3.3: + resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + async-sema@3.1.1: resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==} @@ -2127,8 +2159,8 @@ packages: peerDependencies: postcss: ^8.1.0 - b4a@1.7.3: - resolution: {integrity: sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==} + b4a@1.7.2: + resolution: {integrity: sha512-DyUOdz+E8R6+sruDpQNOaV0y/dBbV6X/8ZkxrDcR0Ifc3BgKlpgG0VAtfOozA0eMtJO5GGe9FsZhueLs00pTww==} peerDependencies: react-native-b4a: '*' peerDependenciesMeta: @@ -3307,6 +3339,10 @@ packages: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} + is-buffer@2.0.5: + resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} + engines: {node: '>=4'} + is-builtin-module@5.0.0: resolution: {integrity: sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==} engines: {node: '>=18.20'} @@ -3355,6 +3391,9 @@ packages: is-module@1.0.0: resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -3964,8 +4003,8 @@ packages: engines: {node: ^14.16.0 || >=16.10.0} hasBin: true - oauth4webapi@3.8.2: - resolution: {integrity: sha512-FzZZ+bht5X0FKe7Mwz3DAVAmlH1BV5blSak/lHMBKz0/EBMhX6B10GlQYI51+oRp8ObJaX0g6pXrAxZh5s8rjw==} + oauth4webapi@3.8.1: + resolution: {integrity: sha512-olkZDELNycOWQf9LrsELFq8n05LwJgV8UkrS0cburk6FOwf8GvLam+YB+Uj5Qvryee+vwWOfQVeI5Vm0MVg7SA==} ofetch@1.4.1: resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==} @@ -3999,8 +4038,8 @@ packages: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} - openid-client@6.8.1: - resolution: {integrity: sha512-VoYT6enBo6Vj2j3Q5Ec0AezS+9YGzQo1f5Xc42lreMGlfP4ljiXPKVDvCADh+XHCV/bqPu/wWSiCVXbJKvrODw==} + openid-client@6.8.0: + resolution: {integrity: sha512-oG1d1nAVhIIE+JSjLS+7E9wY1QOJpZltkzlJdbZ7kEn7Hp3hqur2TEeQ8gLOHoHkhbRAGZJKoOnEQcLOQJuIyg==} optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} @@ -4538,6 +4577,10 @@ packages: restructure@3.0.2: resolution: {integrity: sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==} + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -4767,8 +4810,8 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - strip-literal@3.1.0: - resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + strip-literal@3.0.0: + resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==} structured-clone-es@1.0.0: resolution: {integrity: sha512-FL8EeKFFyNQv5cMnXI31CIMCsFarSVI2bF0U0ImeNE3g/F1IvJQyqzOXxPBRXiwQfyBTlbNe88jh1jFW0O/jiQ==} @@ -4844,6 +4887,10 @@ packages: text-decoder@1.2.3: resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} + throttleit@2.1.0: + resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} + engines: {node: '>=18'} + tiny-inflate@1.0.3: resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} @@ -4924,6 +4971,10 @@ packages: undici-types@7.14.0: resolution: {integrity: sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==} + undici@5.29.0: + resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} + engines: {node: '>=14.0'} + unenv@2.0.0-rc.21: resolution: {integrity: sha512-Wj7/AMtE9MRnAXa6Su3Lk0LNCfqDYgfwVjwRFVum9U7wsto1imuHqk4kTm7Jni+5A0Hn7dttL6O/zjvUvoo+8A==} @@ -5842,7 +5893,7 @@ snapshots: '@eslint/config-inspector@1.3.0(eslint@9.37.0(jiti@2.6.1))': dependencies: '@nodelib/fs.walk': 3.0.1 - ansis: 4.2.0 + ansis: 4.1.0 bundle-require: 5.1.0(esbuild@0.25.10) cac: 6.7.14 chokidar: 4.0.3 @@ -5898,6 +5949,8 @@ snapshots: '@eslint/core': 0.16.0 levn: 0.4.1 + '@fastify/busboy@2.1.1': {} + '@floating-ui/core@1.7.3': dependencies: '@floating-ui/utils': 0.2.10 @@ -6240,7 +6293,7 @@ snapshots: - utf-8-validate - vite - '@nuxt/fonts@0.11.4(db0@0.3.4(drizzle-orm@0.44.6(@opentelemetry/api@1.9.0)(pg@8.16.3)))(ioredis@5.8.0)(magicast@0.3.5)(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))': + '@nuxt/fonts@0.11.4(@vercel/blob@2.0.0)(db0@0.3.4(drizzle-orm@0.44.6(@opentelemetry/api@1.9.0)(pg@8.16.3)))(ioredis@5.8.0)(magicast@0.3.5)(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))': dependencies: '@nuxt/devtools-kit': 2.6.5(magicast@0.3.5)(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1)) '@nuxt/kit': 3.19.2(magicast@0.3.5) @@ -6261,7 +6314,7 @@ snapshots: ufo: 1.6.1 unifont: 0.4.1 unplugin: 2.3.10 - unstorage: 1.17.1(db0@0.3.4(drizzle-orm@0.44.6(@opentelemetry/api@1.9.0)(pg@8.16.3)))(ioredis@5.8.0) + unstorage: 1.17.1(@vercel/blob@2.0.0)(db0@0.3.4(drizzle-orm@0.44.6(@opentelemetry/api@1.9.0)(pg@8.16.3)))(ioredis@5.8.0) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -6365,7 +6418,7 @@ snapshots: '@nuxt/schema@4.1.2': dependencies: - '@vue/shared': 3.5.22 + '@vue/shared': 3.5.21 consola: 3.4.2 defu: 6.1.4 pathe: 2.0.3 @@ -6390,13 +6443,13 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/ui@4.0.1(@babel/parser@7.28.4)(change-case@5.4.4)(db0@0.3.4(drizzle-orm@0.44.6(@opentelemetry/api@1.9.0)(pg@8.16.3)))(embla-carousel@8.6.0)(ioredis@5.8.0)(magicast@0.3.5)(typescript@5.9.3)(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))(zod@4.1.11)': + '@nuxt/ui@4.0.1(@babel/parser@7.28.4)(@vercel/blob@2.0.0)(change-case@5.4.4)(db0@0.3.4(drizzle-orm@0.44.6(@opentelemetry/api@1.9.0)(pg@8.16.3)))(embla-carousel@8.6.0)(ioredis@5.8.0)(magicast@0.3.5)(typescript@5.9.3)(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))(zod@4.1.11)': dependencies: '@ai-sdk/vue': 2.0.60(vue@3.5.22(typescript@5.9.3))(zod@4.1.11) '@iconify/vue': 5.0.0(vue@3.5.22(typescript@5.9.3)) '@internationalized/date': 3.10.0 '@internationalized/number': 3.6.5 - '@nuxt/fonts': 0.11.4(db0@0.3.4(drizzle-orm@0.44.6(@opentelemetry/api@1.9.0)(pg@8.16.3)))(ioredis@5.8.0)(magicast@0.3.5)(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1)) + '@nuxt/fonts': 0.11.4(@vercel/blob@2.0.0)(db0@0.3.4(drizzle-orm@0.44.6(@opentelemetry/api@1.9.0)(pg@8.16.3)))(ioredis@5.8.0)(magicast@0.3.5)(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1)) '@nuxt/icon': 2.0.0(magicast@0.3.5)(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)) '@nuxt/kit': 4.1.2(magicast@0.3.5) '@nuxt/schema': 4.1.2 @@ -6558,7 +6611,7 @@ snapshots: '@shikijs/transformers': 3.13.0 '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - '@vue/compiler-core': 3.5.22 + '@vue/compiler-core': 3.5.21 consola: 3.4.2 debug: 4.4.3 defu: 6.1.4 @@ -7342,6 +7395,14 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true + '@vercel/blob@2.0.0': + dependencies: + async-retry: 1.3.3 + is-buffer: 2.0.5 + is-node-process: 1.2.0 + throttleit: 2.1.0 + undici: 5.29.0 + '@vercel/nft@0.30.2(rollup@4.52.4)': dependencies: '@mapbox/node-pre-gyp': 2.0.0 @@ -7395,7 +7456,7 @@ snapshots: '@vue-macros/common@3.0.0-beta.16(vue@3.5.22(typescript@5.9.3))': dependencies: - '@vue/compiler-sfc': 3.5.22 + '@vue/compiler-sfc': 3.5.21 ast-kit: 2.1.2 local-pkg: 1.1.2 magic-string-ast: 1.0.2 @@ -7415,7 +7476,7 @@ snapshots: '@babel/types': 7.28.4 '@vue/babel-helper-vue-transform-on': 1.5.0 '@vue/babel-plugin-resolve-type': 1.5.0(@babel/core@7.28.4) - '@vue/shared': 3.5.22 + '@vue/shared': 3.5.21 optionalDependencies: '@babel/core': 7.28.4 transitivePeerDependencies: @@ -7428,10 +7489,18 @@ snapshots: '@babel/helper-module-imports': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 '@babel/parser': 7.28.4 - '@vue/compiler-sfc': 3.5.22 + '@vue/compiler-sfc': 3.5.21 transitivePeerDependencies: - supports-color + '@vue/compiler-core@3.5.21': + dependencies: + '@babel/parser': 7.28.4 + '@vue/shared': 3.5.21 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + '@vue/compiler-core@3.5.22': dependencies: '@babel/parser': 7.28.4 @@ -7440,11 +7509,28 @@ snapshots: estree-walker: 2.0.2 source-map-js: 1.2.1 + '@vue/compiler-dom@3.5.21': + dependencies: + '@vue/compiler-core': 3.5.21 + '@vue/shared': 3.5.21 + '@vue/compiler-dom@3.5.22': dependencies: '@vue/compiler-core': 3.5.22 '@vue/shared': 3.5.22 + '@vue/compiler-sfc@3.5.21': + dependencies: + '@babel/parser': 7.28.4 + '@vue/compiler-core': 3.5.21 + '@vue/compiler-dom': 3.5.21 + '@vue/compiler-ssr': 3.5.21 + '@vue/shared': 3.5.21 + estree-walker: 2.0.2 + magic-string: 0.30.19 + postcss: 8.5.6 + source-map-js: 1.2.1 + '@vue/compiler-sfc@3.5.22': dependencies: '@babel/parser': 7.28.4 @@ -7457,6 +7543,11 @@ snapshots: postcss: 8.5.6 source-map-js: 1.2.1 + '@vue/compiler-ssr@3.5.21': + dependencies: + '@vue/compiler-dom': 3.5.21 + '@vue/shared': 3.5.21 + '@vue/compiler-ssr@3.5.22': dependencies: '@vue/compiler-dom': 3.5.22 @@ -7493,8 +7584,8 @@ snapshots: '@vue/language-core@3.1.0(typescript@5.9.3)': dependencies: '@volar/language-core': 2.4.23 - '@vue/compiler-dom': 3.5.22 - '@vue/shared': 3.5.22 + '@vue/compiler-dom': 3.5.21 + '@vue/shared': 3.5.21 alien-signals: 3.0.0 muggle-string: 0.4.1 path-browserify: 1.0.1 @@ -7524,6 +7615,8 @@ snapshots: '@vue/shared': 3.5.22 vue: 3.5.22(typescript@5.9.3) + '@vue/shared@3.5.21': {} + '@vue/shared@3.5.22': {} '@vueuse/core@10.11.1(vue@3.5.22(typescript@5.9.3))': @@ -7629,7 +7722,7 @@ snapshots: ansi-styles@6.2.3: {} - ansis@4.2.0: {} + ansis@4.1.0: {} anymatch@3.1.3: dependencies: @@ -7676,6 +7769,10 @@ snapshots: '@babel/parser': 7.28.4 ast-kit: 2.1.2 + async-retry@1.3.3: + dependencies: + retry: 0.13.1 + async-sema@3.1.1: {} async@3.2.6: {} @@ -7690,7 +7787,7 @@ snapshots: postcss: 8.5.6 postcss-value-parser: 4.2.0 - b4a@1.7.3: {} + b4a@1.7.2: {} bail@2.0.2: {} @@ -8901,6 +8998,8 @@ snapshots: dependencies: binary-extensions: 2.3.0 + is-buffer@2.0.5: {} + is-builtin-module@5.0.0: dependencies: builtin-modules: 5.0.0 @@ -8936,6 +9035,8 @@ snapshots: is-module@1.0.0: {} + is-node-process@1.2.0: {} + is-number@7.0.0: {} is-path-inside@4.0.0: {} @@ -9570,7 +9671,7 @@ snapshots: natural-compare@1.4.0: {} - nitropack@2.12.6(drizzle-orm@0.44.6(@opentelemetry/api@1.9.0)(pg@8.16.3)): + nitropack@2.12.6(@vercel/blob@2.0.0)(drizzle-orm@0.44.6(@opentelemetry/api@1.9.0)(pg@8.16.3)): dependencies: '@cloudflare/kv-asset-handler': 0.4.0 '@rollup/plugin-alias': 5.1.1(rollup@4.52.4) @@ -9637,7 +9738,7 @@ snapshots: unenv: 2.0.0-rc.21 unimport: 4.1.1 unplugin-utils: 0.3.0 - unstorage: 1.17.1(db0@0.3.4(drizzle-orm@0.44.6(@opentelemetry/api@1.9.0)(pg@8.16.3)))(ioredis@5.8.0) + unstorage: 1.17.1(@vercel/blob@2.0.0)(db0@0.3.4(drizzle-orm@0.44.6(@opentelemetry/api@1.9.0)(pg@8.16.3)))(ioredis@5.8.0) untyped: 2.0.0 unwasm: 0.3.11 youch: 4.1.0-beta.11 @@ -9724,7 +9825,7 @@ snapshots: hookable: 5.5.3 jose: 6.1.0 ofetch: 1.4.1 - openid-client: 6.8.1 + openid-client: 6.8.0 pathe: 2.0.3 scule: 1.3.0 uncrypto: 0.1.3 @@ -9733,7 +9834,7 @@ snapshots: - bcrypt - magicast - nuxt@4.1.2(@parcel/watcher@2.5.1)(@types/node@24.7.0)(@vue/compiler-sfc@3.5.22)(db0@0.3.4(drizzle-orm@0.44.6(@opentelemetry/api@1.9.0)(pg@8.16.3)))(drizzle-orm@0.44.6(@opentelemetry/api@1.9.0)(pg@8.16.3))(eslint@9.37.0(jiti@2.6.1))(ioredis@5.8.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.52.4)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.1.0(typescript@5.9.3))(yaml@2.8.1): + nuxt@4.1.2(@parcel/watcher@2.5.1)(@types/node@24.7.0)(@vercel/blob@2.0.0)(@vue/compiler-sfc@3.5.22)(db0@0.3.4(drizzle-orm@0.44.6(@opentelemetry/api@1.9.0)(pg@8.16.3)))(drizzle-orm@0.44.6(@opentelemetry/api@1.9.0)(pg@8.16.3))(eslint@9.37.0(jiti@2.6.1))(ioredis@5.8.0)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.52.4)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.1.0(typescript@5.9.3))(yaml@2.8.1): dependencies: '@nuxt/cli': 3.28.0(magicast@0.3.5) '@nuxt/devalue': 2.0.2 @@ -9743,7 +9844,7 @@ snapshots: '@nuxt/telemetry': 2.6.6(magicast@0.3.5) '@nuxt/vite-builder': 4.1.2(@types/node@24.7.0)(eslint@9.37.0(jiti@2.6.1))(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.52.4)(terser@5.44.0)(typescript@5.9.3)(vue-tsc@3.1.0(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3))(yaml@2.8.1) '@unhead/vue': 2.0.17(vue@3.5.22(typescript@5.9.3)) - '@vue/shared': 3.5.22 + '@vue/shared': 3.5.21 c12: 3.3.0(magicast@0.3.5) chokidar: 4.0.3 compatx: 0.2.0 @@ -9768,7 +9869,7 @@ snapshots: mlly: 1.8.0 mocked-exports: 0.1.1 nanotar: 0.2.0 - nitropack: 2.12.6(drizzle-orm@0.44.6(@opentelemetry/api@1.9.0)(pg@8.16.3)) + nitropack: 2.12.6(@vercel/blob@2.0.0)(drizzle-orm@0.44.6(@opentelemetry/api@1.9.0)(pg@8.16.3)) nypm: 0.6.2 ofetch: 1.4.1 ohash: 2.0.11 @@ -9792,7 +9893,7 @@ snapshots: unimport: 4.1.1 unplugin: 2.3.10 unplugin-vue-router: 0.15.0(@vue/compiler-sfc@3.5.22)(typescript@5.9.3)(vue-router@4.5.1(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)) - unstorage: 1.17.1(db0@0.3.4(drizzle-orm@0.44.6(@opentelemetry/api@1.9.0)(pg@8.16.3)))(ioredis@5.8.0) + unstorage: 1.17.1(@vercel/blob@2.0.0)(db0@0.3.4(drizzle-orm@0.44.6(@opentelemetry/api@1.9.0)(pg@8.16.3)))(ioredis@5.8.0) untyped: 2.0.0 vue: 3.5.22(typescript@5.9.3) vue-bundle-renderer: 2.2.0 @@ -9865,7 +9966,7 @@ snapshots: pkg-types: 2.3.0 tinyexec: 1.0.1 - oauth4webapi@3.8.2: {} + oauth4webapi@3.8.1: {} ofetch@1.4.1: dependencies: @@ -9906,10 +10007,10 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 - openid-client@6.8.1: + openid-client@6.8.0: dependencies: jose: 6.1.0 - oauth4webapi: 3.8.2 + oauth4webapi: 3.8.1 optionator@0.9.4: dependencies: @@ -10543,6 +10644,8 @@ snapshots: restructure@3.0.2: {} + retry@0.13.1: {} + reusify@1.1.0: {} rfdc@1.4.1: {} @@ -10783,7 +10886,7 @@ snapshots: strip-json-comments@3.1.1: {} - strip-literal@3.1.0: + strip-literal@3.0.0: dependencies: js-tokens: 9.0.1 @@ -10837,7 +10940,7 @@ snapshots: tar-stream@3.1.7: dependencies: - b4a: 1.7.3 + b4a: 1.7.2 fast-fifo: 1.3.2 streamx: 2.23.0 transitivePeerDependencies: @@ -10860,10 +10963,12 @@ snapshots: text-decoder@1.2.3: dependencies: - b4a: 1.7.3 + b4a: 1.7.2 transitivePeerDependencies: - react-native-b4a + throttleit@2.1.0: {} + tiny-inflate@1.0.3: {} tiny-invariant@1.3.3: {} @@ -10924,6 +11029,10 @@ snapshots: undici-types@7.14.0: {} + undici@5.29.0: + dependencies: + '@fastify/busboy': 2.1.1 + unenv@2.0.0-rc.21: dependencies: defu: 6.1.4 @@ -10980,7 +11089,7 @@ snapshots: picomatch: 4.0.3 pkg-types: 1.3.1 scule: 1.3.0 - strip-literal: 3.1.0 + strip-literal: 3.0.0 unplugin: 2.3.10 unplugin-utils: 0.2.5 @@ -11111,7 +11220,7 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 - unstorage@1.17.1(db0@0.3.4(drizzle-orm@0.44.6(@opentelemetry/api@1.9.0)(pg@8.16.3)))(ioredis@5.8.0): + unstorage@1.17.1(@vercel/blob@2.0.0)(db0@0.3.4(drizzle-orm@0.44.6(@opentelemetry/api@1.9.0)(pg@8.16.3)))(ioredis@5.8.0): dependencies: anymatch: 3.1.3 chokidar: 4.0.3 @@ -11122,6 +11231,7 @@ snapshots: ofetch: 1.4.1 ufo: 1.6.1 optionalDependencies: + '@vercel/blob': 2.0.0 db0: 0.3.4(drizzle-orm@0.44.6(@opentelemetry/api@1.9.0)(pg@8.16.3)) ioredis: 5.8.0 @@ -11236,7 +11346,7 @@ snapshots: vite-plugin-inspect@11.3.3(@nuxt/kit@3.19.2(magicast@0.3.5))(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1)): dependencies: - ansis: 4.2.0 + ansis: 4.1.0 debug: 4.4.3 error-stack-parser-es: 1.0.5 ohash: 2.0.11 diff --git a/server/api/chats.post.ts b/server/api/chats.post.ts index 5a81a65..6ff6244 100644 --- a/server/api/chats.post.ts +++ b/server/api/chats.post.ts @@ -1,10 +1,11 @@ export default defineEventHandler(async (event) => { const session = await getUserSession(event) - const { input } = await readBody(event) + const { id, message } = await readBody(event) const db = useDrizzle() const [chat] = await db.insert(tables.chats).values({ + id, title: '', userId: session.user?.id || session.id }).returning() @@ -15,7 +16,7 @@ export default defineEventHandler(async (event) => { await db.insert(tables.messages).values({ chatId: chat.id, role: 'user', - parts: [{ type: 'text', text: input }] + parts: message.parts }) return chat diff --git a/server/api/chats/[id].delete.ts b/server/api/chats/[id].delete.ts index 33430f8..a3ee93d 100644 --- a/server/api/chats/[id].delete.ts +++ b/server/api/chats/[id].delete.ts @@ -1,11 +1,41 @@ -export default defineEventHandler(async (event) => { - const session = await getUserSession(event) +import { list, del } from '@vercel/blob' +export default defineEventHandler(async (event) => { + const session = await requireUserSession(event) const { id } = getRouterParams(event) - const db = useDrizzle() + const chat = await db.query.chats.findFirst({ + where: (chat, { eq }) => and(eq(chat.id, id as string), eq(chat.userId, session.user?.id || session.id)) + }) + + if (!chat) { + throw createError({ + statusCode: 404, + statusMessage: 'Chat not found' + }) + } + + const username = session.user.username + const chatFolder = `${username}/${id}/` + + try { + const { blobs } = await list({ + prefix: chatFolder + }) + + await Promise.all( + blobs.map(blob => + del(blob.url).catch(error => + console.error('Failed to delete file:', blob.pathname, error) + ) + ) + ) + } catch (error) { + console.error('Failed to list/delete chat files:', error) + } + return await db.delete(tables.chats) - .where(and(eq(tables.chats.id, id as string), eq(tables.chats.userId, session.user?.id || session.id))) + .where(and(eq(tables.chats.id, id as string), eq(tables.chats.userId, session.user.id))) .returning() }) diff --git a/server/api/chats/[id].get.ts b/server/api/chats/[id].get.ts index b032050..11d9e2c 100644 --- a/server/api/chats/[id].get.ts +++ b/server/api/chats/[id].get.ts @@ -6,7 +6,9 @@ export default defineEventHandler(async (event) => { const chat = await useDrizzle().query.chats.findFirst({ where: (chat, { eq }) => and(eq(chat.id, id as string), eq(chat.userId, session.user?.id || session.id)), with: { - messages: true + messages: { + orderBy: (message, { asc }) => asc(message.createdAt) + } } }) diff --git a/server/api/chats/[id].post.ts b/server/api/chats/[id].post.ts index ed46a03..0401bde 100644 --- a/server/api/chats/[id].post.ts +++ b/server/api/chats/[id].post.ts @@ -1,4 +1,4 @@ -import { convertToModelMessages, createUIMessageStream, createUIMessageStreamResponse, generateText, streamText } from 'ai' +import { convertToModelMessages, createUIMessageStream, createUIMessageStreamResponse, generateText, smoothStream, stepCountIs, streamText } from 'ai' import { gateway } from '@ai-sdk/gateway' import type { UIMessage } from 'ai' import { z } from 'zod' @@ -12,8 +12,11 @@ defineRouteMeta({ export default defineEventHandler(async (event) => { const session = await getUserSession(event) + const username = session.user?.username || 'anonymous' - const { id } = getRouterParams(event) + const { id } = await getValidatedRouterParams(event, z.object({ + id: z.string() + }).parse) const { model, messages } = await readValidatedBody(event, z.object({ model: z.string(), @@ -60,8 +63,21 @@ export default defineEventHandler(async (event) => { execute: ({ writer }) => { const result = streamText({ model: gateway(model), - system: 'You are a helpful assistant that can answer questions and help.', - messages: convertToModelMessages(messages) + system: `You are a helpful assistant that can answer questions and help. User name is ${username}.`, + messages: convertToModelMessages(messages), + providerOptions: { + google: { + thinkingConfig: { + includeThoughts: true, + thinkingBudget: 2048 + } + } + }, + stopWhen: stepCountIs(5), + experimental_transform: smoothStream({ chunking: 'word' }), + tools: { + weather: weatherTool + } }) if (!chat.title) { @@ -72,7 +88,9 @@ export default defineEventHandler(async (event) => { }) } - writer.merge(result.toUIMessageStream()) + writer.merge(result.toUIMessageStream({ + sendReasoning: true + })) }, onFinish: async ({ messages }) => { await db.insert(tables.messages).values(messages.map(message => ({ diff --git a/server/api/upload.post.ts b/server/api/upload.post.ts new file mode 100644 index 0000000..739cd20 --- /dev/null +++ b/server/api/upload.post.ts @@ -0,0 +1,67 @@ +import { put } from '@vercel/blob' + +const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10MB +const ALLOWED_FILE_TYPES = ['image/', 'application/pdf'] + +export default defineEventHandler(async (event) => { + const session = await getUserSession(event) + + if (!session.user) { + throw createError({ + statusCode: 401, + statusMessage: 'Authentication required to upload files' + }) + } + + const username = session.user.username + + const form = await readFormData(event) + const file = form.get('file') as File + const chatId = form.get('chatId') as string | null + + if (!file) { + throw createError({ + statusCode: 400, + statusMessage: 'No file provided' + }) + } + + if (!chatId) { + throw createError({ + statusCode: 400, + statusMessage: 'Chat ID is required' + }) + } + + if (file.size > MAX_FILE_SIZE) { + throw createError({ + statusCode: 400, + statusMessage: `File too large. Maximum size is ${MAX_FILE_SIZE / 1024 / 1024}MB` + }) + } + + const isAllowedType = ALLOWED_FILE_TYPES.some(type => + file.type.startsWith(type) || file.type === type + ) + + if (!isAllowedType) { + throw createError({ + statusCode: 400, + statusMessage: 'File type not allowed. Only images and PDFs are accepted.' + }) + } + + const filename = `${username}/${chatId}/${file.name}` + + const blob = await put(filename, file, { + access: 'public', + addRandomSuffix: true + }) + + return { + url: blob.url, + pathname: blob.pathname, + contentType: file.type, + size: file.size + } +}) diff --git a/shared/utils/index.ts b/shared/utils/index.ts new file mode 100644 index 0000000..29c53ce --- /dev/null +++ b/shared/utils/index.ts @@ -0,0 +1 @@ +export * from './tools/weather' diff --git a/shared/utils/tools/weather.ts b/shared/utils/tools/weather.ts new file mode 100644 index 0000000..52f046d --- /dev/null +++ b/shared/utils/tools/weather.ts @@ -0,0 +1,37 @@ +import type { UIToolInvocation } from 'ai' +import { tool } from 'ai' +import { z } from 'zod' + +const getWeatherData = (k: string) => ({ + 'sunny': { text: 'Sunny', icon: 'i-lucide-sun' }, + 'partly-cloudy': { text: 'Partly Cloudy', icon: 'i-lucide-cloud-sun' }, + 'cloudy': { text: 'Cloudy', icon: 'i-lucide-cloud' }, + 'rainy': { text: 'Rainy', icon: 'i-lucide-cloud-rain' }, + 'foggy': { text: 'Foggy', icon: 'i-lucide-cloud-fog' } +}[k] || { text: 'Sunny', icon: 'i-lucide-sun' }) + +export const weatherTool = tool({ + description: 'Get weather info with 5-day forecast', + inputSchema: z.object({ location: z.string().describe('Location for weather') }), + execute: async ({ location }) => { + const temp = Math.floor(Math.random() * 35) + 5 + const conds = ['sunny', 'partly-cloudy', 'cloudy', 'rainy', 'foggy'] as const + return { + location, + temperature: Math.round(temp), + temperatureHigh: Math.round(temp + Math.random() * 5 + 2), + temperatureLow: Math.round(temp - Math.random() * 5 - 2), + condition: getWeatherData(conds[Math.floor(Math.random() * conds.length)]!), + humidity: Math.floor(Math.random() * 60) + 20, + windSpeed: Math.floor(Math.random() * 25) + 5, + dailyForecast: ['Today', 'Tomorrow', 'Thu', 'Fri', 'Sat'].map((day, i) => ({ + day, + high: Math.round(temp + Math.random() * 8 - 2), + low: Math.round(temp - Math.random() * 8 - 3), + condition: getWeatherData(conds[(Math.floor(Math.random() * conds.length) + i) % conds.length]!) + })) + } + } +}) + +export type WeatherUIToolInvocation = UIToolInvocation
+ Drop your files here +
+ Supported formats: Images and PDFs +