skillok meeting
996ce529a8d705e470aea24a7b7fb9a19578f1e4
Перейти к файлу
Код
meeting srcapi.ts
anc Интерфейс: список встреч слева, заметка справа - Боковая панель: «Записать», «Добавить аудио» (и перетаскивание файла), «Новая заметка», список встреч. - Экран встречи: название правится на месте, шаги «Расшифровать» и команды ИИ с ходом работы, панель записи с уровнями и текстом предупреждения для чата Zoom. - Заметка: «Текст» / «Правка», автосохранение, «Копировать всё», «Как текст», копирование раздела, живые галочки задач. - Сообщения внизу экрана вместо ошибок в тексте страницы. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> 996ce52 30 сентября 2026 г.
api.ts
// Типы и вызовы команд ядра (src-tauri/src/commands.rs). Поля совпадают с
// тем, что отдаёт serde на стороне Rust.

export type Speaker = { kind: "me" } | { kind: "others" } | { kind: "unknown" };

export interface AudioTrack { file: string; speaker: Speaker; duration_ms: number }

export interface Meeting {
  id: string;
  title: string;
  created_at: string;
  updated_at: string;
  audio: AudioTrack[];
  recording: boolean;
  transcribed_at: string | null;
}

export interface MeetingDetail { meeting: Meeting; note: string }

export interface Settings {
  ai_model: string;
  stt_model: string;
  language: string;
  warning_text: string;
}

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

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

export type Command =
  | { kind: "summary" }
  | { kind: "tasks" }
  | { kind: "review" }
  | { kind: "custom"; text: string };

export interface RecordingView { meeting_id: string; started_at: string; paused: boolean; zoom: boolean }

export interface TranscribeResult { note: string; failed: number; last_error: string | null }

export type RecorderEvent =
  | { kind: "level"; data: { speaker: Speaker; dbfs: number } }
  | { kind: "track_failed"; data: { speaker: Speaker; error: string } };

type Invoke = <T>(cmd: string, args?: Record<string, unknown>) => Promise<T>;
type Listen = <T>(event: string, cb: (payload: T) => void) => Promise<() => void>;

interface Core {
  invoke: Invoke;
  listen: Listen;
  pickAudio: () => Promise<string | null>;
  onFileDrop: (cb: (paths: string[]) => void) => Promise<() => void>;
}

const AUDIO_EXTENSIONS = ["mp3", "m4a", "wav", "aiff", "aif", "flac", "ogg", "aac", "mp4", "mkv", "caf"];

let core: Promise<Core> | null = null;

function getCore(): Promise<Core> {
  core ??= (async (): Promise<Core> => {
    if ("__TAURI_INTERNALS__" in window) {
      const { invoke } = await import("@tauri-apps/api/core");
      const { listen } = await import("@tauri-apps/api/event");
      const { getCurrentWebview } = await import("@tauri-apps/api/webview");
      const { open } = await import("@tauri-apps/plugin-dialog");
      return {
        invoke,
        listen: (event, cb) => listen(event, (e) => cb(e.payload as never)),
        pickAudio: async () => {
          const picked = await open({ multiple: false, filters: [{ name: "Аудио", extensions: AUDIO_EXTENSIONS }] });
          return typeof picked === "string" ? picked : null;
        },
        onFileDrop: (cb) =>
          getCurrentWebview().onDragDropEvent((e) => {
            if (e.payload.type === "drop") cb(e.payload.paths);
          }),
      };
    }
    if (import.meta.env.DEV || import.meta.env.VITE_MOCK === "1") {
      // Только для просмотра интерфейса в обычном браузере: `npm run dev`
      // или сборка `npm run build:mock`. В сборку приложения не попадает.
      return import("./dev-mock");
    }
    throw new Error("Skillok Meet запущен без ядра");
  })();
  return core;
}

async function call<T>(cmd: string, args?: Record<string, unknown>): Promise<T> {
  return (await getCore()).invoke<T>(cmd, args);
}

export async function on<T>(event: string, cb: (payload: T) => void): Promise<() => void> {
  return (await getCore()).listen<T>(event, cb);
}

export const events = {
  recorder: (cb: (p: { meeting_id: string; event: RecorderEvent }) => void) => on("meet://recorder", cb),
  progress: (cb: (p: { meeting_id: string; done: number; total: number }) => void) => on("meet://progress", cb),
};

export const files = {
  pickAudio: async () => (await getCore()).pickAudio(),
  onDrop: async (cb: (paths: string[]) => void) => (await getCore()).onFileDrop(cb),
  isAudio: (path: string) => AUDIO_EXTENSIONS.includes(path.split(".").pop()?.toLowerCase() ?? ""),
};

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 }),
  createMeeting: () => call<Meeting>("create_meeting"),
  saveNote: (id: string, text: string) => call<Meeting>("save_note", { id, text }),
  renameMeeting: (id: string, title: string) => call<Meeting>("rename_meeting", { id, title }),
  deleteMeeting: (id: string) => call<void>("delete_meeting", { id }),
  addAudio: (path: string) => call<Meeting>("add_audio", { path }),
  transcribe: (id: string) => call<TranscribeResult>("transcribe_meeting", { id }),
  runCommand: (id: string, command: Command) => call<string>("run_command", { id, command }),
  recorderStatus: () => call<RecordingView | null>("recorder_status"),
  recorderStart: () => call<Meeting>("recorder_start"),
  recorderPause: (paused: boolean) => call<void>("recorder_pause", { paused }),
  recorderStop: () => call<Meeting>("recorder_stop"),
};

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

export function meetingDuration(m: Meeting): number {
  return Math.max(0, ...m.audio.map((a) => a.duration_ms));
}

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 elapsed(since: string): string {
  const t = Math.max(0, Math.floor((Date.now() - Date.parse(since)) / 1000));
  const p = (n: number) => String(n).padStart(2, "0");
  return t >= 3600 ? `${Math.floor(t / 3600)}:${p(Math.floor(t / 60) % 60)}:${p(t % 60)}` : `${p(Math.floor(t / 60))}:${p(t % 60)}`;
}

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