ui+app: Day 5+6 — Settings → Vocabulary panel + Wayland self-relaunch
VOCABULARY PANEL (Day 5):
src/lib/pages/SettingsPage.svelte gains a new collapsible "Vocabulary"
section between Audio and Transcription. Wires the dictionary commands
shipped in 1cce567:
- list_dictionary_command on mount + onfocus refresh
- add_dictionary_entry_command (term + optional note)
- delete_dictionary_entry_command
- Inline error feedback via vocabularyError
- Empty-state copy + per-entry remove button with aria-label
The dictionary terms are intended to be injected into the LLM cleanup
prompt so the model preserves user-specific vocabulary (medication
names, place names, jargon, names of people in the user's support
network — particularly important for the ND audience). The LLM client
itself is currently a stub (crates/ai-formatting/src/llm_client.rs);
when it lands, it can import list_dictionary from kon_storage and
inject terms into the prompt suffix.
WAYLAND SELF-RELAUNCH (Day 6):
src-tauri/src/lib.rs gains ensure_x11_on_wayland() which runs before
tauri::Builder. On Linux Wayland sessions (XDG_SESSION_TYPE=wayland) it
sets GDK_BACKEND=x11, WINIT_UNIX_BACKEND=x11, and
WEBKIT_DISABLE_DMABUF_RENDERER=1 — the env vars HANDOVER.md documents
the user manually prefixing.
Drops the "users have to remember the env-var prefix" friction.
Idempotent: existing values are respected. Inspired by Open-Whispr's
Chromium --ozone-platform=x11 self-relaunch.
Compile-checked: cargo check -p kon-storage clean. Settings.svelte
$state / #if balanced (28 state, 34/34 if/endif).
NEXT: Phase 6 — dogfooding readiness doc with Jake test instructions.
This commit is contained in:
@@ -68,8 +68,53 @@ async fn save_preferences(
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// On Linux Wayland sessions (KDE, GNOME) WebKitGTK + DMABUF rendering hits
|
||||
/// known crashes that the HANDOVER documents working around with a manual
|
||||
/// env-var prefix:
|
||||
///
|
||||
/// env GDK_BACKEND=x11 WINIT_UNIX_BACKEND=x11 \
|
||||
/// WEBKIT_DISABLE_DMABUF_RENDERER=1 npm run tauri dev
|
||||
///
|
||||
/// Detect the Wayland session at startup and apply the env vars before
|
||||
/// anything else loads, so users do not need to remember the prefix and
|
||||
/// the dev launcher / packaged app both work out of the box.
|
||||
///
|
||||
/// Inspired by Open-Whispr's similar Chromium self-relaunch (with
|
||||
/// --ozone-platform=x11). Day 6 of the upgrade plan.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn ensure_x11_on_wayland() {
|
||||
// If the user explicitly opted in to Wayland (XDG_SESSION_TYPE set by
|
||||
// their session, KDE / GNOME compositor flags), force WebKitGTK + GDK
|
||||
// onto X11 via XWayland. Idempotent: if a value is already set, keep
|
||||
// it (the user knows best).
|
||||
let session_type = std::env::var("XDG_SESSION_TYPE").unwrap_or_default();
|
||||
let is_wayland = session_type.eq_ignore_ascii_case("wayland");
|
||||
if !is_wayland {
|
||||
return;
|
||||
}
|
||||
|
||||
let set_if_unset = |key: &str, value: &str| {
|
||||
if std::env::var_os(key).is_none() {
|
||||
// SAFETY: setting env vars before any threads spawn (we are
|
||||
// pre-Tauri-Builder here). This block is the only place these
|
||||
// are written.
|
||||
unsafe { std::env::set_var(key, value); }
|
||||
eprintln!("[startup] Wayland workaround: {key}={value}");
|
||||
}
|
||||
};
|
||||
|
||||
set_if_unset("GDK_BACKEND", "x11");
|
||||
set_if_unset("WINIT_UNIX_BACKEND", "x11");
|
||||
set_if_unset("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
|
||||
// PipeWire screen-capture portal still works fine through XWayland;
|
||||
// we only redirect the GUI rendering path.
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
#[cfg(target_os = "linux")]
|
||||
ensure_x11_on_wayland();
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
|
||||
@@ -43,6 +43,49 @@
|
||||
audioDevices = [];
|
||||
}
|
||||
}
|
||||
|
||||
// --- Custom vocabulary (dictionary) — Day 5 of upgrade plan ---
|
||||
// Backed by SQLite `dictionary` table via list_dictionary_command etc.
|
||||
// Terms get injected into the LLM cleanup prompt so the model preserves
|
||||
// the user's spellings (e.g. medication names, jargon, place names).
|
||||
let vocabulary = $state([]);
|
||||
let vocabularyError = $state(null);
|
||||
let newVocabTerm = $state("");
|
||||
let newVocabNote = $state("");
|
||||
|
||||
async function refreshVocabulary() {
|
||||
vocabularyError = null;
|
||||
try {
|
||||
vocabulary = await invoke("list_dictionary_command");
|
||||
} catch (err) {
|
||||
vocabularyError = "Could not load vocabulary: " + (err?.message || err);
|
||||
}
|
||||
}
|
||||
|
||||
async function addVocabTerm() {
|
||||
const term = newVocabTerm.trim();
|
||||
if (!term) return;
|
||||
try {
|
||||
await invoke("add_dictionary_entry_command", {
|
||||
term,
|
||||
note: newVocabNote.trim() || null,
|
||||
});
|
||||
newVocabTerm = "";
|
||||
newVocabNote = "";
|
||||
await refreshVocabulary();
|
||||
} catch (err) {
|
||||
vocabularyError = "Could not save term: " + (err?.message || err);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteVocabTerm(id) {
|
||||
try {
|
||||
await invoke("delete_dictionary_entry_command", { id });
|
||||
await refreshVocabulary();
|
||||
} catch (err) {
|
||||
vocabularyError = "Could not delete term: " + (err?.message || err);
|
||||
}
|
||||
}
|
||||
let parakeetDownloaded = $state(false);
|
||||
let parakeetDownloading = $state(false);
|
||||
let parakeetProgress = $state(0);
|
||||
@@ -131,6 +174,10 @@
|
||||
// the user opens the Audio section.
|
||||
refreshAudioDevices();
|
||||
|
||||
// Pre-load vocabulary too. Both lists are small; pre-fetching avoids
|
||||
// a flash of "no data" when the section opens.
|
||||
refreshVocabulary();
|
||||
|
||||
try {
|
||||
downloadedModels = await invoke("list_models");
|
||||
} catch {}
|
||||
@@ -380,6 +427,76 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Vocabulary (custom dictionary injected into LLM cleanup prompt) -->
|
||||
<div class="border-b border-border-subtle">
|
||||
<button
|
||||
class="flex items-center justify-between w-full py-4 px-5 text-left"
|
||||
onclick={() => openSection = openSection === 'vocabulary' ? null : 'vocabulary'}
|
||||
>
|
||||
<h3 class="font-display text-[18px] italic text-text">Vocabulary</h3>
|
||||
<span class="text-text-tertiary text-[16px] leading-none">{openSection === 'vocabulary' ? '−' : '+'}</span>
|
||||
</button>
|
||||
{#if openSection === 'vocabulary'}
|
||||
<div class="px-5 pb-5 animate-fade-in">
|
||||
<p class="text-[12px] text-text-tertiary mb-4">
|
||||
Words and phrases the AI cleanup pass should preserve exactly. Useful for medication names, place names, jargon, names of people in your support network, anything Whisper tends to mishear.
|
||||
</p>
|
||||
|
||||
<div class="flex gap-2 mb-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Term (e.g. methylphenidate, Wren, Tartarus)"
|
||||
bind:value={newVocabTerm}
|
||||
onkeydown={(e) => { if (e.key === 'Enter') addVocabTerm(); }}
|
||||
class="flex-1 bg-bg-input border border-border rounded-lg px-3 py-2 text-[13px] text-text
|
||||
focus:outline-none focus:border-accent focus:shadow-[0_0_0_3px_rgba(232,168,124,0.1)]"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Note (optional)"
|
||||
bind:value={newVocabNote}
|
||||
onkeydown={(e) => { if (e.key === 'Enter') addVocabTerm(); }}
|
||||
class="flex-1 bg-bg-input border border-border rounded-lg px-3 py-2 text-[13px] text-text
|
||||
focus:outline-none focus:border-accent focus:shadow-[0_0_0_3px_rgba(232,168,124,0.1)]"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onclick={addVocabTerm}
|
||||
disabled={!newVocabTerm.trim()}
|
||||
class="px-4 py-2 text-[13px] bg-accent text-bg rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-accent-hover"
|
||||
>Add</button>
|
||||
</div>
|
||||
|
||||
{#if vocabularyError}
|
||||
<p class="text-[11px] text-error mb-3">{vocabularyError}</p>
|
||||
{/if}
|
||||
|
||||
{#if vocabulary.length === 0}
|
||||
<p class="text-[11px] text-text-tertiary italic">No terms yet. Add one above.</p>
|
||||
{:else}
|
||||
<ul class="space-y-1">
|
||||
{#each vocabulary as entry (entry.id)}
|
||||
<li class="flex items-center gap-3 py-2 px-3 bg-bg-input border border-border rounded-lg">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-[13px] text-text font-medium truncate">{entry.term}</div>
|
||||
{#if entry.note}
|
||||
<div class="text-[11px] text-text-tertiary truncate">{entry.note}</div>
|
||||
{/if}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => deleteVocabTerm(entry.id)}
|
||||
class="text-[12px] text-text-tertiary hover:text-error border border-border hover:border-error rounded px-2 py-1"
|
||||
aria-label="Delete {entry.term}"
|
||||
>Remove</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Transcription -->
|
||||
<div class="border-b border-border-subtle">
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user