Files
family_home_lab/dsh/templates/chat.html

228 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DeepSeek Harness — {{ DSH_USER }}</title>
<link rel="stylesheet" href="/static/app.css">
<style>
html, body { height: 100%; margin: 0; }
body { display: flex; flex-direction: column; }
header.top { flex: none; }
.app { display: flex; flex: 1; min-height: 0; }
#sidebar {
width: 230px; flex: none; border-right: 1px solid var(--hairline, #e6e6e6);
display: flex; flex-direction: column; background: var(--canvas-soft, #f7f7f7);
}
#sidebar h2 { font: 600 12px/1.3 var(--font-family, sans-serif); color: var(--ink-faint, #999); margin: 12px 12px 6px; }
#newBtn, #dlBtn { margin: 4px 12px; padding: 7px 10px; border: 1px solid #ddd; border-radius: 8px; background: #fff; cursor: pointer; text-align: left; }
#sessions { list-style: none; margin: 0; padding: 0 8px; overflow: auto; flex: 1; }
#sessions li {
display: flex; align-items: center; gap: 6px; padding: 7px 8px; margin: 2px 0;
border-radius: 8px; cursor: pointer; font-size: 13px; color: var(--ink-secondary, #333);
}
#sessions li.sel { background: var(--primary, #0075de); color: #fff; }
#sessions li .t { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#sessions li .n { font-size: 11px; opacity: .7; }
#sessions li .del { background: none; border: none; cursor: pointer; color: inherit; opacity: .5; font-size: 13px; }
.chat-col { display: flex; flex-direction: column; flex: 1; min-width: 0; }
.log { flex: 1; overflow: auto; }
.bubble { position: relative; white-space: pre-wrap; }
.bubble .copy {
position: absolute; top: 4px; right: 4px; display: none;
font-size: 11px; padding: 2px 6px; border: 1px solid #ddd; border-radius: 6px; background: #fff; cursor: pointer; color: #555;
}
.msg:hover .bubble .copy { display: block; }
.modelrow { display: flex; gap: 8px; align-items: center; }
.modelrow select { border: 1px solid #ddd; border-radius: 8px; padding: 6px 8px; font-size: 13px; }
.modelrow label { font-size: 12px; color: #777; }
</style>
</head>
<body>
<header class="top">
<div class="logo">DeepSeek Harness</div>
<div class="who">{{ DSH_SLOT }} · {{ DSH_USER }} · <span class="model">{{ DSH_MODEL }}</span></div>
</header>
<div class="app">
<aside id="sidebar">
<h2>Conversations</h2>
<button id="newBtn" type="button">+ New conversation</button>
<button id="dlBtn" type="button">⬇ Download current (.md)</button>
<ul id="sessions"></ul>
</aside>
<div class="chat-col">
<main id="log" class="log" aria-live="polite"></main>
<div class="modelrow" style="padding:6px 12px 2px;gap:8px">
<label for="modelSel">Model:</label>
<select id="modelSel"></select>
</div>
<form id="chat" class="composer" autocomplete="off">
<input id="input" type="text" placeholder="Type your message…" autofocus />
<button type="submit">Send</button>
<input type="file" id="imgIn" accept="image/*" title="Attach an image" style="max-width:150px;font-size:12px">
</form>
<div class="composer" style="display:flex;gap:8px;flex-wrap:wrap">
<input id="urlInput" type="text" placeholder="Paste a URL to summarise…" style="flex:2;min-width:220px;border:1px solid #ddd;border-radius:8px;padding:6px 10px" />
<button type="button" id="sumWeb" style="border:1px solid #ddd;border-radius:8px;padding:6px 10px;cursor:pointer">Summarise page</button>
<input id="fileInput" type="text" placeholder="Workspace file (e.g. notes.md)…" style="flex:2;min-width:180px;border:1px solid #ddd;border-radius:8px;padding:6px 10px" />
<button type="button" id="sumDocs" style="border:1px solid #ddd;border-radius:8px;padding:6px 10px;cursor:pointer">Summarise file</button>
<input id="genInput" type="text" placeholder="Describe an image to generate…" style="flex:2;min-width:200px;border:1px solid #ddd;border-radius:8px;padding:6px 10px" />
<button type="button" id="genImg" style="border:1px solid #ddd;border-radius:8px;padding:6px 10px;cursor:pointer">Generate image</button>
</div>
</div>
</div>
<script>
const logEl = document.getElementById('log');
const form = document.getElementById('chat');
const input = document.getElementById('input');
const listEl = document.getElementById('sessions');
const modelSel = document.getElementById('modelSel');
let curSid = localStorage.getItem('dshSid') || null;
function addBubble(role, text) {
const row = document.createElement('div');
row.className = 'msg ' + (role === 'user' ? 'user' : 'bot');
const b = document.createElement('div');
b.className = 'bubble';
b.textContent = text || '';
const cp = document.createElement('button'); cp.className = 'copy'; cp.textContent = 'copy';
cp.addEventListener('click', () => {
navigator.clipboard.writeText(b.textContent).then(() => { cp.textContent = '✓'; setTimeout(() => cp.textContent = 'copy', 1200); });
});
b.appendChild(cp);
row.appendChild(b);
logEl.appendChild(row);
logEl.scrollTop = logEl.scrollHeight;
return b;
}
function appendChar(bubble, ch) { bubble.textContent += ch; logEl.scrollTop = logEl.scrollHeight; }
function showGreeting() { if (!logEl.childElementCount) addBubble('bot', 'Hi {{ DSH_USER }} 👋 Ask me anything — pick a conversation on the left or start a new one.'); }
function renderMessages(msgs) { logEl.innerHTML = ''; (msgs || []).forEach(m => { if (m.role === 'user') addBubble('user', m.content); else if (m.role === 'assistant') addBubble('bot', m.content); }); showGreeting(); }
function renderList(sessions) {
listEl.innerHTML = '';
sessions.forEach(s => {
const li = document.createElement('li'); if (s.id === curSid) li.className = 'sel';
const t = document.createElement('span'); t.className = 't'; t.textContent = s.name || 'Conversation';
const n = document.createElement('span'); n.className = 'n'; n.textContent = s.count;
const d = document.createElement('button'); d.className = 'del'; d.textContent = '✕'; d.title = 'Delete';
d.addEventListener('click', (e) => { e.stopPropagation(); deleteSession(s.id); });
li.append(t, n, d);
li.addEventListener('click', () => openSession(s.id));
listEl.appendChild(li);
});
}
async function fetchModels() {
try {
const d = await (await fetch('/api/models')).json();
(d.current || []).forEach(m => {
const o = document.createElement('option'); o.value = m; o.textContent = m;
if (m === d.default) o.selected = true;
modelSel.appendChild(o);
});
} catch (_) {}
}
async function loadSessions() {
try {
const d = await (await fetch('/api/sessions')).json();
renderList(d.sessions || []);
if (!curSid || !(d.sessions || []).some(s => s.id === curSid)) curSid = (d.sessions && d.sessions[0] && d.sessions[0].id) || null;
if (curSid) { localStorage.setItem('dshSid', curSid); openSession(curSid); } else showGreeting();
} catch (_) { showGreeting(); }
}
async function openSession(id) {
curSid = id; localStorage.setItem('dshSid', id);
try {
const d = await (await fetch('/api/sessions/' + id)).json();
renderMessages(d.messages);
if (d.model && [...modelSel.options].some(o => o.value === d.model)) modelSel.value = d.model;
} catch (_) { renderMessages([]); }
renderList((await (await fetch('/api/sessions')).json()).sessions || []);
}
async function newSession() {
const d = await (await fetch('/api/sessions', { method: 'POST' })).json();
curSid = d.id; localStorage.setItem('dshSid', d.id);
renderMessages([]); loadSessions();
}
async function deleteSession(id) {
await fetch('/api/sessions/' + id + '/delete', { method: 'POST' });
if (curSid === id) { curSid = null; localStorage.removeItem('dshSid'); }
loadSessions();
}
function downloadSession() {
if (!curSid) return;
fetch('/api/sessions/' + curSid).then(r => r.json()).then(d => {
let md = "# " + (d.name || 'Conversation') + "\n\n";
(d.messages || []).forEach(m => { md += (m.role === 'user' ? '**You:** ' : '**Assistant:** ') + m.content + '\n\n'; });
const blob = new Blob([md], { type: 'text/markdown' });
const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
a.download = ((d.name || 'conversation').replace(/[^\w]+/g, '_')) + '.md'; a.click();
});
}
document.getElementById('newBtn').addEventListener('click', newSession);
document.getElementById('dlBtn').addEventListener('click', downloadSession);
async function runTool(endpoint, fieldId, label, bodyKey) {
const val = document.getElementById(fieldId).value.trim(); if (!val) return;
addBubble('user', label + ': ' + val); addBubble('bot', '');
const bubble = logEl.lastElementChild.querySelector('.bubble'); bubble.textContent = '…thinking';
try {
const res = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ [bodyKey]: val }) });
const data = await res.json();
bubble.textContent = data.ok ? (data.summary || '') : '[error: ' + (data.error || 'unknown') + ']';
} catch (err) { bubble.textContent = '[error: ' + err + ']'; }
logEl.scrollTop = logEl.scrollHeight;
}
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('genImg').addEventListener('click', async () => {
const val = document.getElementById('genInput').value.trim(); if (!val) return;
addBubble('user', 'Generate: ' + val); addBubble('bot', '');
const b = logEl.lastElementChild.querySelector('.bubble'); b.textContent = '…generating';
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); }
else b.textContent = '[error: ' + (data.error || 'unknown') + ']';
} catch (err) { b.textContent = '[error: ' + err + ']'; }
logEl.scrollTop = logEl.scrollHeight;
});
const imgIn = document.getElementById('imgIn'); window.__img = null;
imgIn.addEventListener('change', () => { const f = imgIn.files[0]; if (!f) return; const r = new FileReader(); r.onload = () => { window.__img = r.result; imgIn.title = f.name + ' ✓'; }; r.readAsDataURL(f); });
form.addEventListener('submit', async (e) => {
e.preventDefault();
const text = input.value.trim(); const img = window.__img || '';
if (!text && !img) return;
if (!curSid) await newSession();
input.value = ''; window.__img = null; imgIn.value = '';
const model = modelSel.value || '';
addBubble('user', text || '(attached image)');
if (img) { const im = document.createElement('img'); im.src = img; im.style.cssText = 'max-width:180px;border-radius:8px;display:block;margin-top:6px'; logEl.lastElementChild.querySelector('.bubble').appendChild(im); }
const bubble = addBubble('bot', ''); bubble.textContent = '…thinking';
let started = false;
try {
const body = new URLSearchParams({ message: text, session_id: curSid, model });
if (img) body.set('image', img);
const res = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body });
const reader = res.body.getReader(); const decoder = new TextDecoder(); let buf = '';
while (true) { const { done, value } = await reader.read(); if (done) break; buf += decoder.decode(value, { stream: true }); const parts = buf.split('\n\n'); buf = parts.pop(); for (const part of parts) { if (!part.startsWith('data: ')) continue; try { const data = JSON.parse(part.slice(6)); if (data.c) { if (!started) { bubble.textContent = ''; started = true; } appendChar(bubble, data.c); } else if (data.e) { if (!started) bubble.textContent = ''; started = true; appendChar(bubble, '\n[error: ' + data.e + ']'); } } catch (_) {} } }
addBubble('bot', '').remove();
} catch (err) { if (!started) bubble.textContent = ''; appendChar(bubble, '\n[connection error]'); }
loadSessions();
});
fetchModels(); loadSessions();
</script>
</body>
</html>