fix some issues

This commit is contained in:
Thomas G. Lopes 2025-06-17 23:14:18 +01:00
parent 5e06c2ca9f
commit 13f40df7c4
5 changed files with 19 additions and 157 deletions

View file

@ -1,6 +1,7 @@
import { v } from 'convex/values'; import { v } from 'convex/values';
import { api } from './_generated/api'; import { api } from './_generated/api';
import { mutation, query } from './_generated/server'; import { query } from './_generated/server';
import { mutation } from './functions';
export const generateUploadUrl = mutation({ export const generateUploadUrl = mutation({
args: { args: {
@ -53,4 +54,5 @@ export const deleteFile = mutation({
await ctx.storage.delete(args.storage_id); await ctx.storage.delete(args.storage_id);
}, },
}); });

View file

@ -1,131 +0,0 @@
<script lang="ts">
import { api } from '$lib/backend/convex/_generated/api';
import { session } from '$lib/state/session.svelte';
import { cn } from '$lib/utils/utils';
import { useConvexClient } from 'convex-svelte';
import { FileUpload } from 'melt/builders';
import ImageIcon from '~icons/lucide/image';
import XIcon from '~icons/lucide/x';
type Props = {
onFilesSelected?: (files: { url: string; storage_id: string }[]) => void;
selectedFiles?: { url: string; storage_id: string }[];
class?: string;
disabled?: boolean;
};
let { onFilesSelected, selectedFiles = $bindable([]), class: className, disabled = false }: Props = $props();
const client = useConvexClient();
const fileUpload = new FileUpload({
multiple: true,
accept: 'image/*',
maxSize: 10 * 1024 * 1024, // 10MB
});
let isUploading = $state(false);
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 });
}
}
const newFiles = [...selectedFiles, ...uploadedFiles];
onFilesSelected?.(newFiles);
} catch (error) {
console.error('Upload failed:', error);
} finally {
isUploading = false;
}
}
function removeFile(index: number) {
const newFiles = selectedFiles.filter((_, i) => i !== index);
onFilesSelected?.(newFiles);
}
$effect(() => {
if (fileUpload.selected.size > 0) {
handleFileChange(Array.from(fileUpload.selected));
fileUpload.clear();
}
});
</script>
<div class={cn('flex flex-col gap-2', className)}>
{#if selectedFiles.length > 0}
<div class="flex flex-wrap gap-2">
{#each selectedFiles as file, index}
<div class="relative">
<img src={file.url} alt="Uploaded" class="h-16 w-16 rounded-lg object-cover" />
<button
type="button"
onclick={() => removeFile(index)}
class="absolute -top-2 -right-2 flex h-5 w-5 items-center justify-center rounded-full bg-red-500 text-white hover:bg-red-600"
>
<XIcon class="h-3 w-3" />
</button>
</div>
{/each}
</div>
{/if}
<div>
<input {...fileUpload.input} />
<div
{...fileUpload.dropzone}
class={cn(
'flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 p-4 text-center transition-colors hover:border-gray-400',
{
'border-blue-400 bg-blue-50': fileUpload.isDragging,
'opacity-50 cursor-not-allowed': disabled || isUploading,
}
)}
>
{#if isUploading}
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-gray-900"></div>
<p class="mt-2 text-sm text-gray-600">Uploading...</p>
{:else if fileUpload.isDragging}
<ImageIcon class="h-8 w-8 text-blue-500" />
<p class="mt-2 text-sm text-blue-600">Drop images here</p>
{:else}
<ImageIcon class="h-8 w-8 text-gray-400" />
<p class="mt-2 text-sm text-gray-600">
Click to upload or drag and drop images
</p>
<p class="text-xs text-gray-500">PNG, JPG, GIF up to 10MB</p>
{/if}
</div>
</div>
</div>

View file

@ -1 +0,0 @@
export { default as FileUpload } from './file-upload.svelte';

View file

@ -6,24 +6,25 @@
import { useCachedQuery } from '$lib/cache/cached-query.svelte.js'; import { useCachedQuery } from '$lib/cache/cached-query.svelte.js';
import * as Icons from '$lib/components/icons'; import * as Icons from '$lib/components/icons';
import { Button } from '$lib/components/ui/button'; 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 { LightSwitch } from '$lib/components/ui/light-switch/index.js';
import { callModal } from '$lib/components/ui/modal/global-modal.svelte'; import { callModal } from '$lib/components/ui/modal/global-modal.svelte';
import * as Sidebar from '$lib/components/ui/sidebar'; import * as Sidebar from '$lib/components/ui/sidebar';
import Tooltip from '$lib/components/ui/tooltip.svelte'; 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 { TextareaAutosize } from '$lib/spells/textarea-autosize.svelte.js';
import { models } from '$lib/state/models.svelte'; import { models } from '$lib/state/models.svelte';
import { usePrompt } from '$lib/state/prompt.svelte.js'; import { usePrompt } from '$lib/state/prompt.svelte.js';
import { session } from '$lib/state/session.svelte.js'; import { session } from '$lib/state/session.svelte.js';
import { settings } from '$lib/state/settings.svelte.js'; import { settings } from '$lib/state/settings.svelte.js';
import { Provider } from '$lib/types'; import { Provider } from '$lib/types';
import { compressImage } from '$lib/utils/image-compression';
import { isString } from '$lib/utils/is.js'; import { isString } from '$lib/utils/is.js';
import { supportsImages } from '$lib/utils/model-capabilities'; import { supportsImages } from '$lib/utils/model-capabilities';
import { omit, pick } from '$lib/utils/object.js'; import { omit, pick } from '$lib/utils/object.js';
import { cn } from '$lib/utils/utils.js'; import { cn } from '$lib/utils/utils.js';
import { compressImage } from '$lib/utils/image-compression';
import { useConvexClient } from 'convex-svelte'; import { useConvexClient } from 'convex-svelte';
import { FileUpload, Popover } from 'melt/builders'; import { FileUpload, Popover } from 'melt/builders';
import { ImageModal } from '$lib/components/ui/image-modal';
import { Avatar } from 'melt/components'; import { Avatar } from 'melt/components';
import { Debounced, ElementSize, IsMounted, ScrollState } from 'runed'; import { Debounced, ElementSize, IsMounted, ScrollState } from 'runed';
import SendIcon from '~icons/lucide/arrow-up'; import SendIcon from '~icons/lucide/arrow-up';
@ -34,12 +35,10 @@
import PinIcon from '~icons/lucide/pin'; import PinIcon from '~icons/lucide/pin';
import PinOffIcon from '~icons/lucide/pin-off'; import PinOffIcon from '~icons/lucide/pin-off';
import Settings2Icon from '~icons/lucide/settings-2'; import Settings2Icon from '~icons/lucide/settings-2';
import XIcon from '~icons/lucide/x';
import UploadIcon from '~icons/lucide/upload'; import UploadIcon from '~icons/lucide/upload';
import XIcon from '~icons/lucide/x';
import { callGenerateMessage } from '../api/generate-message/call.js'; import { callGenerateMessage } from '../api/generate-message/call.js';
import ModelPicker from './model-picker.svelte'; import ModelPicker from './model-picker.svelte';
import { cmdOrCtrl } from '$lib/hooks/is-mac.svelte.js';
import { Provider } from '$lib/types.js';
const client = useConvexClient(); const client = useConvexClient();

View file

@ -26,22 +26,14 @@
let imageModal = $state<{ open: boolean; imageUrl: string; fileName: string }>({ let imageModal = $state<{ open: boolean; imageUrl: string; fileName: string }>({
open: false, open: false,
imageUrl: '', imageUrl: '',
fileName: '' fileName: '',
}); });
function openImageModal(imageUrl: string, fileName: string) { function openImageModal(imageUrl: string, fileName: string) {
imageModal = { imageModal = {
open: true, open: true,
imageUrl, imageUrl,
fileName fileName,
};
}
function closeImageModal() {
imageModal = {
open: false,
imageUrl: '',
fileName: ''
}; };
} }
</script> </script>
@ -49,17 +41,17 @@
{#if message.role !== 'system' && !(message.role === 'assistant' && message.content.length === 0)} {#if message.role !== 'system' && !(message.role === 'assistant' && message.content.length === 0)}
<div class={cn('group flex max-w-[80%] flex-col gap-1', { 'self-end': message.role === 'user' })}> <div class={cn('group flex max-w-[80%] flex-col gap-1', { 'self-end': message.role === 'user' })}>
{#if message.images && message.images.length > 0} {#if message.images && message.images.length > 0}
<div class="flex flex-wrap gap-2 mb-2"> <div class="mb-2 flex flex-wrap gap-2">
{#each message.images as image} {#each message.images as image (image.storage_id)}
<button <button
type="button" type="button"
onclick={() => openImageModal(image.url, image.fileName || 'image')} onclick={() => openImageModal(image.url, image.fileName || 'image')}
class="rounded-lg" class="rounded-lg"
> >
<img <img
src={image.url} src={image.url}
alt={image.fileName || 'Uploaded'} alt={image.fileName || 'Uploaded'}
class="max-w-xs rounded-lg hover:opacity-80 transition-opacity" class="max-w-xs rounded-lg transition-opacity hover:opacity-80"
/> />
</button> </button>
{/each} {/each}
@ -72,7 +64,9 @@
{#snippet failed(error)} {#snippet failed(error)}
<div class="text-destructive"> <div class="text-destructive">
<span>Error rendering markdown:</span> <span>Error rendering markdown:</span>
<pre class="!bg-sidebar"><code>{error instanceof Error ? error.message : String(error)}</code></pre> <pre class="!bg-sidebar"><code
>{error instanceof Error ? error.message : String(error)}</code
></pre>
</div> </div>
{/snippet} {/snippet}
</svelte:boundary> </svelte:boundary>
@ -101,6 +95,5 @@
bind:open={imageModal.open} bind:open={imageModal.open}
imageUrl={imageModal.imageUrl} imageUrl={imageModal.imageUrl}
fileName={imageModal.fileName} fileName={imageModal.fileName}
onClose={closeImageModal}
/> />
{/if} {/if}