Files
deepwork/ui.html
T
Alex CubeandClaude Opus 5 44f07c8af9 DeepSeek через deepseek-api; память шагов переехала из чата в стенограмму
Транспорт: был дочерний процесс ask.mjs из ollama_orchestro с обменом через
временные файлы, стал POST /api/chat к сервису deepseek-api. Он держит вкладку
браузера и отдаёт наружу протокол Ollama, поэтому здесь про браузер больше
не знают: ушли проверка существования моста, временные файлы шагов вместе
с флагом --keep-temp и четыре осиротевших импорта.

Главное не в транспорте. Раньше шаги 2..N уходили с флагом --new, и ход
разговора помнил сам чат DeepSeek. Сервис памяти не имеет принципиально:
каждый запрос он кладёт в чат целиком, затирая прошлый. Поэтому стенограмму
ведём здесь — growTranscript подклеивает к отправленному тексту прошлый ответ
модели между метками === ТВОЙ ПРЕДЫДУЩИЙ ОТВЕТ === и === END ===, и следующим
шагом уезжает всё сразу.

Так надёжнее, чем было: видно в одном месте, что именно уедет модели, а чужой
потребитель, вклинившийся между нашими шагами, ничего не портит — мы всё
приносим заново. Плата — вопрос растёт на длину прошлого ответа.

--check теперь различает три состояния: сервис отвечает и браузер поднят;
сервис жив, но браузера ещё нет; сервис не запущен. Плашка в ui.html тоже.

Настройки: bridge/chat/cdpPort заменены на deepseekHost и deepseekModel.

Проверено живым многошаговым прогоном: шаг 1 — 2094 символа, ответ
NEED: deepwork.mjs; шаг 2 — 28 447 символов стенограммы, ответ дословно верный
и по вопросу из ПЕРВОГО сообщения, то есть память шагов работает. Два шага
за 17 с. --selftest пройден, --check зелёный.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 13:40:09 +03:00

161 lines
7.9 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>
<meta charset="utf-8">
<title>deepwork</title>
<style>
:root { color-scheme: dark; }
* { box-sizing: border-box; }
body { margin: 0; height: 100vh; display: grid; grid-template-columns: 320px 1fr;
font: 14px/1.45 system-ui, "Segoe UI", sans-serif; background: #14161a; color: #e6e8ec; }
aside { border-right: 1px solid #262a31; display: flex; flex-direction: column; min-height: 0; }
main { display: flex; flex-direction: column; min-height: 0; }
h2 { font-size: 13px; text-transform: uppercase; letter-spacing: .06em; color: #8b93a1;
margin: 0; padding: 12px; border-bottom: 1px solid #262a31; }
.scroll { overflow: auto; flex: 1; padding: 8px 12px; min-height: 0; }
label.f { display: flex; gap: 8px; align-items: baseline; padding: 2px 0; cursor: pointer; }
label.f span { color: #6c7381; font-size: 12px; margin-left: auto; }
.row { display: flex; gap: 8px; align-items: center; padding: 10px 12px; border-top: 1px solid #262a31; }
.muted { color: #8b93a1; font-size: 12px; }
input[type=text], textarea, select { background: #1b1e24; color: #e6e8ec; border: 1px solid #333844;
border-radius: 6px; padding: 8px; font: inherit; width: 100%; }
textarea { resize: vertical; min-height: 84px; }
button { background: #262b34; color: #e6e8ec; border: 1px solid #3a414f; border-radius: 6px;
padding: 7px 14px; font: inherit; cursor: pointer; }
button:hover:not(:disabled) { background: #2f3540; }
button.primary { background: #3b6ea5; border-color: #4a83c0; }
button:disabled { opacity: .5; cursor: default; }
pre { background: #1b1e24; border: 1px solid #262a31; border-radius: 6px; padding: 10px;
white-space: pre-wrap; word-break: break-word; margin: 0 0 12px; }
.step { font-family: ui-monospace, Consolas, monospace; font-size: 12px; color: #9fd08b; }
.dot { width: 8px; height: 8px; border-radius: 50%; background: #7a4; display: inline-block; }
.dot.bad { background: #c55; }
.patch { border: 1px solid #333844; border-radius: 6px; margin-bottom: 10px; }
.patch > .row { border-top: none; }
.patch pre { margin: 0 12px 12px; max-height: 300px; overflow: auto; }
</style>
<aside>
<h2>Файлы <span class="muted" id="root"></span></h2>
<div class="row"><input type="text" id="filter" placeholder="фильтр по имени"></div>
<div class="scroll" id="tree">загружаю…</div>
<div class="row"><button id="none">Снять все</button><span class="muted" id="sel">ничего не выбрано</span></div>
</aside>
<main>
<h2><span class="dot" id="dot"></span> <span id="bar">проверяю мост…</span></h2>
<div style="padding:12px">
<textarea id="q" placeholder="Например: почему ответ приходит обрезанным? Дай исправленную функцию целиком."></textarea>
</div>
<div class="row">
<button class="primary" id="send">Спросить</button>
<label class="muted">шагов <select id="steps" style="width:auto">
<option>2</option><option selected>4</option><option>6</option><option>8</option></select></label>
<span class="muted" id="status"></span>
</div>
<div class="scroll" id="out"></div>
</main>
<script>
let files = [];
const $ = id => document.getElementById(id);
const esc = s => s.replace(/[&<>]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c]));
const chosen = () => [...document.querySelectorAll('#tree input:checked')].map(i => i.value);
function renderTree() {
const needle = $('filter').value.toLowerCase();
const keep = new Set(chosen());
$('tree').innerHTML = files
.filter(f => !needle || f.path.toLowerCase().includes(needle))
.map(f => `<label class="f"><input type="checkbox" value="${esc(f.path)}"${keep.has(f.path) ? ' checked' : ''}>
${esc(f.path)}<span>${Math.max(1, Math.round(f.size / 1024))} КБ</span></label>`)
.join('') || '<div class="muted">ничего не подошло</div>';
count();
}
function count() {
const c = chosen();
const kb = files.filter(f => c.includes(f.path)).reduce((s, f) => s + f.size, 0) / 1024;
$('sel').textContent = c.length ? `${c.length} шт., ${Math.round(kb)} КБ` : 'ничего не выбрано';
}
$('tree').addEventListener('change', count);
$('filter').addEventListener('input', renderTree);
$('none').onclick = () => { document.querySelectorAll('#tree input').forEach(i => i.checked = false); count(); };
fetch('/api/tree').then(r => r.json()).then(d => {
files = d.files; $('root').textContent = d.root; renderTree();
});
async function status() {
try {
const s = await fetch('/api/status').then(r => r.json());
$('dot').className = 'dot' + (s.deepseek ? '' : ' bad');
$('bar').textContent = s.deepseek ? 'deepseek-api на месте, вкладка DeepSeek открыта'
: s.service ? 'deepseek-api отвечает, браузер ещё не поднят — встанет на первом вопросе'
: 'deepseek-api не запущен — запустите start.cmd в его папке';
} catch { $('dot').className = 'dot bad'; $('bar').textContent = 'пульт не отвечает'; }
}
status(); setInterval(status, 5000);
// Пока идёт прогон, страница не должна молчать: круг занимает до 4 минут,
// и без счётчика непонятно, работает ли что-то вообще.
let poll = null;
function watch() {
poll = setInterval(async () => {
const p = await fetch('/api/progress').then(r => r.json()).catch(() => null);
if (!p) return;
$('status').textContent = `идёт ${p.seconds} с`;
$('out').innerHTML = '<pre>' + p.steps.map(s => `<div class="step">${esc(s)}</div>`).join('') + '</pre>';
}, 2000);
}
$('send').onclick = async () => {
const question = $('q').value.trim();
if (!question) return;
$('send').disabled = true;
$('out').innerHTML = '<pre class="muted">отправляю…</pre>';
watch();
try {
const r = await fetch('/api/ask', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ question, files: chosen(), steps: +$('steps').value }),
});
const d = await r.json();
clearInterval(poll);
if (d.error) { $('out').innerHTML = `<pre style="color:#e88">${esc(d.error)}</pre>`; return; }
render(d);
} catch (e) {
clearInterval(poll);
$('out').innerHTML = `<pre style="color:#e88">${esc(String(e))}</pre>`;
} finally { $('send').disabled = false; $('status').textContent = ''; }
};
function render(d) {
const steps = d.steps.map(s => `<div class="step">${esc(s)}</div>`).join('');
const patches = d.patches.map((p, i) => `
<div class="patch">
<div class="row"><b>${esc(p.path)}</b>
<span class="muted">${p.content.split('\n').length} строк</span>
<button data-i="${i}" style="margin-left:auto">Записать</button>
<span class="muted" id="note${i}"></span></div>
<pre>${esc(p.content.slice(0, 4000))}${p.content.length > 4000 ? '\n…' : ''}</pre>
</div>`).join('');
$('out').innerHTML = `<pre>${steps}</pre>
<h2 style="border:none;padding:0 0 8px">Ответ</h2>
<pre>${esc(d.answer)}</pre>
${d.patches.length ? `<h2 style="border:none;padding:0 0 8px">Файлы в ответе</h2>${patches}` : ''}`;
$('out').querySelectorAll('.patch button').forEach(b => b.onclick = async () => {
const p = d.patches[+b.dataset.i];
b.disabled = true;
const r = await fetch('/api/apply', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: p.path, content: p.content }),
}).then(x => x.json());
document.getElementById('note' + b.dataset.i).textContent =
r.ok ? (r.backup ? `записан, копия: ${r.backup}` : 'записан (новый файл)') : `не записан: ${r.reason}`;
});
}
</script>