feat(frontend): pasting images to chat in web
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
interface Props {
|
interface Props {
|
||||||
onsubmit: (message: string) => void;
|
onsubmit: (message: string) => void;
|
||||||
|
onpasteimage?: (base64: string, mediaType: string) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
allowEmpty?: boolean;
|
allowEmpty?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { onsubmit, disabled = false, allowEmpty = false }: Props = $props();
|
let { onsubmit, onpasteimage, disabled = false, allowEmpty = false }: Props = $props();
|
||||||
let value = $state('');
|
let value = $state('');
|
||||||
|
|
||||||
function handleSubmit(e: Event) {
|
function handleSubmit(e: Event) {
|
||||||
@@ -16,6 +17,46 @@
|
|||||||
value = '';
|
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>
|
</script>
|
||||||
|
|
||||||
<form onsubmit={handleSubmit} class="flex gap-1">
|
<form onsubmit={handleSubmit} class="flex gap-1">
|
||||||
@@ -23,6 +64,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
bind:value
|
bind:value
|
||||||
{disabled}
|
{disabled}
|
||||||
|
onpaste={handlePaste}
|
||||||
placeholder="..."
|
placeholder="..."
|
||||||
class="min-w-0 flex-1 rounded bg-neutral-800 px-2 py-1 text-[10px] text-white placeholder-neutral-500 outline-none"
|
class="min-w-0 flex-1 rounded bg-neutral-800 px-2 py-1 text-[10px] text-white placeholder-neutral-500 outline-none"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -685,7 +685,11 @@
|
|||||||
ondismiss={handleDismissSheet}
|
ondismiss={handleDismissSheet}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<ChatInput onsubmit={sendMessage} allowEmpty={draftPhotos.length > 0} />
|
<ChatInput
|
||||||
|
onsubmit={sendMessage}
|
||||||
|
onpasteimage={handleCameraCapture}
|
||||||
|
allowEmpty={draftPhotos.length > 0}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user