feat(frontend): pasting images to chat in web

This commit is contained in:
h
2026-05-10 20:08:53 +02:00
parent 19912af0b5
commit a50c36887a
2 changed files with 48 additions and 2 deletions
+43 -1
View File
@@ -1,11 +1,12 @@
<script lang="ts">
interface Props {
onsubmit: (message: string) => void;
onpasteimage?: (base64: string, mediaType: string) => void;
disabled?: boolean;
allowEmpty?: boolean;
}
let { onsubmit, disabled = false, allowEmpty = false }: Props = $props();
let { onsubmit, onpasteimage, disabled = false, allowEmpty = false }: Props = $props();
let value = $state('');
function handleSubmit(e: Event) {
@@ -16,6 +17,46 @@
value = '';
}
}
async function fileToCompressedBase64(
file: File
): Promise<{ base64: string; mediaType: string }> {
const bitmap = await createImageBitmap(file);
const maxSize = 1920;
const scale = Math.min(maxSize / bitmap.width, maxSize / bitmap.height, 1);
const canvas = document.createElement('canvas');
canvas.width = Math.round(bitmap.width * scale);
canvas.height = Math.round(bitmap.height * scale);
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error('canvas 2d context unavailable');
ctx.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
bitmap.close();
const base64 = canvas.toDataURL('image/jpeg', 0.65).split(',')[1];
return { base64, mediaType: 'image/jpeg' };
}
async function handlePaste(e: ClipboardEvent) {
if (!onpasteimage || disabled) return;
const items = e.clipboardData?.items;
if (!items) return;
const imageFiles: File[] = [];
for (const item of items) {
if (item.kind === 'file' && item.type.startsWith('image/')) {
const f = item.getAsFile();
if (f) imageFiles.push(f);
}
}
if (imageFiles.length === 0) return;
e.preventDefault();
for (const file of imageFiles) {
try {
const { base64, mediaType } = await fileToCompressedBase64(file);
onpasteimage(base64, mediaType);
} catch {
// ignore unreadable image
}
}
}
</script>
<form onsubmit={handleSubmit} class="flex gap-1">
@@ -23,6 +64,7 @@
type="text"
bind:value
{disabled}
onpaste={handlePaste}
placeholder="..."
class="min-w-0 flex-1 rounded bg-neutral-800 px-2 py-1 text-[10px] text-white placeholder-neutral-500 outline-none"
/>
+5 -1
View File
@@ -685,7 +685,11 @@
ondismiss={handleDismissSheet}
/>
{/if}
<ChatInput onsubmit={sendMessage} allowEmpty={draftPhotos.length > 0} />
<ChatInput
onsubmit={sendMessage}
onpasteimage={handleCameraCapture}
allowEmpty={draftPhotos.length > 0}
/>
</div>
{/if}