feat: multi-image paste — collect all images + text from one clipboard event
This commit is contained in:
@@ -355,29 +355,47 @@
|
|||||||
} catch(e) {} finally { inFlight = false; }
|
} catch(e) {} finally { inFlight = false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-send on paste (text or image)
|
// Auto-send on paste — all images + all text in one clipboard event go as ONE request
|
||||||
inputEl.addEventListener('paste', (e) => {
|
function readFileB64(file) {
|
||||||
const items = (e.clipboardData || window.clipboardData)?.items || [];
|
return new Promise(resolve => {
|
||||||
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();
|
|
||||||
const fr = new FileReader();
|
const fr = new FileReader();
|
||||||
fr.onload = () => {
|
fr.onload = () => resolve({
|
||||||
const dataUrl = fr.result;
|
data: fr.result.split(',')[1],
|
||||||
const base64 = dataUrl.split(',')[1];
|
media_type: file.type || 'image/png',
|
||||||
send({ image: { data: base64, media_type: file.type || 'image/png' } });
|
});
|
||||||
};
|
|
||||||
fr.readAsDataURL(file);
|
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(() => {
|
setTimeout(() => {
|
||||||
const t = inputEl.value.trim();
|
const t = inputEl.value.trim();
|
||||||
if (t) send({ text: t });
|
if (t) send({ text: t });
|
||||||
}, 50);
|
}, 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 });
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
17
server.js
17
server.js
@@ -73,17 +73,18 @@ async function callClaude(modelId, content, accessToken) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
app.post('/api/chat', express.json({ limit: '25mb' }), async (req, res) => {
|
app.post('/api/chat', express.json({ limit: '50mb' }), async (req, res) => {
|
||||||
const { text, model = 'sonnet', image } = req.body;
|
const { text, model = 'sonnet', image, images } = req.body;
|
||||||
const hasText = text?.trim();
|
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;
|
let content;
|
||||||
if (image?.data && image?.media_type) {
|
if (imgList.length > 0) {
|
||||||
content = [
|
content = imgList
|
||||||
{ type: 'image', source: { type: 'base64', media_type: image.media_type, data: image.data } },
|
.filter(img => img?.data && img?.media_type)
|
||||||
{ type: 'text', text: hasText || 'Analyze the image and answer any questions shown in it.' },
|
.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 {
|
} else {
|
||||||
content = text;
|
content = text;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user