anc
Ход работы не теряется при уходе с экрана; без «Записать» в шапке и предупреждения
- jobs.ts: расшифровка и ответ ИИ живут, пока открыта программа, а не
экран. Вернулся — видны процент, пришедшие реплики, текст ответа и его
вкладка; кончилось — экран перечитывает запись сам. Раньше экран
показывал «Расшифровать», хотя ядро ещё работало.
- В меню у записи — метка «расшифровка» / «ИИ», пока идёт работа.
- Кнопка «Записать» из шапки убрана (метка идущей записи осталась).
- Блок «Текст предупреждения участникам» убран из настроек, экрана
«Новая запись» и панели записи.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
a730a5c
1 октября 2026 г.
jobs.ts
// Работа, которая сейчас идёт над записями: расшифровка или ответ ИИ.
//
// Живёт, пока открыта программа, а не пока открыт экран записи. Раньше ход
// работы хранил сам экран: ушёл с него — и, вернувшись, человек видел кнопку
// «Расшифровать», хотя ядро ещё расшифровывало. Теперь экран при открытии
// берёт отсюда процент, пришедшие реплики и текст ответа, а когда работа
// кончается, обновляется сам — где бы человек ни был в это время.
import { api, errorText, events, type Command, type TranscribeResult } from "./api";
import { toast } from "./state";
export type Stage = "speech" | "voices";
export type Job =
| { kind: "transcribe"; stage: Stage; done: number; total: number; lines: string[] }
| { kind: "command"; tab: string; answer: string };
/** Что изменилось: ход работы, новые реплики, кусок ответа или конец. */
export type JobEvent =
| { type: "progress" | "stage" }
| { type: "lines"; lines: string[] }
| { type: "answer" }
| { type: "done"; kind: Job["kind"]; heading?: string };
const running = new Map<string, Job>();
const errors = new Map<string, string>();
const listeners = new Set<(id: string, e: JobEvent) => void>();
function emit(id: string, e: JobEvent): void {
listeners.forEach((cb) => cb(id, e));
}
export function jobOf(id: string): Job | null {
return running.get(id) ?? null;
}
/** Чем кончилась последняя работа над записью, если ошибкой. */
export function errorOf(id: string): string | null {
return errors.get(id) ?? null;
}
export function clearError(id: string): void {
errors.delete(id);
}
export function isBusy(id: string): boolean {
return running.has(id);
}
/** Подписка на работу над всеми записями; возвращает отписку. */
export function onJobs(cb: (id: string, e: JobEvent) => void): () => void {
listeners.add(cb);
return () => listeners.delete(cb);
}
export async function transcribe(id: string): Promise<void> {
if (running.has(id)) return;
running.set(id, { kind: "transcribe", stage: "speech", done: 0, total: 0, lines: [] });
errors.delete(id);
emit(id, { type: "progress" });
let r: TranscribeResult | null = null;
try {
r = await api.transcribe(id);
} catch (e) {
if (errorText(e) !== "Расшифровка отменена") errors.set(id, errorText(e));
}
running.delete(id);
emit(id, { type: "done", kind: "transcribe" });
if (!r) return;
if (r.failed) toast(`Не распозналось кусков: ${r.failed}. ${r.last_error ?? ""}`);
else if (r.voices_error) toast(`Расшифровка готова, но по голосам не разделилась: ${r.voices_error}`);
else if (r.voices > 1) toast(`Расшифровка готова. Говорящих по голосам: ${r.voices}`, "ok");
else toast("Расшифровка готова", "ok");
}
export async function command(id: string, cmd: Command, tab: string): Promise<void> {
if (running.has(id)) return;
running.set(id, { kind: "command", tab, answer: "" });
errors.delete(id);
emit(id, { type: "progress" });
let heading: string | undefined;
try {
heading = (await api.runCommand(id, cmd)).heading;
} catch (e) {
if (errorText(e) !== "Отменено") errors.set(id, errorText(e));
}
running.delete(id);
emit(id, { type: "done", kind: "command", heading });
}
/** Слушать ядро один раз на всю программу. */
export function startJobs(): void {
void events.progress(({ meeting_id, stage, done, total, lines }) => {
const job = running.get(meeting_id);
if (job?.kind !== "transcribe") return;
const stageChanged = job.stage !== stage;
Object.assign(job, { stage, done, total });
if (lines.length) {
job.lines.push(...lines);
emit(meeting_id, { type: "lines", lines });
}
emit(meeting_id, { type: stageChanged ? "stage" : "progress" });
});
void events.answer(({ meeting_id, text }) => {
const job = running.get(meeting_id);
if (job?.kind !== "command") return;
job.answer += text;
emit(meeting_id, { type: "answer" });
});
}