dsh: add Transcribe audio (whisper) + Ask with a file tools, workspace download route, OmniRoute x-opencode-session fix, regrouped toolbar

This commit is contained in:
2026-09-12 08:45:24 +10:00
parent a71059ac1e
commit 3d1a068c3b
2 changed files with 233 additions and 25 deletions

View File

@@ -62,8 +62,10 @@
border: 1px solid var(--hairline, #e6e6e6); border-radius: 8px;
font: 15px/1.4 var(--font-family, sans-serif); resize: vertical;
}
.send-row { display: flex; justify-content: flex-end; }
#sendBtn { border: none; border-radius: 8px; padding: 9px 20px; cursor: pointer; background: var(--primary, #0075de); color: #fff; font-size: 14px; }
.send-row { display: flex; align-items: stretch; gap: 6px; }
.send-row #input { flex: 1 1 auto; }
.attach { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; border: 1px solid var(--hairline, #e6e6e6); border-radius: 8px; padding: 0 9px; cursor: pointer; font-size: 16px; color: var(--ink, #111); }
#sendBtn { border: none; border-radius: 8px; padding: 0 18px; cursor: pointer; background: var(--primary, #0075de); color: #fff; font-size: 14px; }
#toolsPanel { border-top: 1px solid var(--hairline, #e6e6e6); }
#toolsToggle {
width: 100%; text-align: left; background: none; border: none; cursor: pointer;
@@ -72,6 +74,8 @@
#toolsArrow { font-size: 11px; }
#toolsBody { padding: 0 12px 10px; }
#toolsBody.hidden { display: none; }
.tool-group { margin-top: 8px; border: 1px solid var(--hairline, #e6e6e6); border-radius: 10px; padding: 6px 10px; background: rgba(0,0,0,.02); }
.tool-group-title { font-size: 11px; font-weight: 600; color: #888; text-transform: uppercase; letter-spacing: .4px; margin-bottom: 2px; }
.tools-row { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 6px; }
.tools-row input[type=text], .tools-row input[type=file] { flex: 2 1 170px; border: 1px solid var(--hairline, #e6e6e6); border-radius: 8px; padding: 6px 10px; font-size: 13px; }
.tools-row button { flex: 0 0 auto; border: 1px solid var(--hairline, #e6e6e6); border-radius: 8px; padding: 6px 10px; cursor: pointer; background: #fff; }
@@ -113,24 +117,53 @@
<div class="chat-col">
<main id="log" class="log" aria-live="polite"></main>
<form id="chat" class="composer" autocomplete="off">
<form id="chat" class="composer" autocomplete="off">
<div class="send-row">
<label class="attach" title="Attach an image to your message">🖼
<input type="file" id="imgIn" accept="image/*" style="display:none">
</label>
<textarea id="input" rows="2" placeholder="Type your message…" autofocus></textarea>
<div class="send-row"><button type="submit" id="sendBtn">Send</button></div>
</form>
<button type="submit" id="sendBtn">Send</button>
</div>
</form>
<div id="toolsPanel">
<button type="button" id="toolsToggle"><span id="toolsArrow">▾</span> Model &amp; tools</button>
<div id="toolsBody">
<div class="modelrow"><label for="modelSel">Model:</label><select id="modelSel"></select></div>
<div class="tools-row">
<input id="urlInput" type="text" placeholder="Paste a URL to summarise…">
<button type="button" id="sumWeb">Summarise page</button>
<input id="fileInput" type="text" placeholder="Workspace file (e.g. notes.md)…">
<button type="button" id="sumDocs">Summarise file</button>
<input id="genInput" type="text" placeholder="Describe an image to generate…">
<button type="button" id="genImg">Generate image</button>
<input type="file" id="imgIn" accept="image/*" title="Attach an image">
<input type="file" id="vidIn" accept="video/*" title="Analyze a video / screen recording (GLM)">
<button type="button" id="genVid">Analyze video</button>
<div class="tool-group">
<div class="tool-group-title">Ask with a file</div>
<div class="tools-row">
<input type="file" id="askIn" accept="audio/*,video/*,image/*,.txt,.md,.json,.log,.csv" title="Any file: audio, video, image, text">
<input id="askQ" type="text" placeholder="What should I do? e.g. transcribe this, summarise, what's in this…">
<button type="button" id="askBtn">Ask</button>
</div>
</div>
<div class="tool-group">
<div class="tool-group-title">Transcribe audio → text</div>
<div class="tools-row">
<input type="file" id="audioIn" accept="audio/*,.mp4,.mov,.m4a" title="Audio or video with speech">
<button type="button" id="transcribeBtn">Transcribe</button>
</div>
</div>
<div class="tool-group">
<div class="tool-group-title">Generate image</div>
<div class="tools-row">
<input id="genInput" type="text" placeholder="Describe an image to generate…">
<button type="button" id="genImg">Generate</button>
</div>
</div>
<div class="tool-group">
<div class="tool-group-title">Workspace file / web page (legacy)</div>
<div class="tools-row">
<input id="urlInput" type="text" placeholder="Paste a URL to summarise…">
<button type="button" id="sumWeb">Summarise page</button>
<input id="fileInput" type="text" placeholder="Workspace file (e.g. notes.md)…">
<button type="button" id="sumDocs">Summarise file</button>
</div>
</div>
</div>
</div>
@@ -249,20 +282,54 @@
}
document.getElementById('sumWeb').addEventListener('click', () => runTool('/api/summarize/web', 'urlInput', 'Summarise page', 'url'));
document.getElementById('sumDocs').addEventListener('click', () => runTool('/api/summarize/docs', 'fileInput', 'Summarise file', 'path'));
document.getElementById('genVid').addEventListener('click', async () => {
const f = document.getElementById('vidIn').files[0];
function downloadLink(path, label) {
const a = document.createElement('a'); a.href = '/api/files/' + path; a.download = ''; a.textContent = label;
a.style.cssText = 'font-size:12px;color:var(--accent,#6db3f2);text-decoration:underline'; return a;
}
async function postForm(url, body) {
const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams(body) });
return res.json();
}
function fail(bubble, data) { bubble.textContent = '[error: ' + (data.error || 'unknown') + ']'; }
document.getElementById('askBtn').addEventListener('click', async () => {
const f = document.getElementById('askIn').files[0]; const q = document.getElementById('askQ').value.trim();
if (!f) return;
addBubble('user', 'Analyze video: ' + f.name); addBubble('bot', '');
const b = logEl.lastElementChild.querySelector('.bubble'); b.textContent = '…analyzing';
addBubble('user', (q ? 'Do: ' + q + ' — ' : '') + f.name); addBubble('bot', '');
const b = logEl.lastElementChild.querySelector('.bubble'); b.textContent = '…working on ' + f.name;
const fd = new FormData(); fd.append('file', f);
let path = null;
try {
const res = await fetch('/api/tool/video', { method: 'POST', body: fd });
const d = await res.json();
b.textContent = d.ok ? (d.summary || '') : '[error: ' + (d.error || 'unknown') + ']';
const up = await fetch('/api/tool/upload', { method: 'POST', body: fd }); const u = await up.json();
path = u.path; if (!u.ok) return fail(b, u);
const d = await postForm('/api/tool/askfile', { path, question: q });
if (!d.ok) return fail(b, d);
b.textContent = d.error ? '' : (d.answer || '');
if (d.error) { b.textContent = d.error; return; }
if (d.transcript && !d.answer) b.textContent = d.transcript;
} catch (err) { b.textContent = '[error: ' + err + ']'; }
logEl.scrollTop = logEl.scrollHeight;
});
document.getElementById('transcribeBtn').addEventListener('click', async () => {
const f = document.getElementById('audioIn').files[0]; if (!f) return;
addBubble('user', 'Transcribe: ' + f.name); addBubble('bot', '');
const b = logEl.lastElementChild.querySelector('.bubble'); b.textContent = '…transcribing (whisper)';
const fd = new FormData(); fd.append('file', f);
try {
const res = await fetch('/api/tool/transcribe', { method: 'POST', body: fd });
const d = await res.json();
if (!d.ok) return fail(b, d);
b.textContent = d.transcript ? d.transcript : (d.note || 'No speech detected.');
if (d.transcript) {
const dl = document.createElement('div');
dl.appendChild(downloadLink(d.txt, '⬇ download transcript (.txt)'));
dl.appendChild(document.createTextNode(' · '));
dl.appendChild(downloadLink(d.path, '⬇ audio'));
b.appendChild(dl);
}
} catch (err) { b.textContent = '[error: ' + err + ']'; }
logEl.scrollTop = logEl.scrollHeight;
});
document.getElementById('genImg').addEventListener('click', async () => {
const val = document.getElementById('genInput').value.trim(); if (!val) return;
addBubble('user', 'Generate: ' + val); addBubble('bot', '');
@@ -270,7 +337,7 @@
try {
const res = await fetch('/api/tool/image', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ prompt: val }) });
const data = await res.json();
if (data.ok) { b.textContent = ''; const im = document.createElement('img'); im.src = data.b64; im.style.cssText = 'max-width:320px;border-radius:8px;display:block'; b.appendChild(im); const cap = document.createElement('div'); cap.textContent = 'saved: ' + data.path; b.appendChild(cap); }
if (data.ok) { b.textContent = ''; const im = document.createElement('img'); im.src = data.b64; im.style.cssText = 'max-width:320px;border-radius:8px;display:block'; b.appendChild(im); const cap = document.createElement('div'); cap.appendChild(downloadLink(data.path, '⬇ download image')); b.appendChild(cap); }
else b.textContent = '[error: ' + (data.error || 'unknown') + ']';
} catch (err) { b.textContent = '[error: ' + err + ']'; }
logEl.scrollTop = logEl.scrollHeight;