skillok meeting
43a49bdad7084369e41933f51c5fa3e45dec2040
Перейти к файлу
Код
meeting srcside.ts
anc Интерфейс как у MacWhisper в стиле Skillok, только светлая тема - Слева библиотека записей: «Записать», «Добавить аудиофайл», список, «Настройки» внизу; пока идёт запись — метка с таймером вместо кнопки. - Экран записи: сверху название и «Копировать», расшифровка по репликам (время — кто — текст), под ней полоса «Дальше» с Конспектом, Задачами, Ревью и своим запросом. Шаги-«лесенка» убраны. - Ход расшифровки — крупный процент и полоса. - Тёмная тема убрана, окно Tauri всегда светлое. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> 43a49bd 30 сентября 2026 г.
side.ts
// Боковая панель, как библиотека у MacWhisper: «Записать» сверху, все записи
// списком, «Настройки» внизу. Видна с любого экрана; пока идёт запись, вместо
// кнопки — метка с таймером.

import { api, elapsed, files, formatDate, formatDuration, meetingDuration, type Meeting } from "./api";
import { esc } from "./dom";
import { addAudio, startRecording, state } from "./state";

const side = () => document.getElementById("side")!;
let meetings: Meeting[] = [];
let captureSupported = true;

function currentId(): string | null {
  const [section, id] = location.hash.replace(/^#\/?/, "").split("/");
  return section === "m" && id ? decodeURIComponent(id) : null;
}

function item(m: Meeting, current: string | null): string {
  const meta = [formatDate(m.created_at), formatDuration(meetingDuration(m))].filter(Boolean).join(" · ");
  const mark = m.recording
    ? `<span class="side-mark is-live"><span class="dot dot-live"></span>запись</span>`
    : m.audio.length && !m.transcribed_at
      ? `<span class="side-mark">не расшифровано</span>`
      : "";
  return `
    <a class="side-item" href="#/m/${encodeURIComponent(m.id)}"${m.id === current ? ` aria-current="page"` : ""}>
      <span class="side-title">${esc(m.title)}</span>
      <span class="side-meta">${esc(meta)}${mark}</span>
    </a>`;
}

function draw(): void {
  const r = state.recording;
  const current = currentId();
  const top = r
    ? `<a class="side-live" href="#/m/${encodeURIComponent(r.meeting_id)}">
         <span class="dot dot-live${r.paused ? " is-paused" : ""}"></span>${r.paused ? "Пауза" : "Идёт запись"}
         <span class="mono-num" data-side-timer>${elapsed(r.started_at)}</span>
       </a>`
    : captureSupported
      ? `<button class="ui-btn side-rec" data-side-record>Записать</button>`
      : "";
  side().innerHTML = `
    <a class="brand" href="#/">
      <svg class="brand-mark" viewBox="0 0 1024 1024" aria-hidden="true">
        <rect width="1024" height="1024" fill="var(--color-accent)"/>
        <path fill="var(--background)" d="M232 792V232h136l144 232 144-232h136v560H672V440L540 648h-56L352 440v352z"/>
      </svg>
      Skillok Meet
    </a>
    <div class="side-actions">
      ${top}
      <button class="link-btn" data-side-pick>Добавить аудиофайл</button>
    </div>
    <p class="side-head">Записи${meetings.length ? ` <span class="ui-nav-count">${meetings.length}</span>` : ""}</p>
    <nav class="side-list">
      ${meetings.length ? meetings.map((m) => item(m, current)).join("") : `<p class="side-empty">Здесь появятся ваши записи.</p>`}
    </nav>
    <a class="side-foot" href="#/settings"${location.hash.startsWith("#/settings") ? ` aria-current="page"` : ""}>Настройки</a>`;
  side().querySelector("[data-side-record]")?.addEventListener("click", startRecording);
  side().querySelector("[data-side-pick]")?.addEventListener("click", async () => addAudio(await files.pickAudio()));
}

/** Перечитать записи и перерисовать панель: после записи, расшифровки, правки. */
export async function refreshSide(): Promise<void> {
  try {
    const [list, view] = await Promise.all([api.listMeetings(), api.getSettings()]);
    meetings = list;
    captureSupported = view.capture_supported;
  } catch {
    // Панель — не главное: список останется прежним, ошибку покажет экран.
  }
  draw();
}

export function startSide(): void {
  window.setInterval(() => {
    const t = document.querySelector("[data-side-timer]");
    if (t && state.recording) t.textContent = elapsed(state.recording.started_at);
  }, 1000);
}