anc
Этап 0: каркас Tauri 2, разбор встречи через OpenRouter, стиль skillok.org
- Ядро на Rust: расшифровка и импорт текста, схема разбора со строгой
проверкой ответа модели, клиент OpenRouter (проверка ключа, разбор),
настройки и ключ в хранилище паролей ОС, встречи папками на диске.
- Интерфейс: экраны «Встречи», «Встреча» (расшифровка, конспект, задачи),
«Настройки»; токены, шрифты и кнопки как на skillok.org.
- Промпт разбора prompts/analysis.md и проверочные расшифровки.
- 30 тестов ядра; клиент OpenRouter проверяется против локальной заглушки.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
d93e31a
30 сентября 2026 г.
meetings.ts
// Экран «Встречи»: список и импорт расшифровки.
import { api, errorText, formatDate, formatDuration, type Meeting } from "../api";
import { $, busy, esc } from "../dom";
function status(m: Meeting): string {
if (!m.analysis) return `<span class="label">не разобрана</span>`;
const n = m.analysis.tasks;
return `<span class="label label-accent">разобрана · ${n} ${plural(n, "задача", "задачи", "задач")}</span>`;
}
export function plural(n: number, one: string, few: string, many: string): string {
const d = n % 10, h = n % 100;
if (d === 1 && h !== 11) return one;
if (d >= 2 && d <= 4 && (h < 12 || h > 14)) return few;
return many;
}
export async function renderMeetings(root: HTMLElement): Promise<void> {
const meetings = await api.listMeetings();
root.innerHTML = `
<div class="page-head">
<h1>Встречи</h1>
<div class="actions"><button class="ui-btn" data-open-import>Импорт расшифровки</button></div>
</div>
<form class="box" data-import hidden>
<div class="box-head"><h2>Импорт расшифровки</h2></div>
<div class="box-body">
<div class="field">
<label for="imp-title">Название</label>
<input id="imp-title" class="input" name="title" autocomplete="off" required>
</div>
<div class="field">
<label for="imp-text">Расшифровка</label>
<textarea id="imp-text" class="textarea mono" name="text" rows="10" required
placeholder="[00:00:05] Анна: Начнём с отчёта."></textarea>
<p class="field-hint">Каждая реплика — с новой строки: [чч:мм:сс] Имя: текст.</p>
</div>
<p class="error" data-import-error hidden></p>
<div class="actions">
<button class="ui-btn" type="submit">Создать встречу</button>
<button class="ui-btn" type="button" data-close-import>Отмена</button>
</div>
</div>
</form>
<section class="box">
${
meetings.length
? meetings
.map(
(m) => `
<a class="row meeting-row" href="#/meeting/${encodeURIComponent(m.id)}">
<span class="title">${esc(m.title)}</span>
<span class="when small muted">${esc(formatDate(m.created_at))} · ${esc(formatDuration(m.duration_ms))}</span>
<span class="status">${status(m)}</span>
</a>`,
)
.join("")
: `<p class="empty">Встреч пока нет.</p>`
}
</section>`;
const form = $(root, "[data-import]") as HTMLFormElement;
const error = $(root, "[data-import-error]");
$(root, "[data-open-import]").addEventListener("click", () => {
form.hidden = false;
$(form, "#imp-title").focus();
});
$(root, "[data-close-import]").addEventListener("click", () => {
form.hidden = true;
error.hidden = true;
});
form.addEventListener("submit", async (event) => {
event.preventDefault();
const data = new FormData(form);
const submit = $(form, "[type=submit]") as HTMLButtonElement;
try {
const m = await busy(submit, "Создаю…", () => api.importMeeting(String(data.get("title")), String(data.get("text"))));
location.hash = `#/meeting/${encodeURIComponent(m.id)}`;
} catch (e) {
error.textContent = errorText(e);
error.hidden = false;
}
});
}