Files
deepwork/ui.html
T
Alex CubeandClaude Opus 5 97eefb57c7 Первая версия: инструменты и цикл для DeepSeek
DeepSeek через веб-чат видит только то, что человек положил в вопрос. Здесь
у него появляется обратное направление: он возвращает блок-запрос, программа
исполняет его и досылает результат следующим сообщением в тот же чат. Обычный
агентский цикл, только транспорт — текст.

Инструменты (только чтение, только внутри рабочей папки):
  === NEED: путь, путь ===   прислать файлы целиком
  === LIST: **/*.mjs ===     список файлов по маске
  === FIND: текст ===        где встречается, с номерами строк

Запуска команд намеренно нет: исполнять строки из ответа, пришедшего из
интернета, без белого списка и подтверждения нельзя.

Мост ollama_orchestro/ask.mjs не импортируется и не копируется — он разбирает
process.argv и исполняет команду на верхнем уровне, поэтому import немедленно
запустил бы её. Вызывается подпроцессом теми флагами, которые у него уже есть:
шаг 1 без --new (правка первого сообщения), шаги 2..N с --new, иначе DeepSeek
не помнит предыдущий шаг. Взамен orchestro не меняется ни на строку, а его
правки прилетают сюда даром.

Два урока, каждый стоил живого прогона и вписан в код:
- инструкция в начале сообщения не работает. 22 КБ файла между приставкой и
  вопросом — и блоки-запросы игнорируются. Помогает дословное напоминание
  вплотную к вопросу;
- дерево папки надо давать сразу. Без него DeepSeek угадывает имена файлов и
  тратит два круга по минуте на LIST.

Есть веб-пульт (start-deepwork.cmd, бросить папку): файлы галочками, журнал
шагов по ходу, запись правок с копией в backup/.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 16:31:33 +03:00

160 lines
7.8 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 открыта'
: s.browser ? 'браузер есть, но вкладки DeepSeek нет' : 'браузер с портом 9333 не найден — запустите start-browser.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>