skillok meeting
d93e31a829c96025e18e4ceed0cf1552df5934c8
Перейти к файлу
Код
meeting srcviewsmeetings.ts
anc Этап 0: каркас Tauri 2, разбор встречи через OpenRouter, стиль skillok.org - Ядро на Rust: расшифровка и импорт текста, схема разбора со строгой проверкой ответа модели, клиент OpenRouter (проверка ключа, разбор), настройки и ключ в хранилище паролей ОС, встречи папками на диске. - Интерфейс: экраны «Встречи», «Встреча» (расшифровка, конспект, задачи), «Настройки»; токены, шрифты и кнопки как на skillok.org. - Промпт разбора prompts/analysis.md и проверочные расшифровки. - 30 тестов ядра; клиент OpenRouter проверяется против локальной заглушки. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> d93e31a 30 сентября 2026 г.
meetings.ts
// Экран «Встречи»: список и импорт расшифровки.

import { api, errorText, formatDate, formatDuration, type Meeting } from "../api";
import { $, busy, esc } from "../dom";

function status(m: Meeting): string {
  if (!m.analysis) return `<span class="label">не разобрана</span>`;
  const n = m.analysis.tasks;
  return `<span class="label label-accent">разобрана · ${n} ${plural(n, "задача", "задачи", "задач")}</span>`;
}

export function plural(n: number, one: string, few: string, many: string): string {
  const d = n % 10, h = n % 100;
  if (d === 1 && h !== 11) return one;
  if (d >= 2 && d <= 4 && (h < 12 || h > 14)) return few;
  return many;
}

export async function renderMeetings(root: HTMLElement): Promise<void> {
  const meetings = await api.listMeetings();
  root.innerHTML = `
    <div class="page-head">
      <h1>Встречи</h1>
      <div class="actions"><button class="ui-btn" data-open-import>Импорт расшифровки</button></div>
    </div>

    <form class="box" data-import hidden>
      <div class="box-head"><h2>Импорт расшифровки</h2></div>
      <div class="box-body">
        <div class="field">
          <label for="imp-title">Название</label>
          <input id="imp-title" class="input" name="title" autocomplete="off" required>
        </div>
        <div class="field">
          <label for="imp-text">Расшифровка</label>
          <textarea id="imp-text" class="textarea mono" name="text" rows="10" required
            placeholder="[00:00:05] Анна: Начнём с отчёта."></textarea>
          <p class="field-hint">Каждая реплика — с новой строки: [чч:мм:сс] Имя: текст.</p>
        </div>
        <p class="error" data-import-error hidden></p>
        <div class="actions">
          <button class="ui-btn" type="submit">Создать встречу</button>
          <button class="ui-btn" type="button" data-close-import>Отмена</button>
        </div>
      </div>
    </form>

    <section class="box">
      ${
        meetings.length
          ? meetings
              .map(
                (m) => `
        <a class="row meeting-row" href="#/meeting/${encodeURIComponent(m.id)}">
          <span class="title">${esc(m.title)}</span>
          <span class="when small muted">${esc(formatDate(m.created_at))} · ${esc(formatDuration(m.duration_ms))}</span>
          <span class="status">${status(m)}</span>
        </a>`,
              )
              .join("")
          : `<p class="empty">Встреч пока нет.</p>`
      }
    </section>`;

  const form = $(root, "[data-import]") as HTMLFormElement;
  const error = $(root, "[data-import-error]");
  $(root, "[data-open-import]").addEventListener("click", () => {
    form.hidden = false;
    $(form, "#imp-title").focus();
  });
  $(root, "[data-close-import]").addEventListener("click", () => {
    form.hidden = true;
    error.hidden = true;
  });
  form.addEventListener("submit", async (event) => {
    event.preventDefault();
    const data = new FormData(form);
    const submit = $(form, "[type=submit]") as HTMLButtonElement;
    try {
      const m = await busy(submit, "Создаю…", () => api.importMeeting(String(data.get("title")), String(data.get("text"))));
      location.hash = `#/meeting/${encodeURIComponent(m.id)}`;
    } catch (e) {
      error.textContent = errorText(e);
      error.hidden = false;
    }
  });
}