Files
family_home_lab/dsh/templates/chat.html

207 lines
11 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: 240px; 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 { margin: 6px 12px 8px; 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; }
#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; }
#sessions li .del:hover { opacity: 1; }
.chat-col { display: flex; flex-direction: column; flex: 1; min-width: 0; }
.log { flex: 1; overflow: auto; }
</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>
<ul id="sessions"></ul>
</aside>
<div class="chat-col">
<main id="log" class="log" aria-live="polite"></main>
<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');
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 || '';
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 conversation';
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 loadSessions() {
try {
const r = await fetch('/api/sessions'); const d = await r.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 r = await fetch('/api/sessions/' + id); const d = await r.json();
renderMessages(d.messages);
} 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();
}
document.getElementById('newBtn').addEventListener('click', newSession);
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 = '';
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); }
addBubble('bot', '');
const bubble = logEl.lastElementChild.querySelector('.bubble');
try {
const body = new URLSearchParams({ message: text, session_id: curSid });
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) appendChar(bubble, data.c); else if (data.e) appendChar(bubble, '\n[error: ' + data.e + ']'); } catch (_) {} } }
addBubble('bot', '').remove();
} catch (err) { appendChar(bubble, '\n[connection error]'); }
loadSessions(); // reflect updated ordering/count + rename
});
loadSessions();
</script>
</body>
</html>