skillok meeting
d93e31a829c96025e18e4ceed0cf1552df5934c8
Перейти к файлу
Код
meeting srcapi.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 г.
103 строки 3.9 КБ
api.ts
// Типы и вызовы команд ядра (src-tauri/src/commands.rs). Поля совпадают с
// тем, что отдаёт serde на стороне Rust.

export type Speaker = { kind: "me" } | { kind: "others" } | { kind: "named"; name: string };

export interface Segment { start_ms: number; speaker: Speaker; text: string }
export interface Transcript { segments: Segment[] }

export interface Usage { prompt_tokens: number; completion_tokens: number; cost: number | null }

export interface AnalysisMeta { model: string; at: string; usage: Usage; tasks: number }

export interface Meeting {
  id: string;
  title: string;
  created_at: string;
  source: "import" | "recording";
  duration_ms: number;
  segments: number;
  analysis: AnalysisMeta | null;
}

export interface Analysis {
  title: string;
  summary: string;
  review: { achieved: string[]; open: string[]; off_track: string[]; verdict: string };
  decisions: { text: string; source_segment: number }[];
  tasks: { text: string; owner: string | null; due: string | null; quote: string; source_segment: number }[];
  open_questions: string[];
}

export interface MeetingDetail { meeting: Meeting; transcript: Transcript; analysis: Analysis | null }

export interface Settings {
  live_model: string;
  analysis_model: string;
  stt_model: string;
  warning_text: string;
  always_warn_myself: boolean;
}

export interface SettingsView { settings: Settings; openrouter_key: string | null; meetings_dir: string }

export interface KeyInfo { label: string | null; limit_remaining: number | null; usage: number | null; is_free_tier: boolean }

type Invoke = <T>(cmd: string, args?: Record<string, unknown>) => Promise<T>;

let invokeImpl: Invoke | null = null;

async function call<T>(cmd: string, args?: Record<string, unknown>): Promise<T> {
  if (!invokeImpl) {
    if ("__TAURI_INTERNALS__" in window) {
      invokeImpl = (await import("@tauri-apps/api/core")).invoke;
    } else if (import.meta.env.DEV || import.meta.env.VITE_MOCK === "1") {
      // Только для просмотра интерфейса в обычном браузере: `npm run dev`
      // или сборка `npm run build:mock`. В сборку приложения не попадает.
      invokeImpl = (await import("./dev-mock")).invoke;
    } else {
      throw new Error("Skillok Meet запущен без ядра");
    }
  }
  return invokeImpl<T>(cmd, args);
}

export const api = {
  getSettings: () => call<SettingsView>("get_settings"),
  saveSettings: (settings: Settings) => call<void>("save_settings", { settings }),
  setOpenrouterKey: (key: string) => call<KeyInfo>("set_openrouter_key", { key }),
  checkOpenrouterKey: () => call<KeyInfo>("check_openrouter_key"),
  deleteOpenrouterKey: () => call<void>("delete_openrouter_key"),
  listMeetings: () => call<Meeting[]>("list_meetings"),
  getMeeting: (id: string) => call<MeetingDetail>("get_meeting", { id }),
  importMeeting: (title: string, text: string) => call<Meeting>("import_meeting", { title, text }),
  analyzeMeeting: (id: string) => call<Meeting>("analyze_meeting", { id }),
};

export function speakerLabel(s: Speaker): string {
  switch (s.kind) {
    case "me": return "Я";
    case "others": return "Собеседники";
    case "named": return s.name;
  }
}

export function formatTime(ms: number): string {
  const t = Math.floor(ms / 1000);
  const p = (n: number) => String(n).padStart(2, "0");
  return `${p(Math.floor(t / 3600))}:${p(Math.floor(t / 60) % 60)}:${p(t % 60)}`;
}

export function formatDuration(ms: number): string {
  const min = Math.round(ms / 60000);
  if (min < 60) return `${min} мин`;
  return `${Math.floor(min / 60)} ч ${min % 60} мин`;
}

export function formatDate(iso: string): string {
  return new Date(iso).toLocaleString("ru-RU", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" });
}

export function errorText(e: unknown): string {
  return typeof e === "string" ? e : e instanceof Error ? e.message : String(e);
}