skillok meeting
e581846c7ddcb785b10d499fa25b4c0cde98f88f
Перейти к файлу
Код
meeting srcviewsnew.ts
anc Проверка звука до записи и предупреждение, если дорожка молчит Встреча, которая молча не записалась, — худшее для программы: человек узнаёт об этом после встречи. - «Новая запись»: проверка звука — микрофон и звук компьютера открываются без записи в файл (audio_check_start/stop), две живые полоски «слышно / тишина». Устройство не открылось — причина и где разрешить (на Mac — «Запись экрана и системного звука»). - Во время записи: дорожка минуту тише −60 dBFS — событие silent, сообщение и строка на панели записи; звук вернулся — heard, строка уходит. На паузе тишина не считается; звук компьютера, который в тишине не приходит вовсе, тоже ловится. - Начало записи гасит проверку: устройства не заняты дважды. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> 57e443b 1 октября 2026 г.
121 строка 6.1 КБ
new.ts
// Экран «Новая запись»: «Записать встречу» ведёт сюда, а не запускает запись
// сразу (решение владельца 01.10.2026). Здесь проверка звука — две живые
// полоски, микрофон и звук компьютера, — и совет про наушники; запись
// начинается только кнопкой «Начать запись».
//
// Проверка — главная защита от встречи, которая молча не записалась: что
// звука собеседников нет, человек видит до встречи, а не после.

import { api, events, type CheckView } from "../api";
import { esc } from "../dom";
import type { Scope } from "../scope";
import { startRecording, state } from "../state";

export async function renderNew(root: HTMLElement, scope: Scope): Promise<void> {
  // Запись уже идёт — вторую не начать, показываем идущую.
  if (state.recording) {
    location.replace(`#/m/${encodeURIComponent(state.recording.meeting_id)}`);
    return;
  }
  const view = await api.getSettings();
  if (!scope.alive) return;

  if (!view.capture_supported) {
    root.innerHTML = `
      <div class="page">
        <section class="card">
          <h1>Новая запись</h1>
          <p>На этой системе запись не поддерживается. Добавьте готовый аудиофайл — перетащите его в окно или выберите через меню.</p>
        </section>
      </div>`;
    return;
  }

  root.innerHTML = `
    <div class="page">
      <section class="card prep">
        <h1>Новая запись</h1>
        <div class="prep-block">
          <p class="field-label">Проверка звука</p>
          <div class="check" data-check>
            ${checkRow("me", "Микрофон", "скажите что-нибудь")}
            ${checkRow("others", "Звук компьютера", "включите любой звук — видео, музыку; на встрече это голоса собеседников")}
          </div>
        </div>
        <p class="muted">Лучше в наушниках — так голоса собеседников не попадут в микрофон второй раз.</p>
        ${
          view.openrouter_key
            ? ""
            : `<p class="notice">Записать можно и без ИИ. Чтобы потом расшифровать, подключите его в <a href="#/settings">настройках</a>.</p>`
        }
        <button class="ui-btn hero-btn" data-start>Начать запись</button>
      </section>
    </div>`;
  // Проверка звука: уровни приходят, пока открыт экран; уходим — устройства
  // закрываются. «Начать запись» ядро гасит проверку само.
  let check: CheckView | null = null;
  try {
    check = await api.audioCheckStart();
  } catch (e) {
    check = { mic_error: String(e), system_error: String(e) };
  }
  if (!scope.alive) {
    void api.audioCheckStop();
    return;
  }
  showError(root, "me", check.mic_error);
  showError(root, "others", check.system_error);
  const heard: Record<string, number> = {};
  scope.on(
    events.recorder(({ meeting_id, event }) => {
      if (meeting_id || event.kind !== "level") return;
      const key = event.data.speaker.kind === "me" ? "me" : "others";
      const row = root.querySelector<HTMLElement>(`[data-check-row="${key}"]`);
      if (!row || row.dataset.error) return;
      const fill = row.querySelector<HTMLElement>(".meter-fill");
      if (fill) fill.style.width = `${Math.max(0, Math.min(100, ((event.data.dbfs + 60) / 55) * 100))}%`;
      if (event.data.dbfs > -45) heard[key] = performance.now();
      const ok = performance.now() - (heard[key] ?? -1e9) < 3_000;
      row.classList.toggle("is-ok", ok);
      row.querySelector("[data-check-state]")!.textContent = ok ? "слышно" : "тишина";
    }),
  );
  scope.on(Promise.resolve(() => void api.audioCheckStop()));

  root.querySelector<HTMLButtonElement>("[data-start]")!.addEventListener("click", async (e) => {
    const b = e.currentTarget as HTMLButtonElement;
    b.disabled = true;
    b.textContent = "Начинаю…";
    await startRecording();
    // Не началась — ошибку уже показало сообщение внизу, кнопку возвращаем.
    if (scope.alive && !state.recording) {
      b.disabled = false;
      b.textContent = "Начать запись";
    }
  });
}

function checkRow(key: "me" | "others", title: string, hint: string): string {
  return `
    <div class="check-row" data-check-row="${key}">
      <span class="check-title">${title}</span>
      <span class="meter-bar"><span class="meter-fill"></span></span>
      <span class="check-state" data-check-state>тишина</span>
      <span class="check-hint xs muted" data-check-hint>${esc(hint)}</span>
    </div>`;
}

/** Устройство не открылось — вместо полоски причина и где разрешить. */
function showError(root: HTMLElement, key: "me" | "others", error: string | null): void {
  if (!error) return;
  const row = root.querySelector<HTMLElement>(`[data-check-row="${key}"]`);
  if (!row) return;
  row.dataset.error = "1";
  row.classList.add("is-error");
  row.querySelector("[data-check-state]")!.textContent = "не пишется";
  const where =
    key === "others"
      ? " На Mac: Системные настройки → Конфиденциальность и безопасность → «Запись экрана и системного звука» → включите Skillok Meet и перезапустите программу."
      : " Проверьте, что микрофон подключён и программе разрешён доступ к нему.";
  row.querySelector("[data-check-hint]")!.textContent = error + where;
}