skillok meeting
a730a5c5ec9fe89299948ec3d7d5d1b31cd2600a
Перейти к файлу
Код
meeting srcrecording.ts
anc Ход работы не теряется при уходе с экрана; без «Записать» в шапке и предупреждения - jobs.ts: расшифровка и ответ ИИ живут, пока открыта программа, а не экран. Вернулся — видны процент, пришедшие реплики, текст ответа и его вкладка; кончилось — экран перечитывает запись сам. Раньше экран показывал «Расшифровать», хотя ядро ещё работало. - В меню у записи — метка «расшифровка» / «ИИ», пока идёт работа. - Кнопка «Записать» из шапки убрана (метка идущей записи осталась). - Блок «Текст предупреждения участникам» убран из настроек, экрана «Новая запись» и панели записи. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> a730a5c 1 октября 2026 г.
recording.ts
// Запись: панель с таймером, уровнями и кнопками — одна на главной и на
// экране записи. Метка «идёт запись» с любого экрана — в шапке (nav.ts).

import { api, elapsed, events } from "./api";
import { esc } from "./dom";
import type { Scope } from "./scope";
import { changed, refreshRecording, state, toast } from "./state";

export function recordingPanel(): string {
  const r = state.recording;
  if (!r) return "";
  return `
    <div class="rec" data-rec>
      <p class="rec-state"><span class="dot dot-live${r.paused ? " is-paused" : ""}"></span>${
        r.paused ? "Запись на паузе" : r.system ? "Идёт запись: собеседники и вы" : "Идёт запись: только ваш микрофон"
      }</p>
      ${r.system_error ? `<p class="rec-warn small">Голоса собеседников не пишутся: ${esc(r.system_error)}</p>` : ""}
      <p class="rec-timer mono-num" data-timer>${elapsed(r.started_at)}</p>
      <div class="meter" title="Громкость того, что пишется: ваш микрофон и звук компьютера вместе">
        <span class="meter-bar"><span class="meter-fill" data-level></span></span>
      </div>
      <div class="actions">
        <button class="ui-btn" data-rec-stop>Остановить</button>
        <button class="link-btn" data-rec-pause>${r.paused ? "Продолжить" : "Пауза"}</button>
      </div>
    </div>`;
}

/** Кнопки панели и живые уровни звука. `onStop` — после остановки. */
export function bindRecording(root: HTMLElement, scope: Scope, onStop: () => void): void {
  root.querySelector<HTMLButtonElement>("[data-rec-stop]")?.addEventListener("click", async (e) => {
    const b = e.currentTarget as HTMLButtonElement;
    b.disabled = true;
    b.textContent = "Останавливаю…";
    try {
      await api.recorderStop();
      await refreshRecording();
      onStop();
    } catch (err) {
      b.disabled = false;
      toast(err);
    }
  });
  root.querySelector("[data-rec-pause]")?.addEventListener("click", async () => {
    if (!state.recording) return;
    try {
      await api.recorderPause(!state.recording.paused);
      state.recording.paused = !state.recording.paused;
      changed();
    } catch (err) {
      toast(err);
    }
  });
  // Один общий уровень: громче из дорожек за последние полсекунды. Человеку
  // важно одно — запись идёт и звук есть; делить его на «вы» и «собеседники»
  // незачем (решение владельца 01.10.2026).
  const last: Record<string, { dbfs: number; at: number }> = {};
  scope.on(
    events.recorder(({ event }) => {
      if (event.kind === "level") {
        const now = performance.now();
        last[event.data.speaker.kind] = { dbfs: event.data.dbfs, at: now };
        const dbfs = Math.max(-90, ...Object.values(last).filter((l) => now - l.at < 500).map((l) => l.dbfs));
        const fill = root.querySelector<HTMLElement>("[data-level]");
        if (fill) fill.style.width = `${Math.max(0, Math.min(100, ((dbfs + 60) / 55) * 100))}%`;
      } else {
        toast(`Дорожка «${event.data.speaker.kind === "me" ? "вы" : "собеседники"}» перестала писаться: ${event.data.error}`);
      }
    }),
  );
  scope.every(1000, () => {
    const t = root.querySelector("[data-timer]");
    if (t && state.recording) t.textContent = elapsed(state.recording.started_at);
  });
}