skillok meeting
87935b95adabf6d8cf19d1519e1834acf6fab5ff
Перейти к файлу
Код
meeting srcviewshome.ts
anc Переименование говорящих и экран «Новая запись» (#13) - Имя говорящего в расшифровке — кнопка: нажал, вписал «Ольга», Enter — подпись сменилась во всех его репликах, а имя целым словом — в конспекте, задачах и других результатах («Я» там не трогается). - «Записать встречу» в меню, «Записать» в шапке и на главной открывают экран «Новая запись»: что запишется, совет про наушники, предупреждение участникам с копированием. Запись начинается только кнопкой «Начать запись». Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> 87935b9 30 сентября 2026 г.
home.ts
// Главная: записать встречу или добавить файл, ниже — все записи с поиском,
// как список репозиториев на главной skillok.org.

import { api, files, formatDate, formatDuration, meetingDuration, type Meeting, type SettingsView } from "../api";
import { esc } from "../dom";
import { bindRecording, recordingPanel } from "../recording";
import type { Scope } from "../scope";
import { addAudio, onChange, state } from "../state";

function hero(view: SettingsView): string {
  if (state.recording) {
    return `<section class="hero is-live">${recordingPanel(view.settings.warning_text)}</section>`;
  }
  const pick = `<button class="link-btn" data-pick>выберите файл</button>`;
  if (!view.capture_supported) {
    return `
      <section class="hero drop">
        <h1>Добавьте запись встречи</h1>
        <p class="hero-text">На этой системе запись не поддерживается. Перетащите сюда аудиофайл — mp3, m4a, wav и другие.</p>
        <button class="ui-btn hero-btn" data-pick>Выбрать файл</button>
      </section>`;
  }
  return `
    <section class="hero">
      <h1>Запишите встречу</h1>
      <p class="hero-text">Zoom, Google Meet, Телемост — что угодно. Запишутся собеседники и вы, потом — расшифровка, конспект и задачи.</p>
      <a class="ui-btn hero-btn" href="#/new">Записать</a>
      <p class="small muted">Лучше в наушниках — так голоса собеседников не попадут в микрофон второй раз.</p>
    </section>
    <section class="hero drop">
      <p>Уже есть запись? Перетащите аудиофайл сюда или ${pick}.</p>
      <p class="small muted">mp3, m4a, wav, aiff, flac, ogg</p>
    </section>`;
}

function status(m: Meeting): string {
  if (m.recording) return `<span class="status status-live">Идёт запись</span>`;
  if (!m.audio.length) return `<span class="status">Нет звука</span>`;
  if (!m.transcribed_at) return `<span class="status status-todo">Не расшифровано</span>`;
  return `<span class="status">Расшифровано</span>`;
}

function list(meetings: Meeting[]): string {
  if (!meetings.length) return "";
  return `
    <section class="records">
      <div class="records-head"><h2>Записи</h2></div>
      <input class="input records-filter" type="search" data-filter placeholder="Найти запись…" aria-label="Найти запись…">
      <div class="record-list">
        ${meetings
          .map((m) => {
            const meta = [formatDate(m.created_at), formatDuration(meetingDuration(m))].filter(Boolean).join(" · ");
            return `
          <a class="record" data-row href="#/m/${encodeURIComponent(m.id)}">
            <span class="record-title">${esc(m.title)}</span>
            <span class="record-meta small muted">${esc(meta)}</span>
            ${status(m)}
          </a>`;
          })
          .join("")}
      </div>
      <p class="small muted" data-nothing hidden>Ничего не нашлось.</p>
    </section>`;
}

export async function renderHome(root: HTMLElement, scope: Scope): Promise<void> {
  const [view, meetings] = await Promise.all([api.getSettings(), api.listMeetings()]);
  if (!scope.alive) return;

  const draw = () => {
    root.innerHTML = `
      <div class="home">
        ${hero(view)}
        ${
          view.openrouter_key
            ? ""
            : `<p class="notice">Чтобы расшифровывать записи, подключите ИИ — <a href="#/settings">это минута</a>.</p>`
        }
        ${list(meetings)}
      </div>`;
    root.querySelector<HTMLInputElement>("[data-filter]")?.addEventListener("input", (e) => {
      const q = (e.target as HTMLInputElement).value.trim().toLowerCase();
      let shown = 0;
      root.querySelectorAll<HTMLElement>("[data-row]").forEach((row) => {
        row.hidden = q !== "" && !row.textContent!.toLowerCase().includes(q);
        if (!row.hidden) shown++;
      });
      root.querySelector<HTMLElement>("[data-nothing]")!.hidden = shown > 0;
    });
    root.querySelectorAll("[data-pick]").forEach((b) => b.addEventListener("click", async () => addAudio(await files.pickAudio())));
    // После «Остановить» переход к записи делает подписка ниже.
    bindRecording(root, scope, view.settings.warning_text, () => undefined);
  };
  draw();

  // Запись остановили или поставили на паузу — карточка меняется.
  let last = JSON.stringify(state.recording);
  const off = onChange(async () => {
    const now = JSON.stringify(state.recording);
    if (now === last || !scope.alive) return;
    const stoppedId = last && !state.recording ? (JSON.parse(last) as { meeting_id: string }).meeting_id : null;
    last = now;
    if (stoppedId) {
      location.hash = `#/m/${encodeURIComponent(stoppedId)}`;
      return;
    }
    draw();
  });
  scope.on(Promise.resolve(off));
}