import { invoke as tauriInvoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; export type RecordingStatus = "idle" | "recording"; export interface AppSettings { transcription: { mode: "localFirst" | "apiOnly"; modelId: string; language: string | null; openaiCompatible: { endpoint: string; apiKeyConfigured: boolean; model: string; }; }; insertion: { method: "clipboardPaste" | "keyTyping" | "manualCopy"; restoreClipboard: boolean; waylandManualFallback: boolean; }; storage: { keepAudio: boolean; audioFormat: "wav" | "ogg" | "mp3"; }; } export interface ModelWithStatus { model: { id: string; displayName: string; repository: string; filename: string; quantization: string; sizeBytes: number; languages: string[]; recommendedForSimplePc: boolean; }; status: "notInstalled" | "ready" | { error: string }; } export interface TranscriptDraft { text: string; lastMessage: string | null; } export interface AppSnapshot { settings: AppSettings; recordingStatus: RecordingStatus; transcript: TranscriptDraft; models: ModelWithStatus[]; } export interface NativeClient { invoke(command: string, args?: Record): Promise; } const fallbackSnapshot: AppSnapshot = { recordingStatus: "idle", transcript: { text: "", lastMessage: "Modo pré-visualização: comandos nativos disponíveis dentro do Tauri.", }, settings: { transcription: { mode: "localFirst", modelId: "parakeet-tdt-0.6b-v3-q8_0", language: null, openaiCompatible: { endpoint: "https://api.openai.com/v1/audio/transcriptions", apiKeyConfigured: false, model: "whisper-1", }, }, insertion: { method: "clipboardPaste", restoreClipboard: true, waylandManualFallback: true, }, storage: { keepAudio: false, audioFormat: "wav", }, }, models: [ { model: { id: "parakeet-tdt-0.6b-v3-q8_0", displayName: "Parakeet TDT 0.6B v3 Q8", repository: "cstr/parakeet-tdt-0.6b-v3-GGUF", filename: "parakeet-tdt-0.6b-v3-q8_0.gguf", quantization: "Q8_0", sizeBytes: 745_121_600, languages: ["pt", "en", "es", "fr", "de"], recommendedForSimplePc: true, }, status: "notInstalled", }, ], }; function createNativeClient(): NativeClient { return { invoke: (command: string, args?: Record) => tauriInvoke(command, args), }; } function element( tag: K, className?: string, text?: string, ): HTMLElementTagNameMap[K] { const node = document.createElement(tag); if (className) node.className = className; if (text !== undefined) node.textContent = text; return node; } function formatBytes(bytes: number): string { const mib = bytes / 1024 / 1024; return `${mib.toFixed(0)} MiB`; } async function safeSnapshot(client: NativeClient): Promise { try { return await client.invoke("get_app_snapshot"); } catch { return fallbackSnapshot; } } export function createTranscriptionApp( mount: HTMLElement, client: NativeClient = createNativeClient(), ): void { mount.textContent = ""; let snapshot = fallbackSnapshot; const shell = element("section", "modal-shell"); shell.setAttribute("aria-labelledby", "app-title"); const topbar = element("header", "topbar"); const titleGroup = element("div", "title-group"); const eyebrow = element("p", "eyebrow", "Transcript Desktop"); const title = element("h1", undefined, "Transcrição"); title.id = "app-title"; const subtitle = element("p", "subtitle"); titleGroup.append(eyebrow, title, subtitle); const status = element("span", "status-pill"); topbar.append(titleGroup, status); const editorLabel = element("label", "editor-label", "Texto transcrito"); editorLabel.setAttribute("for", "transcript-editor"); const editor = element( "textarea", "transcript-editor", ) as HTMLTextAreaElement; editor.id = "transcript-editor"; editor.placeholder = "A transcrição aparecerá aqui para revisão antes da inserção."; editor.rows = 10; const message = element("p", "message"); message.setAttribute("role", "status"); const actions = element("div", "actions"); const recordButton = element( "button", "primary-button", "Iniciar gravação", ) as HTMLButtonElement; const continueButton = element( "button", "secondary-button", "+ Continuar", ) as HTMLButtonElement; const copyButton = element( "button", "secondary-button", "Copiar", ) as HTMLButtonElement; const insertButton = element( "button", "secondary-button", "Inserir no app", ) as HTMLButtonElement; const saveButton = element( "button", "ghost-button", "Salvar texto", ) as HTMLButtonElement; actions.append( recordButton, continueButton, copyButton, insertButton, saveButton, ); const modelPanel = element("aside", "model-panel"); const modelTitle = element("h2", undefined, "Modelo offline"); const modelDetails = element("p"); modelPanel.append(modelTitle, modelDetails); shell.append(topbar, editorLabel, editor, message, actions, modelPanel); mount.append(shell); const render = (next: AppSnapshot): void => { snapshot = next; const isRecording = snapshot.recordingStatus === "recording"; status.textContent = isRecording ? "Gravando" : "Pronto"; status.dataset.state = snapshot.recordingStatus; subtitle.textContent = `${snapshot.settings.transcription.mode === "localFirst" ? "Offline primeiro" : "API"} · idioma ${snapshot.settings.transcription.language ?? "automático"}`; editor.value = snapshot.transcript.text; message.textContent = snapshot.transcript.lastMessage ?? "Revise o texto antes de inserir."; recordButton.textContent = isRecording ? "Parar e transcrever" : "Iniciar gravação"; const firstModel = snapshot.models[0]?.model; modelDetails.textContent = firstModel ? `${firstModel.displayName} · ${firstModel.quantization} · ${formatBytes(firstModel.sizeBytes)} · ${firstModel.repository}` : "Nenhum modelo offline homologado encontrado."; }; const refresh = async (): Promise => { render(await safeSnapshot(client)); }; const showError = (error: unknown): void => { message.textContent = error instanceof Error ? error.message : String(error); }; const toggleRecording = async (): Promise => { const command = snapshot.recordingStatus === "recording" ? "stop_recording_and_transcribe" : "start_recording"; render(await client.invoke(command)); }; recordButton.addEventListener("click", () => { void toggleRecording().catch(showError); }); continueButton.addEventListener("click", () => { void client .invoke("start_recording") .then(render) .catch(showError); }); copyButton.addEventListener("click", () => { void navigator.clipboard .writeText(editor.value) .then(() => { message.textContent = "Texto copiado para a área de transferência."; }) .catch(() => { message.textContent = "Não foi possível copiar automaticamente. Selecione o texto manualmente."; }); }); insertButton.addEventListener("click", () => { void client .invoke<{ message: string }>("request_insert_text", { text: editor.value, }) .then((result) => { message.textContent = result.message; }) .catch(showError); }); saveButton.addEventListener("click", () => { const blob = new Blob([editor.value], { type: "text/plain;charset=utf-8" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = "transcricao.txt"; link.click(); URL.revokeObjectURL(url); message.textContent = "Arquivo de texto preparado para download."; }); void listen("transcript://toggle-recording", () => { void toggleRecording().catch(showError); }).catch(() => { // Browser preview and tests do not expose the Tauri event bridge. }); void refresh(); }