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);
}