fix some issues
This commit is contained in:
parent
5e06c2ca9f
commit
13f40df7c4
5 changed files with 19 additions and 157 deletions
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default as FileUpload } from './file-upload.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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
|
@ -49,17 +41,17 @@
|
|||
{#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' })}>
|
||||
{#if message.images && message.images.length > 0}
|
||||
<div class="flex flex-wrap gap-2 mb-2">
|
||||
{#each message.images as image}
|
||||
<div class="mb-2 flex flex-wrap gap-2">
|
||||
{#each message.images as image (image.storage_id)}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => openImageModal(image.url, image.fileName || 'image')}
|
||||
class="rounded-lg"
|
||||
>
|
||||
<img
|
||||
src={image.url}
|
||||
alt={image.fileName || 'Uploaded'}
|
||||
class="max-w-xs rounded-lg hover:opacity-80 transition-opacity"
|
||||
<img
|
||||
src={image.url}
|
||||
alt={image.fileName || 'Uploaded'}
|
||||
class="max-w-xs rounded-lg transition-opacity hover:opacity-80"
|
||||
/>
|
||||
</button>
|
||||
{/each}
|
||||
|
|
@ -72,7 +64,9 @@
|
|||
{#snippet failed(error)}
|
||||
<div class="text-destructive">
|
||||
<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>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
|
|
@ -101,6 +95,5 @@
|
|||
bind:open={imageModal.open}
|
||||
imageUrl={imageModal.imageUrl}
|
||||
fileName={imageModal.fileName}
|
||||
onClose={closeImageModal}
|
||||
/>
|
||||
{/if}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue