feat(webchat): support image attachments
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user