diff --git a/public/index.html b/public/index.html index 68609d3..11959f1 100644 --- a/public/index.html +++ b/public/index.html @@ -355,29 +355,47 @@ } catch(e) {} finally { inFlight = false; } } - // Auto-send on paste (text or image) - inputEl.addEventListener('paste', (e) => { - const items = (e.clipboardData || window.clipboardData)?.items || []; - let imgItem = null; - for (const it of items) { - if (it.kind === 'file' && it.type.startsWith('image/')) { imgItem = it; break; } - } - if (imgItem) { - e.preventDefault(); - const file = imgItem.getAsFile(); + // Auto-send on paste — all images + all text in one clipboard event go as ONE request + function readFileB64(file) { + return new Promise(resolve => { const fr = new FileReader(); - fr.onload = () => { - const dataUrl = fr.result; - const base64 = dataUrl.split(',')[1]; - send({ image: { data: base64, media_type: file.type || 'image/png' } }); - }; + fr.onload = () => resolve({ + data: fr.result.split(',')[1], + media_type: file.type || 'image/png', + }); fr.readAsDataURL(file); - } else { + }); + } + function getAsStringP(item) { + return new Promise(r => item.getAsString(r)); + } + + inputEl.addEventListener('paste', async (e) => { + const items = Array.from((e.clipboardData || window.clipboardData)?.items || []); + const imageFiles = []; + let pastedTextP = Promise.resolve(''); + for (const it of items) { + if (it.kind === 'file' && it.type.startsWith('image/')) { + imageFiles.push(it.getAsFile()); + } else if (it.kind === 'string' && it.type === 'text/plain') { + pastedTextP = getAsStringP(it); + } + } + + if (imageFiles.length === 0) { setTimeout(() => { const t = inputEl.value.trim(); if (t) send({ text: t }); }, 50); + return; } + + e.preventDefault(); + const images = await Promise.all(imageFiles.map(readFileB64)); + const pastedText = (await pastedTextP).trim(); + const existing = inputEl.value.trim(); + const fullText = [existing, pastedText].filter(Boolean).join('\n'); + send({ text: fullText, images }); }); diff --git a/server.js b/server.js index 323dd26..6f166ee 100644 --- a/server.js +++ b/server.js @@ -73,17 +73,18 @@ async function callClaude(modelId, content, accessToken) { }); } -app.post('/api/chat', express.json({ limit: '25mb' }), async (req, res) => { - const { text, model = 'sonnet', image } = req.body; +app.post('/api/chat', express.json({ limit: '50mb' }), async (req, res) => { + const { text, model = 'sonnet', image, images } = req.body; const hasText = text?.trim(); - if (!hasText && !image) return res.status(400).json({ error: 'text or image required' }); + const imgList = Array.isArray(images) ? images : (image ? [image] : []); + if (!hasText && imgList.length === 0) return res.status(400).json({ error: 'text or image required' }); let content; - if (image?.data && image?.media_type) { - content = [ - { type: 'image', source: { type: 'base64', media_type: image.media_type, data: image.data } }, - { type: 'text', text: hasText || 'Analyze the image and answer any questions shown in it.' }, - ]; + if (imgList.length > 0) { + content = imgList + .filter(img => img?.data && img?.media_type) + .map(img => ({ type: 'image', source: { type: 'base64', media_type: img.media_type, data: img.data } })); + content.push({ type: 'text', text: hasText || 'Analyze all images and answer any questions shown in them.' }); } else { content = text; }