image modal
This commit is contained in:
parent
ea46448dd9
commit
e3dd9bf073
9 changed files with 168 additions and 9 deletions
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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<Id<'messages'>> => {
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ export default defineSchema({
|
|||
v.object({
|
||||
url: v.string(),
|
||||
storage_id: v.string(),
|
||||
fileName: v.optional(v.string()),
|
||||
})
|
||||
)
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
79
src/lib/components/ui/image-modal/image-modal.svelte
Normal file
79
src/lib/components/ui/image-modal/image-modal.svelte
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<script lang="ts">
|
||||
import { Modal } from '$lib/components/ui/modal';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import DownloadIcon from '~icons/lucide/download';
|
||||
import ExternalLinkIcon from '~icons/lucide/external-link';
|
||||
import XIcon from '~icons/lucide/x';
|
||||
import Tooltip from '../tooltip.svelte';
|
||||
|
||||
type Props = {
|
||||
open?: boolean;
|
||||
imageUrl: string;
|
||||
fileName?: string;
|
||||
};
|
||||
|
||||
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');
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:open>
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold">{fileName}</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
<Tooltip>
|
||||
{#snippet trigger(tooltip)}
|
||||
<Button size="iconSm" variant="outline" onclick={downloadImage} {...tooltip.trigger}>
|
||||
<DownloadIcon class="size-4" />
|
||||
</Button>
|
||||
{/snippet}
|
||||
Download image
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
{#snippet trigger(tooltip)}
|
||||
<Button size="iconSm" variant="outline" onclick={openInNewTab} {...tooltip.trigger}>
|
||||
<ExternalLinkIcon class="size-4" />
|
||||
</Button>
|
||||
{/snippet}
|
||||
Open in new tab
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
{#snippet trigger(tooltip)}
|
||||
<Button
|
||||
size="iconSm"
|
||||
variant="outline"
|
||||
onclick={() => (open = false)}
|
||||
{...tooltip.trigger}
|
||||
>
|
||||
<XIcon class="size-4" />
|
||||
</Button>
|
||||
{/snippet}
|
||||
Close
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex items-center justify-center">
|
||||
<img src={imageUrl} alt={fileName} class="max-h-[60vh] max-w-full rounded-lg object-contain" />
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
1
src/lib/components/ui/image-modal/index.ts
Normal file
1
src/lib/components/ui/image-modal/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default as ImageModal } from './image-modal.svelte';
|
||||
|
|
@ -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(),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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<HTMLInputElement>();
|
||||
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 @@
|
|||
<div
|
||||
class="group border-secondary-foreground/[0.08] bg-secondary-foreground/[0.02] hover:bg-secondary-foreground/10 relative flex h-12 w-12 max-w-full shrink-0 items-center justify-center gap-2 rounded-xl border border-solid p-0 transition-[width,height] duration-500"
|
||||
>
|
||||
<img
|
||||
src={image.url}
|
||||
alt="Uploaded"
|
||||
class="size-10 rounded-lg object-cover opacity-100 transition-opacity"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => openImageModal(image.url, image.fileName || 'image')}
|
||||
class="rounded-lg"
|
||||
>
|
||||
<img
|
||||
src={image.url}
|
||||
alt="Uploaded"
|
||||
class="size-10 rounded-lg object-cover opacity-100 transition-opacity"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => removeImage(index)}
|
||||
|
|
@ -742,4 +770,10 @@
|
|||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<ImageModal
|
||||
bind:open={imageModal.open}
|
||||
imageUrl={imageModal.imageUrl}
|
||||
fileName={imageModal.fileName}
|
||||
/>
|
||||
</Sidebar.Root>
|
||||
|
|
|
|||
|
|
@ -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: ''
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if message.role !== 'system' && !(message.role === 'assistant' && message.content.length === 0)}
|
||||
|
|
@ -28,7 +51,17 @@
|
|||
{#if message.images && message.images.length > 0}
|
||||
<div class="flex flex-wrap gap-2 mb-2">
|
||||
{#each message.images as image}
|
||||
<img src={image.url} alt="Uploaded" class="max-w-xs rounded-lg" />
|
||||
<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"
|
||||
/>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -52,4 +85,11 @@
|
|||
<CopyButton class="size-7" text={message.content} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ImageModal
|
||||
bind:open={imageModal.open}
|
||||
imageUrl={imageModal.imageUrl}
|
||||
fileName={imageModal.fileName}
|
||||
onClose={closeImageModal}
|
||||
/>
|
||||
{/if}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue