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 { 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: {
|
||||||
|
|
@ -54,3 +55,4 @@ export const deleteFile = mutation({
|
||||||
await ctx.storage.delete(args.storage_id);
|
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 { 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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,8 +41,8 @@
|
||||||
{#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')}
|
||||||
|
|
@ -59,7 +51,7 @@
|
||||||
<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}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue