skillok meeting
57e443b222306e63eaf81d54fae10fa0a4f4ea1a
Перейти к файлу
Код
meeting srcjobs.ts
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" });
  });
}