Files
deepwork/ui.html
T
Alex CubeandClaude Opus 5 263ad14810 Пульт: выключатель инструментов, помощь с git, память прошлого ответа
Три вещи, которых не хватало по сравнению с пультом DeepShim, — теперь
здесь, и DeepShim можно закрывать.

1. Выключатель инструментов. Снят — приставка, дерево папки и напоминание
   про NEED/LIST/FIND не едут вовсе, круг ровно один: обычное "спросил
   по этим файлам, получил ответ". Замер на одной задаче: 229 символов
   вопроса против 1886. В консоли это флаг --no-tools.

2. Помощь с git: ряд всплывает, только если в папке нет репозитория или
   в .gitignore не хватает строк. Главная - backup/, куда ложатся прежние
   версии файлов перед записью. Перенесено из пульта DeepShim.

3. "Помнить прошлый ответ": галочка, по которой прошлый ответ уезжает
   вместе со следующим вопросом. Разговор из нескольких вопросов, которого
   раньше не было: стенограмма велась внутри одного вопроса и на этом всё.

Попутно два дефекта, оба найдены при проверке:

- Разбор флагов съедал аргумент после булева флага: "deepwork --apply вопрос"
  превращался в apply="вопрос", а вопрос пропадал. Молчало, пока булев флаг
  стоял последним. Та же грабля уже ловилась в ask.mjs; добавлен BOOL_FLAGS.

- Блок-запрос вплотную к прозе не распознавался. DeepSeek регулярно пишет
  "Запрашиваю поиск константы.=== FIND: MAGIC ===" одной строкой, разбор
  требовал начала строки, и круг уходил впустую. Якорь начала снят, якорь
  конца оставлен: упоминание формата посреди фразы запросом не считается.
  От запросов внутри === FILE: === бережёт вырезание этих блоков, а не якорь.

Проверено вживую: без инструментов - 229 символов, один круг, верный ответ;
с инструментами - FIND распознан, два круга, верный ответ; git - repo:false
и пять недостающих строк, после кнопки repo:true и missing пуст; память -
"удвой это число" вернуло 80834 против бессмыслицы без галочки.
--selftest дополнен двумя случаями на разбор запросов.

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

209 lines
10 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>
<div class="row" id="gitrow" hidden>
<button id="gitinit">Инициализировать git</button>
<span class="muted" id="gitnote"></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" title="Разрешить модели самой запрашивать файлы блоками NEED / LIST / FIND. Выключено — один круг и только отмеченные файлы.">
<input type="checkbox" id="tools" checked> инструменты</label>
<label class="muted" id="stepsbox">шагов <select id="steps" style="width:auto">
<option>2</option><option selected>4</option><option>6</option><option>8</option></select></label>
<label class="muted" title="Прошлый ответ уедет вместе со следующим вопросом.">
<input type="checkbox" id="keep"> помнить прошлый ответ</label>
<span class="muted" id="status"></span>
</div>
<div class="scroll" id="out"></div>
</main>
<script>
let files = [];
let lastAnswer = ''; // для галочки «помнить прошлый ответ»
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);
// Без инструментов круг ровно один — выбор числа шагов теряет смысл.
$('tools').addEventListener('change', () => {
$('stepsbox').style.opacity = $('tools').checked ? '' : '.4';
$('steps').disabled = !$('tools').checked;
});
$('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();
});
// Ряд с git всплывает, только когда он нужен: репозитория нет или в .gitignore
// не хватает строк. Главная из них — backup/: туда ложатся прежние версии
// файлов перед записью, и однажды они уедут в коммит.
async function gitState() {
try {
const g = await fetch('/api/git').then(r => r.json());
const need = !g.repo || g.missing.length;
$('gitrow').hidden = !need;
if (!need) return;
$('gitinit').textContent = g.repo ? 'Дополнить .gitignore' : 'Инициализировать git';
$('gitnote').textContent = g.repo ? `не хватает: ${g.missing.join(', ')}`
: 'папка не под git';
} catch { $('gitrow').hidden = true; }
}
gitState();
$('gitinit').onclick = async () => {
$('gitinit').disabled = true;
const r = await fetch('/api/git/init', { method: 'POST' }).then(x => x.json());
$('gitnote').textContent = r.ok
? (r.added.length ? `добавлено в .gitignore: ${r.added.join(', ')}` : 'всё уже на месте')
: `не вышло: ${r.reason}`;
$('gitinit').disabled = false;
if (r.ok) setTimeout(gitState, 1200);
};
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,
tools: $('tools').checked,
// Галочку между вопросами не сбрасываем: если человек ведёт разговор,
// он ведёт его несколькими вопросами подряд.
context: $('keep').checked ? lastAnswer : '',
}),
});
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) {
lastAnswer = d.answer || '';
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>