feat(preview): floating transcription overlay with listening→live→cleanup→final phases
Ported the best bits of OpenWhispr's TranscriptionPreviewOverlay into Kon's window conventions. Off by default — toggle in Settings → Output → "Floating preview when Kon is unfocused". Opens only when the main window isn't focused at the start of a recording, so it never adds noise when the user can already see the transcript in the main surface. Phase state machine (src/routes/preview/+page.svelte): - listening — pulsing dot, no text yet - live — animated bars + streaming raw Whisper output - cleanup — accent bars while the LLM cleanup pass runs - final — checkmark + formatted text + 4s auto-hide Data plumbing: raw segment text is captured before post_process_segments in live.rs (new raw_text field on LiveResultMessage) and in transcription.rs (new raw_text in the transcription-result payload). DictationPage forwards raw_text to the overlay via Tauri global events — preview-listening on start, preview-append per chunk, preview-cleanup before the LLM pass, preview-final with the formatted text, preview-hide when a run produced no transcript (empty recording / cancel). Window is always_on_top, skip_taskbar, focused=false so it never steals focus from whatever the user is dictating into. open_preview_window shows an existing hidden preview or builds it fresh; close_preview_window hides without destroying so the next open is instant. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
68
src/routes/preview/+layout@.svelte
Normal file
68
src/routes/preview/+layout@.svelte
Normal file
@@ -0,0 +1,68 @@
|
||||
<script lang="ts">
|
||||
// @ts-nocheck
|
||||
import "../../app.css";
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { settings } from "$lib/stores/page.svelte.js";
|
||||
import {
|
||||
getPreferences,
|
||||
updatePreferences,
|
||||
applyExternalPreferences,
|
||||
PREFERENCES_CHANGED_EVENT,
|
||||
} from "$lib/stores/preferences.svelte.js";
|
||||
|
||||
let { children } = $props();
|
||||
let unlistenPrefs = null;
|
||||
|
||||
const prefs = getPreferences();
|
||||
|
||||
// Keep transcript-editor theme sync trick: legacy settings → preferences
|
||||
$effect(() => {
|
||||
const legacyTheme = settings.theme;
|
||||
const mapped = legacyTheme === "Light" ? "light" : legacyTheme === "Dark" ? "dark" : "system";
|
||||
if (prefs.theme !== mapped) {
|
||||
updatePreferences({ theme: mapped });
|
||||
}
|
||||
});
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.addEventListener("storage", (event) => {
|
||||
if (event.key === "kon_settings" && event.newValue) {
|
||||
try { Object.assign(settings, JSON.parse(event.newValue)); } catch {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
let ownLabel = null;
|
||||
try { ownLabel = getCurrentWindow().label; } catch {}
|
||||
unlistenPrefs = await listen(PREFERENCES_CHANGED_EVENT, (event) => {
|
||||
const payload = event?.payload;
|
||||
if (!payload || payload.source === ownLabel) return;
|
||||
applyExternalPreferences(payload.prefs);
|
||||
});
|
||||
} catch {}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (unlistenPrefs) unlistenPrefs();
|
||||
});
|
||||
|
||||
// Escape closes the preview without destroying it — the next dictation
|
||||
// reopens it instantly via open_preview_window.
|
||||
function handleKeydown(event) {
|
||||
if (event.key === "Escape") {
|
||||
getCurrentWindow().hide().catch(() => {});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<div class="h-screen w-screen overflow-hidden grain border border-border shadow-xl flex flex-col">
|
||||
<div class="flex-1 min-h-0 overflow-hidden">
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user