fix(dictation): PR 1.4 review polish — banner re-prompt, nav flush, language restore
- Track draft-handled-this-session flag so Restore/Discard suppresses the recovery banner on page-nav round-trips within one session; resets cleanly on the next successful save. - Flush pending autosave in onDestroy so Tauri SPA navigation preserves the last <1.5s of input. - Round-trip language on Restore so a draft captured under one language doesn't silently inherit the current setting on revival. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,23 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
// PR 1.4 review polish: module-scoped (NOT component-scoped) so the
|
||||||
|
// flag survives Tauri SPA navigation re-mounting this page. Once the
|
||||||
|
// user has explicitly Restored or Discarded the recovery banner this
|
||||||
|
// session, suppress re-prompting on subsequent in-app navigations
|
||||||
|
// (Dictation → History → Dictation). Without this, the autosave that
|
||||||
|
// immediately follows a Restore would re-trigger the banner on the
|
||||||
|
// next visit. Reset to false on the next successful save (or Clear)
|
||||||
|
// so a fresh capture-and-recovery cycle still works within the same
|
||||||
|
// session without needing a full app restart.
|
||||||
|
let draftHandledThisSession = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { onMount, onDestroy } from "svelte";
|
import { onMount, onDestroy } from "svelte";
|
||||||
import { Channel, invoke } from "@tauri-apps/api/core";
|
import { Channel, invoke } from "@tauri-apps/api/core";
|
||||||
import { emit } from "@tauri-apps/api/event";
|
import { emit } from "@tauri-apps/api/event";
|
||||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
import { page, settings, templates, profiles, addToHistory, addTask, tasks } from "$lib/stores/page.svelte.js";
|
import { page, settings, saveSettings, templates, profiles, addToHistory, addTask, tasks } from "$lib/stores/page.svelte.js";
|
||||||
import { markError, markGenerating, markGenerationDone, markLoading, refreshLlmStatus } from "$lib/stores/llmStatus.svelte.js";
|
import { markError, markGenerating, markGenerationDone, markLoading, refreshLlmStatus } from "$lib/stores/llmStatus.svelte.js";
|
||||||
import { playStartCue, playStopCue, playCompleteCue } from "$lib/utils/sounds.js";
|
import { playStartCue, playStopCue, playCompleteCue } from "$lib/utils/sounds.js";
|
||||||
import { profilesStore } from "$lib/stores/profiles.svelte.ts";
|
import { profilesStore } from "$lib/stores/profiles.svelte.ts";
|
||||||
@@ -91,11 +104,15 @@
|
|||||||
// Recovery check — runs even in non-Tauri preview so the browser
|
// Recovery check — runs even in non-Tauri preview so the browser
|
||||||
// shell can also offer to restore. Only shows the banner if there's
|
// shell can also offer to restore. Only shows the banner if there's
|
||||||
// nothing currently in the textarea (otherwise the user has live
|
// nothing currently in the textarea (otherwise the user has live
|
||||||
// content and a banner would be confusing).
|
// content and a banner would be confusing). Also skipped if the
|
||||||
const draft = loadRecoverableDraft();
|
// user already Restored/Discarded this session — see
|
||||||
if (draft && !transcript.trim()) {
|
// draftHandledThisSession above.
|
||||||
recoverableDraft = draft;
|
if (!draftHandledThisSession) {
|
||||||
recoverableDraftAgo = formatTimeAgo(draft.updatedAt);
|
const draft = loadRecoverableDraft();
|
||||||
|
if (draft && !transcript.trim()) {
|
||||||
|
recoverableDraft = draft;
|
||||||
|
recoverableDraftAgo = formatTimeAgo(draft.updatedAt);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// visibilitychange + pagehide flush the autosave immediately so
|
// visibilitychange + pagehide flush the autosave immediately so
|
||||||
@@ -125,10 +142,11 @@
|
|||||||
visibilityHandler = null;
|
visibilityHandler = null;
|
||||||
}
|
}
|
||||||
window.removeEventListener("pagehide", flushAutosave);
|
window.removeEventListener("pagehide", flushAutosave);
|
||||||
if (autosaveTimer) {
|
// Tauri SPA navigation (Dictation → History → Files) doesn't fire
|
||||||
clearTimeout(autosaveTimer);
|
// pagehide or visibilitychange, so without this flush the last
|
||||||
autosaveTimer = null;
|
// <1.5s of pre-nav input would be lost from the draft. flushAutosave
|
||||||
}
|
// also clears the timer, so no need to clear it again below.
|
||||||
|
flushAutosave();
|
||||||
clearInterval(timerInterval);
|
clearInterval(timerInterval);
|
||||||
if (page.recording) {
|
if (page.recording) {
|
||||||
page.recording = false;
|
page.recording = false;
|
||||||
@@ -194,8 +212,16 @@
|
|||||||
segments = Array.isArray(recoverableDraft.segments)
|
segments = Array.isArray(recoverableDraft.segments)
|
||||||
? [...recoverableDraft.segments]
|
? [...recoverableDraft.segments]
|
||||||
: [];
|
: [];
|
||||||
|
// Round-trip the language so a draft captured under a different
|
||||||
|
// language (e.g. user was in Spanish, switched away, came back)
|
||||||
|
// doesn't silently inherit the current setting on revival.
|
||||||
|
if (recoverableDraft.language) {
|
||||||
|
settings.language = recoverableDraft.language;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
recoverableDraft = null;
|
recoverableDraft = null;
|
||||||
recoverableDraftAgo = "";
|
recoverableDraftAgo = "";
|
||||||
|
draftHandledThisSession = true;
|
||||||
// Don't clear the localStorage entry — the autosave $effect will
|
// Don't clear the localStorage entry — the autosave $effect will
|
||||||
// immediately rewrite it with a fresh `updatedAt` reflecting the
|
// immediately rewrite it with a fresh `updatedAt` reflecting the
|
||||||
// restored state. If the app crashes between restore and next
|
// restored state. If the app crashes between restore and next
|
||||||
@@ -205,6 +231,7 @@
|
|||||||
function discardDraft() {
|
function discardDraft() {
|
||||||
recoverableDraft = null;
|
recoverableDraft = null;
|
||||||
recoverableDraftAgo = "";
|
recoverableDraftAgo = "";
|
||||||
|
draftHandledThisSession = true;
|
||||||
clearDraft();
|
clearDraft();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -734,6 +761,10 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
clearDraft();
|
clearDraft();
|
||||||
|
// Successful save closes the current draft cycle; clear the
|
||||||
|
// session flag so a fresh capture-and-recovery cycle works
|
||||||
|
// normally without needing a full app restart.
|
||||||
|
draftHandledThisSession = false;
|
||||||
|
|
||||||
// Extract tasks from transcript
|
// Extract tasks from transcript
|
||||||
const extracted = await extractTasksForTranscript(transcript);
|
const extracted = await extractTasksForTranscript(transcript);
|
||||||
@@ -799,6 +830,7 @@
|
|||||||
// throw away.
|
// throw away.
|
||||||
recoverableDraft = null;
|
recoverableDraft = null;
|
||||||
recoverableDraftAgo = "";
|
recoverableDraftAgo = "";
|
||||||
|
draftHandledThisSession = false;
|
||||||
clearDraft();
|
clearDraft();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -876,6 +908,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
clearDraft();
|
clearDraft();
|
||||||
|
draftHandledThisSession = false;
|
||||||
saved = true;
|
saved = true;
|
||||||
setTimeout(() => { saved = false; }, FEEDBACK_TIMEOUT_MS);
|
setTimeout(() => { saved = false; }, FEEDBACK_TIMEOUT_MS);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user