From 8186e2adc1c7a8a15f7f472f05a35812d52e65ee Mon Sep 17 00:00:00 2001 From: "Thomas G. Lopes" <26071571+TGlide@users.noreply.github.com> Date: Tue, 17 Jun 2025 19:08:31 +0100 Subject: [PATCH 1/8] wip by claude --- src/lib/backend/convex/conversations.ts | 5 + src/lib/backend/convex/messages.ts | 7 + src/lib/backend/convex/schema.ts | 11 +- src/lib/backend/convex/storage.ts | 56 +++++++ .../ui/file-upload/file-upload.svelte | 131 ++++++++++++++++ src/lib/components/ui/file-upload/index.ts | 1 + src/lib/utils/model-capabilities.ts | 9 ++ src/routes/api/generate-message/+server.ts | 27 +++- src/routes/chat/+layout.svelte | 140 +++++++++++++++++- src/routes/chat/[id]/message.svelte | 9 +- 10 files changed, 391 insertions(+), 5 deletions(-) create mode 100644 src/lib/backend/convex/storage.ts create mode 100644 src/lib/components/ui/file-upload/file-upload.svelte create mode 100644 src/lib/components/ui/file-upload/index.ts create mode 100644 src/lib/utils/model-capabilities.ts diff --git a/src/lib/backend/convex/conversations.ts b/src/lib/backend/convex/conversations.ts index 4e5d60b..08d7e66 100644 --- a/src/lib/backend/convex/conversations.ts +++ b/src/lib/backend/convex/conversations.ts @@ -89,6 +89,10 @@ export const createAndAddMessage = mutation({ content: v.string(), role: messageRoleValidator, session_token: v.string(), + images: v.optional(v.array(v.object({ + url: v.string(), + storage_id: v.string(), + }))), }, handler: async ( ctx, @@ -118,6 +122,7 @@ export const createAndAddMessage = mutation({ role: args.role, conversation_id: conversationId, session_token: args.session_token, + images: args.images, }); return { diff --git a/src/lib/backend/convex/messages.ts b/src/lib/backend/convex/messages.ts index 0c44403..7c7cde4 100644 --- a/src/lib/backend/convex/messages.ts +++ b/src/lib/backend/convex/messages.ts @@ -40,6 +40,11 @@ export const create = mutation({ model_id: v.optional(v.string()), provider: v.optional(providerValidator), token_count: v.optional(v.number()), + // Optional image attachments + images: v.optional(v.array(v.object({ + url: v.string(), + storage_id: v.string(), + }))), }, handler: async (ctx, args): Promise> => { const session = await ctx.runQuery(api.betterAuth.publicGetSession, { @@ -72,6 +77,8 @@ export const create = mutation({ model_id: args.model_id, provider: args.provider, token_count: args.token_count, + // Optional image attachments + images: args.images, }), ctx.db.patch(args.conversation_id as Id<'conversations'>, { generating: true, diff --git a/src/lib/backend/convex/schema.ts b/src/lib/backend/convex/schema.ts index 618e629..8054265 100644 --- a/src/lib/backend/convex/schema.ts +++ b/src/lib/backend/convex/schema.ts @@ -46,7 +46,7 @@ export default defineSchema({ title: v.string(), updated_at: v.optional(v.number()), pinned: v.optional(v.boolean()), - generating: v.boolean(), + generating: v.optional(v.boolean()), }).index('by_user', ['user_id']), messages: defineTable({ conversation_id: v.string(), @@ -56,5 +56,14 @@ export default defineSchema({ model_id: v.optional(v.string()), provider: v.optional(providerValidator), token_count: v.optional(v.number()), + // Optional image attachments + images: v.optional( + v.array( + v.object({ + url: v.string(), + storage_id: v.string(), + }) + ) + ), }).index('by_conversation', ['conversation_id']), }); diff --git a/src/lib/backend/convex/storage.ts b/src/lib/backend/convex/storage.ts new file mode 100644 index 0000000..c672e79 --- /dev/null +++ b/src/lib/backend/convex/storage.ts @@ -0,0 +1,56 @@ +import { v } from 'convex/values'; +import { api } from './_generated/api'; +import { mutation, query } from './_generated/server'; + +export const generateUploadUrl = mutation({ + args: { + session_token: v.string(), + }, + handler: async (ctx, args) => { + const session = await ctx.runQuery(api.betterAuth.publicGetSession, { + session_token: args.session_token, + }); + + if (!session) { + throw new Error('Unauthorized'); + } + + return await ctx.storage.generateUploadUrl(); + }, +}); + +export const getUrl = query({ + args: { + storage_id: v.string(), + session_token: v.string(), + }, + handler: async (ctx, args) => { + const session = await ctx.runQuery(api.betterAuth.publicGetSession, { + session_token: args.session_token, + }); + + if (!session) { + throw new Error('Unauthorized'); + } + + return await ctx.storage.getUrl(args.storage_id); + }, +}); + +export const deleteFile = mutation({ + args: { + storage_id: v.string(), + session_token: v.string(), + }, + handler: async (ctx, args) => { + const session = await ctx.runQuery(api.betterAuth.publicGetSession, { + session_token: args.session_token, + }); + + if (!session) { + throw new Error('Unauthorized'); + } + + await ctx.storage.delete(args.storage_id); + }, +}); \ No newline at end of file diff --git a/src/lib/components/ui/file-upload/file-upload.svelte b/src/lib/components/ui/file-upload/file-upload.svelte new file mode 100644 index 0000000..8395e9c --- /dev/null +++ b/src/lib/components/ui/file-upload/file-upload.svelte @@ -0,0 +1,131 @@ + + +
+ {#if selectedFiles.length > 0} +
+ {#each selectedFiles as file, index} +
+ Uploaded + +
+ {/each} +
+ {/if} + +
+ +
+ {#if isUploading} +
+

Uploading...

+ {:else if fileUpload.isDragging} + +

Drop images here

+ {:else} + +

+ Click to upload or drag and drop images +

+

PNG, JPG, GIF up to 10MB

+ {/if} +
+
+
\ No newline at end of file diff --git a/src/lib/components/ui/file-upload/index.ts b/src/lib/components/ui/file-upload/index.ts new file mode 100644 index 0000000..8048151 --- /dev/null +++ b/src/lib/components/ui/file-upload/index.ts @@ -0,0 +1 @@ +export { default as FileUpload } from './file-upload.svelte'; \ No newline at end of file diff --git a/src/lib/utils/model-capabilities.ts b/src/lib/utils/model-capabilities.ts new file mode 100644 index 0000000..94d2e3e --- /dev/null +++ b/src/lib/utils/model-capabilities.ts @@ -0,0 +1,9 @@ +import type { OpenRouterModel } from '$lib/backend/models/open-router'; + +export function supportsImages(model: OpenRouterModel): boolean { + return model.architecture.input_modalities.includes('image'); +} + +export function getImageSupportedModels(models: OpenRouterModel[]): OpenRouterModel[] { + return models.filter(supportsImages); +} \ No newline at end of file diff --git a/src/routes/api/generate-message/+server.ts b/src/routes/api/generate-message/+server.ts index b6d6949..bea996b 100644 --- a/src/routes/api/generate-message/+server.ts +++ b/src/routes/api/generate-message/+server.ts @@ -21,6 +21,10 @@ const reqBodySchema = z.object({ session_token: z.string(), conversation_id: z.string().optional(), + images: z.array(z.object({ + url: z.string(), + storage_id: z.string(), + })).optional(), }); export type GenerateMessageRequestBody = z.infer; @@ -247,10 +251,29 @@ ${attachedRules.map((r) => `- ${r.name}: ${r.rule}`).join('\n')}`, apiKey: key, }); + const formattedMessages = messages.map((m) => { + if (m.images && m.images.length > 0 && m.role === 'user') { + return { + role: 'user' as const, + content: [ + { type: 'text' as const, text: m.content }, + ...m.images.map(img => ({ + type: 'image_url' as const, + image_url: { url: img.url } + })) + ] + }; + } + return { + role: m.role as 'user' | 'assistant' | 'system', + content: m.content + }; + }); + const streamResult = await ResultAsync.fromPromise( openai.chat.completions.create({ model: model.model_id, - messages: [...messages.map((m) => ({ role: m.role, content: m.content })), systemMessage], + messages: [...formattedMessages, systemMessage], temperature: 0.7, stream: true, }), @@ -411,6 +434,7 @@ export const POST: RequestHandler = async ({ request }) => { content: args.message, role: 'user', session_token: session.token, + images: args.images, }), (e) => `Failed to create conversation: ${e}` ); @@ -444,6 +468,7 @@ export const POST: RequestHandler = async ({ request }) => { session_token: args.session_token, model_id: args.model_id, role: 'user', + images: args.images, }), (e) => `Failed to create user message: ${e}` ); diff --git a/src/routes/chat/+layout.svelte b/src/routes/chat/+layout.svelte index 907b6e4..e50060f 100644 --- a/src/routes/chat/+layout.svelte +++ b/src/routes/chat/+layout.svelte @@ -31,6 +31,11 @@ import XIcon from '~icons/lucide/x'; import { callGenerateMessage } from '../api/generate-message/call.js'; import ModelPicker from './model-picker.svelte'; + import { models } from '$lib/state/models.svelte'; + import { supportsImages } from '$lib/utils/model-capabilities'; + import { Provider } from '$lib/types'; + import { FileUpload } from 'melt/builders'; + import ImageIcon from '~icons/lucide/image'; const client = useConvexClient(); @@ -46,11 +51,16 @@ if (!isString(message) || !session.current?.user.id || !settings.modelId) return; if (textarea) textarea.value = ''; + const messageCopy = message; + const imagesCopy = [...selectedImages]; + selectedImages = []; + const res = await callGenerateMessage({ - message, + message: messageCopy, session_token: session.current?.session.token, conversation_id: page.params.id ?? undefined, model_id: settings.modelId, + images: imagesCopy.length > 0 ? imagesCopy : undefined, }); if (res.isErr()) return; // TODO: Handle error @@ -158,12 +168,85 @@ ]); let message = $state(''); + let selectedImages = $state<{ url: string; storage_id: string }[]>([]); + let isUploading = $state(false); + let fileInput = $state(); usePrompt( () => message, (v) => (message = v) ); + models.init(); + + const currentModelSupportsImages = $derived.by(() => { + if (!settings.modelId) return false; + const openRouterModels = models.from(Provider.OpenRouter); + const currentModel = openRouterModels.find(m => m.id === settings.modelId); + return currentModel ? supportsImages(currentModel) : false; + }); + + const fileUpload = new FileUpload({ + multiple: true, + accept: 'image/*', + maxSize: 10 * 1024 * 1024, // 10MB + }); + + async function handleFileChange(files: File[]) { + if (!files.length || !session.current?.session.token) return; + + isUploading = true; + const uploadedFiles: { url: string; storage_id: string }[] = []; + + try { + for (const file of files) { + // Generate upload URL + const uploadUrl = await client.mutation(api.storage.generateUploadUrl, { + session_token: session.current.session.token, + }); + + // Upload file + const result = await fetch(uploadUrl, { + method: 'POST', + body: file, + }); + + if (!result.ok) { + throw new Error(`Upload failed: ${result.statusText}`); + } + + const { storageId } = await result.json(); + + // Get the URL for the uploaded file + const url = await client.query(api.storage.getUrl, { + storage_id: storageId, + session_token: session.current.session.token, + }); + + if (url) { + uploadedFiles.push({ url, storage_id: storageId }); + } + } + + selectedImages = [...selectedImages, ...uploadedFiles]; + } catch (error) { + console.error('Upload failed:', error); + } finally { + isUploading = false; + } + } + + function removeImage(index: number) { + selectedImages = selectedImages.filter((_, i) => i !== index); + } + + $effect(() => { + if (fileUpload.selected.size > 0) { + handleFileChange(Array.from(fileUpload.selected)); + fileUpload.clear(); + } + }); + const suggestedRules = $derived.by(() => { if (!rulesQuery.data || rulesQuery.data.length === 0) return; if (!textarea) return; @@ -508,7 +591,32 @@ {/if}
-
+ {#if selectedImages.length > 0} +
+ {#each selectedImages as image, index} +
+ Uploaded + +
+ {/each} +
+ {/if} +
+ + {#if fileUpload.isDragging && currentModelSupportsImages} +
+
+ +

Drop images here

+
+
+ {/if}
+ {#if currentModelSupportsImages} + + {#snippet trigger(tooltip)} + + {/snippet} + {isUploading ? 'Uploading...' : 'Add images'} + + {/if} {#snippet trigger(tooltip)} @@ -607,15 +626,7 @@ {/each}
{/if} -
+
@@ -660,14 +671,6 @@ autocomplete="off" {@attach autosize.attachment} > - {#if fileUpload.isDragging && currentModelSupportsImages} -
-
- -

Drop images here

-
-
- {/if}
@@ -682,7 +685,9 @@ {...tooltip.trigger} > {#if isUploading} -
+
{:else} {/if} @@ -727,4 +732,14 @@
+ + {#if fileUpload.isDragging && currentModelSupportsImages} +
+
+ +

Add image

+

Drop an image here to attach it to your message.

+
+
+ {/if} From e3dd9bf07352adb61ead3320adcea783f2ff13be Mon Sep 17 00:00:00 2001 From: "Thomas G. Lopes" <26071571+TGlide@users.noreply.github.com> Date: Tue, 17 Jun 2025 22:54:41 +0100 Subject: [PATCH 3/8] image modal --- src/lib/backend/convex/conversations.ts | 1 + src/lib/backend/convex/messages.ts | 1 + src/lib/backend/convex/schema.ts | 1 + src/lib/components/ui/button/button.svelte | 1 + .../ui/image-modal/image-modal.svelte | 79 +++++++++++++++++++ src/lib/components/ui/image-modal/index.ts | 1 + src/routes/api/generate-message/+server.ts | 1 + src/routes/chat/+layout.svelte | 50 ++++++++++-- src/routes/chat/[id]/message.svelte | 42 +++++++++- 9 files changed, 168 insertions(+), 9 deletions(-) create mode 100644 src/lib/components/ui/image-modal/image-modal.svelte create mode 100644 src/lib/components/ui/image-modal/index.ts diff --git a/src/lib/backend/convex/conversations.ts b/src/lib/backend/convex/conversations.ts index 08d7e66..3e1105a 100644 --- a/src/lib/backend/convex/conversations.ts +++ b/src/lib/backend/convex/conversations.ts @@ -92,6 +92,7 @@ export const createAndAddMessage = mutation({ images: v.optional(v.array(v.object({ url: v.string(), storage_id: v.string(), + fileName: v.optional(v.string()), }))), }, handler: async ( diff --git a/src/lib/backend/convex/messages.ts b/src/lib/backend/convex/messages.ts index 7c7cde4..5646856 100644 --- a/src/lib/backend/convex/messages.ts +++ b/src/lib/backend/convex/messages.ts @@ -44,6 +44,7 @@ export const create = mutation({ images: v.optional(v.array(v.object({ url: v.string(), storage_id: v.string(), + fileName: v.optional(v.string()), }))), }, handler: async (ctx, args): Promise> => { diff --git a/src/lib/backend/convex/schema.ts b/src/lib/backend/convex/schema.ts index 8054265..16832c6 100644 --- a/src/lib/backend/convex/schema.ts +++ b/src/lib/backend/convex/schema.ts @@ -62,6 +62,7 @@ export default defineSchema({ v.object({ url: v.string(), storage_id: v.string(), + fileName: v.optional(v.string()), }) ) ), diff --git a/src/lib/components/ui/button/button.svelte b/src/lib/components/ui/button/button.svelte index a9b71fe..37c5887 100644 --- a/src/lib/components/ui/button/button.svelte +++ b/src/lib/components/ui/button/button.svelte @@ -24,6 +24,7 @@ sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5', lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', icon: 'size-9', + iconSm: 'size-7', }, }, defaultVariants: { diff --git a/src/lib/components/ui/image-modal/image-modal.svelte b/src/lib/components/ui/image-modal/image-modal.svelte new file mode 100644 index 0000000..869e0b4 --- /dev/null +++ b/src/lib/components/ui/image-modal/image-modal.svelte @@ -0,0 +1,79 @@ + + + +
+

{fileName}

+
+ + {#snippet trigger(tooltip)} + + {/snippet} + Download image + + + {#snippet trigger(tooltip)} + + {/snippet} + Open in new tab + + + {#snippet trigger(tooltip)} + + {/snippet} + Close + +
+
+ +
+ {fileName} +
+
+ diff --git a/src/lib/components/ui/image-modal/index.ts b/src/lib/components/ui/image-modal/index.ts new file mode 100644 index 0000000..0a335d0 --- /dev/null +++ b/src/lib/components/ui/image-modal/index.ts @@ -0,0 +1 @@ +export { default as ImageModal } from './image-modal.svelte'; \ No newline at end of file diff --git a/src/routes/api/generate-message/+server.ts b/src/routes/api/generate-message/+server.ts index bea996b..37daffc 100644 --- a/src/routes/api/generate-message/+server.ts +++ b/src/routes/api/generate-message/+server.ts @@ -24,6 +24,7 @@ const reqBodySchema = z.object({ images: z.array(z.object({ url: z.string(), storage_id: z.string(), + fileName: z.string().optional(), })).optional(), }); diff --git a/src/routes/chat/+layout.svelte b/src/routes/chat/+layout.svelte index e3fdf22..962526f 100644 --- a/src/routes/chat/+layout.svelte +++ b/src/routes/chat/+layout.svelte @@ -23,6 +23,7 @@ import { compressImage } from '$lib/utils/image-compression'; import { useConvexClient } from 'convex-svelte'; import { FileUpload, Popover } from 'melt/builders'; + import { ImageModal } from '$lib/components/ui/image-modal'; import { Avatar } from 'melt/components'; import { Debounced, ElementSize, IsMounted, ScrollState } from 'runed'; import SendIcon from '~icons/lucide/arrow-up'; @@ -169,9 +170,14 @@ ]); let message = $state(''); - let selectedImages = $state<{ url: string; storage_id: string }[]>([]); + let selectedImages = $state<{ url: string; storage_id: string; fileName?: string }[]>([]); let isUploading = $state(false); let fileInput = $state(); + let imageModal = $state<{ open: boolean; imageUrl: string; fileName: string }>({ + open: false, + imageUrl: '', + fileName: '', + }); usePrompt( () => message, @@ -197,7 +203,7 @@ if (!files.length || !session.current?.session.token) return; isUploading = true; - const uploadedFiles: { url: string; storage_id: string }[] = []; + const uploadedFiles: { url: string; storage_id: string; fileName?: string }[] = []; try { for (const file of files) { @@ -234,7 +240,7 @@ }); if (url) { - uploadedFiles.push({ url, storage_id: storageId }); + uploadedFiles.push({ url, storage_id: storageId, fileName: file.name }); } } @@ -250,6 +256,22 @@ selectedImages = selectedImages.filter((_, i) => i !== index); } + function openImageModal(imageUrl: string, fileName: string) { + imageModal = { + open: true, + imageUrl, + fileName, + }; + } + + function closeImageModal() { + imageModal = { + open: false, + imageUrl: '', + fileName: '', + }; + } + $effect(() => { if (fileUpload.selected.size > 0) { handleFileChange(Array.from(fileUpload.selected)); @@ -610,11 +632,17 @@
- Uploaded +
{/if} + + diff --git a/src/routes/chat/[id]/message.svelte b/src/routes/chat/[id]/message.svelte index ce3f649..aa2f79a 100644 --- a/src/routes/chat/[id]/message.svelte +++ b/src/routes/chat/[id]/message.svelte @@ -5,6 +5,7 @@ import { CopyButton } from '$lib/components/ui/copy-button'; import '../../../markdown.css'; import MarkdownRenderer from './markdown-renderer.svelte'; + import { ImageModal } from '$lib/components/ui/image-modal'; const style = tv({ base: 'prose rounded-lg p-2', @@ -21,6 +22,28 @@ }; let { message }: Props = $props(); + + let imageModal = $state<{ open: boolean; imageUrl: string; fileName: string }>({ + open: false, + imageUrl: '', + fileName: '' + }); + + function openImageModal(imageUrl: string, fileName: string) { + imageModal = { + open: true, + imageUrl, + fileName + }; + } + + function closeImageModal() { + imageModal = { + open: false, + imageUrl: '', + fileName: '' + }; + } {#if message.role !== 'system' && !(message.role === 'assistant' && message.content.length === 0)} @@ -28,7 +51,17 @@ {#if message.images && message.images.length > 0}
{#each message.images as image} - Uploaded + {/each}
{/if} @@ -52,4 +85,11 @@
+ + {/if} From 06a75e4e3e915afe0d44ef69933eb2bd14164854 Mon Sep 17 00:00:00 2001 From: "Thomas G. Lopes" <26071571+TGlide@users.noreply.github.com> Date: Tue, 17 Jun 2025 23:01:13 +0100 Subject: [PATCH 4/8] modal --- src/routes/chat/+layout.svelte | 47 ++++++++++++---------------------- 1 file changed, 17 insertions(+), 30 deletions(-) diff --git a/src/routes/chat/+layout.svelte b/src/routes/chat/+layout.svelte index 962526f..795be72 100644 --- a/src/routes/chat/+layout.svelte +++ b/src/routes/chat/+layout.svelte @@ -264,14 +264,6 @@ }; } - function closeImageModal() { - imageModal = { - open: false, - imageUrl: '', - fileName: '', - }; - } - $effect(() => { if (fileUpload.selected.size > 0) { handleFileChange(Array.from(fileUpload.selected)); @@ -702,28 +694,6 @@
- {#if currentModelSupportsImages} - - {#snippet trigger(tooltip)} - - {/snippet} - {isUploading ? 'Uploading...' : 'Add images'} - - {/if} {#snippet trigger(tooltip)} + {/if}
From db0dc84ee695ab97d41e8234718888930e3dc9dd Mon Sep 17 00:00:00 2001 From: Aidan Bleser Date: Tue, 17 Jun 2025 17:08:29 -0500 Subject: [PATCH 5/8] fix await --- src/routes/api/generate-message/+server.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/routes/api/generate-message/+server.ts b/src/routes/api/generate-message/+server.ts index 37daffc..06cc24b 100644 --- a/src/routes/api/generate-message/+server.ts +++ b/src/routes/api/generate-message/+server.ts @@ -195,14 +195,12 @@ async function generateAIResponse({ log('Background: Model found and enabled', startTime); - const messagesQuery = await messagesQueryResult; - - if (messagesQuery.isErr()) { - log(`Background messages query failed: ${messagesQuery.error}`, startTime); + if (messagesQueryResult.isErr()) { + log(`Background messages query failed: ${messagesQueryResult.error}`, startTime); return; } - const messages = messagesQuery.value; + const messages = messagesQueryResult.value; log(`Background: Retrieved ${messages.length} messages from conversation`, startTime); if (keyResult.isErr()) { From 5e06c2ca9f34fd3691a885b4b74b74b8079cdb0d Mon Sep 17 00:00:00 2001 From: "Thomas G. Lopes" <26071571+TGlide@users.noreply.github.com> Date: Tue, 17 Jun 2025 23:10:23 +0100 Subject: [PATCH 6/8] enable cookie cache --- src/lib/auth.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 581cbc0..68eae36 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -22,4 +22,10 @@ export const auth = betterAuth({ }, }, plugins: [], + session: { + cookieCache: { + enabled: true, + maxAge: 5 * 60, // Cache duration in seconds + }, + }, }); From 13f40df7c4ea8ddb391603874680e5d04768c861 Mon Sep 17 00:00:00 2001 From: "Thomas G. Lopes" <26071571+TGlide@users.noreply.github.com> Date: Tue, 17 Jun 2025 23:14:18 +0100 Subject: [PATCH 7/8] fix some issues --- src/lib/backend/convex/storage.ts | 6 +- .../ui/file-upload/file-upload.svelte | 131 ------------------ src/lib/components/ui/file-upload/index.ts | 1 - src/routes/chat/+layout.svelte | 9 +- src/routes/chat/[id]/message.svelte | 29 ++-- 5 files changed, 19 insertions(+), 157 deletions(-) delete mode 100644 src/lib/components/ui/file-upload/file-upload.svelte delete mode 100644 src/lib/components/ui/file-upload/index.ts diff --git a/src/lib/backend/convex/storage.ts b/src/lib/backend/convex/storage.ts index c672e79..6b14d40 100644 --- a/src/lib/backend/convex/storage.ts +++ b/src/lib/backend/convex/storage.ts @@ -1,6 +1,7 @@ import { v } from 'convex/values'; import { api } from './_generated/api'; -import { mutation, query } from './_generated/server'; +import { query } from './_generated/server'; +import { mutation } from './functions'; export const generateUploadUrl = mutation({ args: { @@ -53,4 +54,5 @@ export const deleteFile = mutation({ await ctx.storage.delete(args.storage_id); }, -}); \ No newline at end of file +}); + diff --git a/src/lib/components/ui/file-upload/file-upload.svelte b/src/lib/components/ui/file-upload/file-upload.svelte deleted file mode 100644 index 8395e9c..0000000 --- a/src/lib/components/ui/file-upload/file-upload.svelte +++ /dev/null @@ -1,131 +0,0 @@ - - -
- {#if selectedFiles.length > 0} -
- {#each selectedFiles as file, index} -
- Uploaded - -
- {/each} -
- {/if} - -
- -
- {#if isUploading} -
-

Uploading...

- {:else if fileUpload.isDragging} - -

Drop images here

- {:else} - -

- Click to upload or drag and drop images -

-

PNG, JPG, GIF up to 10MB

- {/if} -
-
-
\ No newline at end of file diff --git a/src/lib/components/ui/file-upload/index.ts b/src/lib/components/ui/file-upload/index.ts deleted file mode 100644 index 8048151..0000000 --- a/src/lib/components/ui/file-upload/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as FileUpload } from './file-upload.svelte'; \ No newline at end of file diff --git a/src/routes/chat/+layout.svelte b/src/routes/chat/+layout.svelte index ea99540..5a9e9c6 100644 --- a/src/routes/chat/+layout.svelte +++ b/src/routes/chat/+layout.svelte @@ -6,24 +6,25 @@ import { useCachedQuery } from '$lib/cache/cached-query.svelte.js'; import * as Icons from '$lib/components/icons'; import { Button } from '$lib/components/ui/button'; + import { ImageModal } from '$lib/components/ui/image-modal'; import { LightSwitch } from '$lib/components/ui/light-switch/index.js'; import { callModal } from '$lib/components/ui/modal/global-modal.svelte'; import * as Sidebar from '$lib/components/ui/sidebar'; import Tooltip from '$lib/components/ui/tooltip.svelte'; + import { cmdOrCtrl } from '$lib/hooks/is-mac.svelte.js'; import { TextareaAutosize } from '$lib/spells/textarea-autosize.svelte.js'; import { models } from '$lib/state/models.svelte'; import { usePrompt } from '$lib/state/prompt.svelte.js'; import { session } from '$lib/state/session.svelte.js'; import { settings } from '$lib/state/settings.svelte.js'; import { Provider } from '$lib/types'; + import { compressImage } from '$lib/utils/image-compression'; import { isString } from '$lib/utils/is.js'; import { supportsImages } from '$lib/utils/model-capabilities'; import { omit, pick } from '$lib/utils/object.js'; import { cn } from '$lib/utils/utils.js'; - import { compressImage } from '$lib/utils/image-compression'; import { useConvexClient } from 'convex-svelte'; import { FileUpload, Popover } from 'melt/builders'; - import { ImageModal } from '$lib/components/ui/image-modal'; import { Avatar } from 'melt/components'; import { Debounced, ElementSize, IsMounted, ScrollState } from 'runed'; import SendIcon from '~icons/lucide/arrow-up'; @@ -34,12 +35,10 @@ import PinIcon from '~icons/lucide/pin'; import PinOffIcon from '~icons/lucide/pin-off'; import Settings2Icon from '~icons/lucide/settings-2'; - import XIcon from '~icons/lucide/x'; import UploadIcon from '~icons/lucide/upload'; + import XIcon from '~icons/lucide/x'; import { callGenerateMessage } from '../api/generate-message/call.js'; import ModelPicker from './model-picker.svelte'; - import { cmdOrCtrl } from '$lib/hooks/is-mac.svelte.js'; - import { Provider } from '$lib/types.js'; const client = useConvexClient(); diff --git a/src/routes/chat/[id]/message.svelte b/src/routes/chat/[id]/message.svelte index 31962fc..5e85a4a 100644 --- a/src/routes/chat/[id]/message.svelte +++ b/src/routes/chat/[id]/message.svelte @@ -26,22 +26,14 @@ let imageModal = $state<{ open: boolean; imageUrl: string; fileName: string }>({ open: false, imageUrl: '', - fileName: '' + fileName: '', }); function openImageModal(imageUrl: string, fileName: string) { imageModal = { open: true, imageUrl, - fileName - }; - } - - function closeImageModal() { - imageModal = { - open: false, - imageUrl: '', - fileName: '' + fileName, }; } @@ -49,17 +41,17 @@ {#if message.role !== 'system' && !(message.role === 'assistant' && message.content.length === 0)}
{#if message.images && message.images.length > 0} -
- {#each message.images as image} +
+ {#each message.images as image (image.storage_id)} {/each} @@ -72,7 +64,9 @@ {#snippet failed(error)}
Error rendering markdown: -
{error instanceof Error ? error.message : String(error)}
+
{error instanceof Error ? error.message : String(error)}
{/snippet} @@ -101,6 +95,5 @@ bind:open={imageModal.open} imageUrl={imageModal.imageUrl} fileName={imageModal.fileName} - onClose={closeImageModal} /> {/if} From 34da7558f1f2a1d583b59f093612620dcd751176 Mon Sep 17 00:00:00 2001 From: "Thomas G. Lopes" <26071571+TGlide@users.noreply.github.com> Date: Tue, 17 Jun 2025 23:17:35 +0100 Subject: [PATCH 8/8] improve download code --- .../ui/image-modal/image-modal.svelte | 26 +++++-------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/src/lib/components/ui/image-modal/image-modal.svelte b/src/lib/components/ui/image-modal/image-modal.svelte index 869e0b4..d7552cf 100644 --- a/src/lib/components/ui/image-modal/image-modal.svelte +++ b/src/lib/components/ui/image-modal/image-modal.svelte @@ -14,23 +14,6 @@ let { open = $bindable(false), imageUrl, fileName = 'image' }: Props = $props(); - async function downloadImage() { - try { - const response = await fetch(imageUrl); - const blob = await response.blob(); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = fileName; - document.body.appendChild(a); - a.click(); - window.URL.revokeObjectURL(url); - document.body.removeChild(a); - } catch (error) { - console.error('Failed to download image:', error); - } - } - function openInNewTab() { window.open(imageUrl, '_blank'); } @@ -42,7 +25,13 @@
{#snippet trigger(tooltip)} - {/snippet} @@ -76,4 +65,3 @@ {fileName}
-