skillok meeting
393887285dfc8accae94edba2c7c575b098e705e
Перейти к файлу
Код
meeting srcmain.ts
anc Меню как у skillok.org, живая расшифровка и ответ ИИ, длинные записи (#13) - Логотип убран. Шапка и меню как у skillok.org: кнопка ☰ открывает панель слева (Главная, Записать, Добавить аудиофайл, записи с поиском, Настройки), закрытие крестиком, кликом мимо, Esc и переходом; крошки «Skillok Meet / запись». Список записей с поиском — на главной. - Во время записи один общий уровень вместо «Вы» и «Собеседники». - Расшифровка по ходу: реплики приходят по порядку времени (кусок уходит, когда готовы все куски раньше), текст растёт на экране. - Ответ ИИ потоком (stream: true): конспект, задачи, ревью и свой запрос пишутся на глазах. - Длинные записи: дорожка не читается в память целиком — нарезка блоками по 10 с, каждый кусок читается с диска перед отправкой; чужой файл один раз перегоняется во временный WAV. Прогон 2 ч × 2 дорожки с подделкой распознавания: 1600 кусков, последняя реплика 01:59:55, пик памяти 25 МБ. - Говорящий в файле без дорожек — «Спикер 1». Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> 3938872 30 сентября 2026 г.
main.ts
// Оболочка. Маршруты: #/ — главная, #/m/<id>[/<вкладка>] — запись, #/settings.

import { renderHome } from "./views/home";
import { renderMeeting } from "./views/meeting";
import { renderSettings } from "./views/settings";
import { refreshNav, setCrumb, startNav } from "./nav";
import { errorText, files } from "./api";
import { esc } from "./dom";
import { Scope } from "./scope";
import { addAudio, onChange, refreshRecording } from "./state";

const main = document.getElementById("app")!;
let scope = new Scope();

async function route(): Promise<void> {
  scope.dispose();
  scope = new Scope();
  const [section, id, tab] = location.hash.replace(/^#\/?/, "").split("/");
  main.scrollTop = 0;
  setCrumb(null);
  void refreshNav();
  try {
    if (section === "m" && id) {
      await renderMeeting(main, scope, decodeURIComponent(id), tab ? decodeURIComponent(tab) : null);
    } else if (section === "settings") {
      await renderSettings(main);
    } else {
      await renderHome(main, scope);
    }
  } catch (e) {
    main.innerHTML = `<div class="page"><p class="error">${esc(errorText(e))}</p></div>`;
  }
}

async function start(): Promise<void> {
  await refreshRecording();
  startNav();
  // Запись началась, встала на паузу или закончилась — шапка и меню меняются.
  onChange(() => void refreshNav());
  // Аудиофайл, брошенный в окно с любого экрана, — новая запись.
  await files.onDrop(async (paths) => {
    for (const p of paths) await addAudio(p);
  });
  window.addEventListener("hashchange", route);
  await route();
}

start().catch((e) => {
  main.innerHTML = `<p class="error">${esc(errorText(e))}</p>`;
});