feat(webchat): support image attachments

This commit is contained in:
William Valentin
2026-02-13 15:03:48 -08:00
parent 955b9e28e0
commit cc54b3a10c
7 changed files with 707 additions and 31 deletions
+60 -3
View File
@@ -15,6 +15,8 @@
</header>
<div class="messages" id="messages"></div>
<div class="input-area">
<button id="attach" title="Attach image">Attach</button>
<input type="file" id="file" accept="image/png,image/jpeg,image/gif,image/webp" multiple style="display:none">
<input type="text" id="input" placeholder="Type a message..." autofocus>
<button id="send">Send</button>
</div>
@@ -34,8 +36,36 @@
const messagesEl = document.getElementById('messages');
const inputEl = document.getElementById('input');
const sendBtn = document.getElementById('send');
const attachBtn = document.getElementById('attach');
const fileEl = document.getElementById('file');
const statusEl = document.getElementById('status');
let pendingAttachments = [];
function isSupportedImageMime(mimeType) {
return mimeType === 'image/jpeg'
|| mimeType === 'image/png'
|| mimeType === 'image/gif'
|| mimeType === 'image/webp';
}
function readFileAsBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = () => reject(new Error('Failed to read file'));
reader.onload = () => {
const result = String(reader.result || '');
const comma = result.indexOf(',');
if (comma === -1) {
reject(new Error('Unexpected file encoding'));
return;
}
resolve(result.slice(comma + 1));
};
reader.readAsDataURL(file);
});
}
// ── WebSocket connection ───────────────────────────────────────
function connect() {
@@ -82,13 +112,13 @@
function sendMessage() {
const text = inputEl.value.trim();
if (!text || !ws || ws.readyState !== WebSocket.OPEN || inputDisabled) return;
if ((!text && pendingAttachments.length === 0) || !ws || ws.readyState !== WebSocket.OPEN || inputDisabled) return;
requestId++;
const id = requestId;
// Display user message
appendUserMessage(text);
appendUserMessage(text || `Sent ${pendingAttachments.length} attachment(s)`);
// Create an assistant message area for tool events + final response
createAssistantArea(id);
@@ -97,14 +127,41 @@
ws.send(JSON.stringify({
id: id,
method: 'agent.send',
params: { message: text },
params: { message: text, attachments: pendingAttachments },
}));
// Clear input and disable while waiting
inputEl.value = '';
pendingAttachments = [];
fileEl.value = '';
setInputDisabled(true);
}
attachBtn.addEventListener('click', () => {
fileEl.click();
});
fileEl.addEventListener('change', async () => {
const files = Array.from(fileEl.files ?? []);
const MAX_BYTES = 5 * 1024 * 1024;
for (const file of files) {
if (!isSupportedImageMime(file.type)) {
appendErrorMessage(`Unsupported file type: ${file.type || file.name}`);
continue;
}
if (file.size > MAX_BYTES) {
appendErrorMessage(`File too large (max 5MB): ${file.name}`);
continue;
}
try {
const data = await readFileAsBase64(file);
pendingAttachments.push({ mimeType: file.type, data, filename: file.name });
} catch (err) {
appendErrorMessage(`Failed to attach ${file.name}: ${err.message}`);
}
}
});
function setInputDisabled(disabled) {
inputDisabled = disabled;
inputEl.disabled = disabled;