Files
wisp-open/frontend/index.html
T
Vibe Nuage Agentandjahruz67 83ef777dc0 Fix HTML parsing error by removing control characters
Replace control characters (\u0012 Form Feed and \u0013 Carriage Return) with
proper HTML entities and clean text in index.html to fix parse5 parsing error.

- Line 106-107: Replace \u0012 with > for menu navigation separators
- Line 224: Remove \u0013 control character

Fixes vite build error: Unable to parse HTML; parse5 error code
control-character-in-input-stream

Co-authored-by: jahruz67 <jahruz67@users.noreply.github.com>
2026-07-29 22:13:09 +00:00

744 lines
34 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title>Wisp Settings</title>
<link rel="stylesheet" href="./src/style.css">
</head>
<body>
<div id="app" class="container">
<!-- Header -->
<div class="flex-between" style="margin-bottom: 28px;">
<h1>Settings</h1>
<div id="appVersion" style="color: var(--text-2); font-size: 12px; font-weight: 500;"></div>
</div>
<div id="saveStatus" class="save-status">Saved</div>
<!-- Settings Form -->
<div id="settingsForm">
<div class="section-group-label">Authentication</div>
<!-- API Keys Dropdown Section -->
<div class="section">
<details class="api-keys-details">
<summary class="api-keys-summary">
<span>API Keys</span>
<span id="apiKeysStatus" style="color: var(--text-2); font-size: 12px; margin-left: 8px;">
<span id="groqKeyStatus">No Groq key</span> | <span id="mistralKeyStatus">No Mistral key</span>
</span>
</summary>
<div class="api-keys-content">
<!-- Groq API Key -->
<div class="api-key-section">
<label style="margin-top: 12px; margin-bottom: 8px; display: block;">Groq API Key</label>
<div class="form-control">
<div class="flex-row">
<div class="input-wrapper">
<input type="password" id="groqApiKey" placeholder="gsk_...">
<button class="eye-btn" onclick="toggleGroqApiKey()" id="groqEyeBtn">👁️</button>
</div>
<button onclick="saveGroqApiKey()">Save</button>
</div>
</div>
<p class="hint">Get your free key at <a href="#" onclick="window.runtime.BrowserOpenURL('https://console.groq.com/keys'); return false;" style="color: var(--accent);">console.groq.com/keys</a></p>
</div>
<!-- Mistral API Key -->
<div class="api-key-section">
<label style="margin-top: 12px; margin-bottom: 8px; display: block;">Mistral API Key</label>
<div class="form-control">
<div class="flex-row">
<div class="input-wrapper">
<input type="password" id="mistralApiKey" placeholder="mx_...">
<button class="eye-btn" onclick="toggleMistralApiKey()" id="mistralEyeBtn">👁️</button>
</div>
<button onclick="saveMistralApiKey()">Save</button>
</div>
</div>
<p class="hint">Get your key at <a href="#" onclick="window.runtime.BrowserOpenURL('https://console.mistral.ai/api-keys'); return false;" style="color: var(--accent);">console.mistral.ai/api-keys</a></p>
</div>
</div>
</details>
</div>
<!-- Global Hotkey (hidden on Linux because Linux uses the system shortcut command below) -->
<div class="section" id="shortcutSection">
<label>Global Hotkey</label>
<div class="form-control">
<div class="flex-row">
<div class="input-wrapper">
<input type="text" id="shortcutInput" readonly placeholder="Click Record then press a key combo">
</div>
<button id="recordBtn" onclick="recordShortcut()">Record</button>
</div>
</div>
<p class="hint">Windows and macOS can listen for this shortcut directly. Modifier-only combos like ctrl+alt are supported only on Windows.</p>
</div>
<div class="section-group-label">Input</div>
<!-- Linux ydotool status / Direct typing setup -->
<div class="section" id="linuxYdotoolSection" style="display: none;">
<label>Direct Typing (ydotool)</label>
<div id="linuxYdotoolStatus"></div>
</div>
<!-- Linux system shortcut command (collapsible) -->
<div class="section" id="linuxFallbackSection" style="display: none;">
<details class="fallback-details">
<summary class="fallback-summary">Linux System Shortcut</summary>
<p class="hint" style="margin-top: 12px;">This is the main Linux shortcut method. Copy this command into your desktop's custom shortcut settings. One press starts recording, and the next press stops.</p>
<div class="form-control" style="margin-top: 12px;">
<div class="flex-row">
<div class="input-wrapper">
<input type="text" id="linuxPressCommand" readonly>
</div>
<button onclick="copyLinuxPressCommand()">Copy</button>
</div>
</div>
<p class="hint" style="margin-top: 10px;">GNOME: Settings &gt; Keyboard &gt; Custom Shortcuts &gt; Add a shortcut with the copied command.</p>
<p class="hint">KDE: System Settings &gt; Shortcuts &gt; Command/URL &gt; Add a shortcut with the copied command.</p>
</details>
</div>
<!-- Microphone -->
<div class="section">
<label>Microphone</label>
<select id="micDevice" onchange="saveMicDevice()" class="form-control">
<option value="default">System Default</option>
</select>
</div>
<div class="section-group-label">Transcription</div>
<!-- Whisper Model -->
<div class="section">
<label>Whisper Model</label>
<select id="whisperModel" onchange="saveWhisperModel()" class="form-control">
<option value="whisper-large-v3-turbo">Whisper Large v3 Turbo (Groq)</option>
<option value="whisper-large-v3">Whisper Large v3 (Groq)</option>
<option value="voxtral-small-latest">Voxtral Small (Mistral)</option>
</select>
</div>
<!-- Language -->
<div class="section">
<label>Language</label>
<select id="language" onchange="saveLanguage()" class="form-control">
<option value="auto">Auto-Detect</option>
<option value="en">English (en)</option>
<option value="es">Spanish (es)</option>
</select>
<p class="hint">Specifying language reduces latency and improves accuracy.</p>
</div>
<div class="section-group-label">AI Refinement</div>
<!-- AI Model -->
<div class="section">
<label>AI Model</label>
<select id="aiModel" onchange="saveAiModel()" class="form-control">
<option value="None">None — skip refinement</option>
<option value="openai/gpt-oss-120b-high">GPT OSS 120B High (Groq)</option>
<option value="openai/gpt-oss-120b">GPT OSS 120B (Groq)</option>
<option value="openai/gpt-oss-20b-high">GPT OSS 20B High (Groq)</option>
<option value="openai/gpt-oss-20b">GPT OSS 20B (Groq)</option>
<option value="llama-3.3-70b-versatile">Llama 3.3 70B Versatile (Groq)</option>
<option value="llama-3.1-8b-instant">Llama 3.1 8B Instant (Groq)</option>
<option value="mistral-small-latest">Mistral Small (Mistral)</option>
<option value="mistral-medium-latest">Mistral Medium (Mistral)</option>
</select>
</div>
<!-- AI Prompt -->
<div class="section">
<label>System Prompt</label>
<textarea id="aiPrompt" rows="3" placeholder="Custom instructions for text refinement..." class="form-control"></textarea>
<div style="text-align: right; margin-top: 10px;">
<button onclick="savePrompt()">Save Prompt</button>
</div>
</div>
<div class="section-group-label">System</div>
<!-- Startup -->
<div class="section flex-between">
<div>
<span id="startupLabel" style="font-weight: 500;">Run on System Startup</span>
<p class="hint" style="margin-top: 4px; margin-bottom: 0;">Launch automatically when you log in.</p>
</div>
<label class="flex-row" style="margin: 0; cursor: pointer;">
<input type="checkbox" id="startupToggle" onchange="toggleStartup()" style="width: auto;">
</label>
</div>
<!-- History -->
<div class="section">
<div class="flex-between" style="margin-bottom: 12px;">
<label style="margin: 0;">Transcription History</label>
<button onclick="clearHistory()" class="btn-link">Clear all</button>
</div>
<div id="historyList" class="history-list">
<div style="text-align: center; color: var(--text-2); padding: 24px 0; font-size: 13px;">No history yet.</div>
</div>
</div>
<!-- Offline Whisper -->
<div id="offlineWhisperSection" class="section" style="margin-top: 20px; padding-top: 20px; border-top: 1px solid var(--border);">
<label>Offline Whisper</label>
<!-- Connection Status -->
<div id="connectionStatus" style="margin-bottom: 14px; padding: 9px 14px; border-radius: 7px; text-align: center; font-size: 13px;">
Checking connection...
</div>
<!-- Whisper Status -->
<div id="whisperSection">
<div id="whisperNotInstalled">
<p class="hint" style="margin-bottom: 14px;">
Install offline transcription for use without internet. Uses GPU acceleration if available.
</p>
<div style="margin-bottom: 14px;">
<label style="font-size: 12px; color: var(--text-2); margin-bottom: 6px;">Model Size</label>
<select id="whisperModelSelect" class="form-control">
<option value="tiny">Tiny (75 MB) — Fastest</option>
<option value="base">Base (150 MB) — Fast</option>
<option value="small" selected>Small (500 MB) — Recommended</option>
<option value="medium">Medium (1.5 GB) — Best Quality</option>
</select>
</div>
<button onclick="installWhisper()" id="installBtn">
Install Offline Whisper
</button>
</div>
<div id="whisperInstalled" style="display: none;">
<div style="background: var(--green-dim); border: 1px solid rgba(61, 186, 110, 0.25); border-radius: 7px; padding: 13px 16px; margin-bottom: 14px;">
<div style="color: var(--green); font-weight: 500; font-size: 13px;"> Offline Whisper installed</div>
<div style="font-size: 12px; color: var(--text-2); margin-top: 4px;">
Model: <span id="installedModel"></span>
</div>
</div>
<p class="hint" style="margin-bottom: 14px;">
Select "Local — Whisper" in the Whisper Model dropdown above to use offline transcription.
</p>
<button onclick="uninstallWhisper()" class="btn-danger">
Uninstall Offline Whisper
</button>
</div>
</div>
</div>
</div>
</div>
<script type="module">
let groqApiKeyVisible = false;
let mistralApiKeyVisible = false;
let linuxShortcutMode = false;
// Toggle Groq API key visibility
window.toggleGroqApiKey = function () {
const input = document.getElementById('groqApiKey');
const btn = document.getElementById('groqEyeBtn');
groqApiKeyVisible = !groqApiKeyVisible;
input.type = groqApiKeyVisible ? 'text' : 'password';
btn.textContent = groqApiKeyVisible ? '👁️' : '👁️';
};
// Toggle Mistral API key visibility
window.toggleMistralApiKey = function () {
const input = document.getElementById('mistralApiKey');
const btn = document.getElementById('mistralEyeBtn');
mistralApiKeyVisible = !mistralApiKeyVisible;
input.type = mistralApiKeyVisible ? 'text' : 'password';
btn.textContent = mistralApiKeyVisible ? '👁️' : '👁️';
};
// Save Groq API key
window.saveGroqApiKey = async function () {
const key = document.getElementById('groqApiKey').value.trim();
await window.go.main.App.SaveSettings({ groq_api_key: key });
showSaveStatus();
updateApiKeyStatus();
};
// Save Mistral API key
window.saveMistralApiKey = async function () {
const key = document.getElementById('mistralApiKey').value.trim();
await window.go.main.App.SaveSettings({ mistral_api_key: key });
showSaveStatus();
updateApiKeyStatus();
};
// Update API key status display
function updateApiKeyStatus() {
const groqKey = document.getElementById('groqApiKey').value;
const mistralKey = document.getElementById('mistralApiKey').value;
const groqStatus = document.getElementById('groqKeyStatus');
const mistralStatus = document.getElementById('mistralKeyStatus');
groqStatus.textContent = groqKey ? (groqKey.length > 4 ? 'Groq key set' : 'Groq key') : 'No Groq key';
mistralStatus.textContent = mistralKey ? (mistralKey.length > 4 ? 'Mistral key set' : 'Mistral key') : 'No Mistral key';
}
// Record shortcut
window.recordShortcut = function () {
if (linuxShortcutMode) {
return;
}
const btn = document.getElementById('recordBtn');
const input = document.getElementById('shortcutInput');
// WebView2 on Windows often omits metaKey on key events; e.code still identifies Meta.
const winKeyActive = (e) =>
e.metaKey ||
e.key === 'Meta' ||
e.key === 'OS' ||
e.code === 'MetaLeft' ||
e.code === 'MetaRight';
btn.textContent = 'Recording...';
btn.disabled = true;
input.value = '';
let lastShortcut = '';
const keydownHandler = (e) => {
e.preventDefault();
e.stopPropagation();
const mods = [];
if (e.ctrlKey) mods.push('ctrl');
if (e.altKey) mods.push('alt');
if (e.shiftKey) mods.push('shift');
if (winKeyActive(e)) mods.push('win');
let key = e.key.toLowerCase();
if (key === 'control') key = 'ctrl';
if (key === 'alt') key = 'alt';
if (key === 'shift') key = 'shift';
if (key === 'meta' || key === 'os') key = 'win';
if (e.code === 'MetaLeft' || e.code === 'MetaRight') key = 'win';
if (key === ' ') key = 'space';
const parts = new Set(mods);
parts.add(key);
// Order them: modifiers first, then the latest key
const partsArray = Array.from(parts).filter(p => p !== key);
partsArray.push(key);
lastShortcut = partsArray.join('+');
input.value = lastShortcut;
// Finalize immediately if a non-modifier key is pressed
if (!['ctrl', 'alt', 'shift', 'win'].includes(key)) {
finish();
}
};
const keyupHandler = (e) => {
e.preventDefault();
e.stopPropagation();
// Finalize when keys are released if we have a valid shortcut
if (lastShortcut) {
// Check if it's a modifier-only shortcut that we should finalize
const isModifierOnly = lastShortcut.split('+').every(p => ['ctrl', 'alt', 'shift', 'win'].includes(p));
if (isModifierOnly && lastShortcut.includes('+')) {
// Wait a tiny bit to see if they press another key, but usually release means they are done
setTimeout(finish, 100);
} else if (!e.ctrlKey && !e.altKey && !e.shiftKey && !winKeyActive(e)) {
finish();
}
}
};
const finish = () => {
if (!lastShortcut) {
btn.textContent = 'Record';
btn.disabled = false;
cleanup();
return;
}
window.go.main.App.SaveSettings({ shortcut: lastShortcut }).then((result) => {
if (result && result.indexOf('Invalid shortcut') !== -1) {
alert(result);
}
btn.textContent = 'Record';
btn.disabled = false;
cleanup();
});
};
const cleanup = () => {
window.removeEventListener('keydown', keydownHandler, true);
window.removeEventListener('keyup', keyupHandler, true);
};
window.addEventListener('keydown', keydownHandler, true);
window.addEventListener('keyup', keyupHandler, true);
};
function renderHistoryList(history) {
const container = document.getElementById('historyList');
if (!history || history.length === 0) {
container.innerHTML = '<div style="text-align: center; color: var(--text-2); padding: 24px 0; font-size: 13px;">No history yet.</div>';
return;
}
container.innerHTML = '';
[...history].reverse().forEach(item => {
const el = document.createElement('div');
el.className = 'history-item';
const time = document.createElement('span');
time.className = 'history-time';
time.textContent = new Date(item.timestamp).toLocaleString(undefined, {
month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'
});
const text = document.createElement('div');
text.textContent = item.text;
el.appendChild(time);
el.appendChild(text);
container.appendChild(el);
});
}
function renderLinuxYdotoolStatus(status) {
const el = document.getElementById('linuxYdotoolStatus');
if (!el || !status) return;
const ready = !!status.ready;
el.style.display = 'block';
el.style.background = ready ? 'var(--green-dim)' : 'var(--red-dim)';
el.style.color = ready ? 'var(--green)' : 'var(--red)';
el.style.border = ready ? '1px solid rgba(61, 186, 110, 0.25)' : '1px solid rgba(224, 82, 82, 0.25)';
el.innerHTML = '';
const title = document.createElement('div');
title.style.fontWeight = '600';
title.textContent = ready ? 'Direct typing ready' : 'Direct typing needs ydotool setup';
el.appendChild(title);
const message = document.createElement('div');
message.style.marginTop = '4px';
message.textContent = status.message || '';
el.appendChild(message);
if (!ready && Array.isArray(status.setup_commands) && status.setup_commands.length) {
const pre = document.createElement('pre');
pre.style.whiteSpace = 'pre-wrap';
pre.style.margin = '8px 0 0';
pre.style.color = 'inherit';
pre.textContent = status.setup_commands.join('\n');
el.appendChild(pre);
const restart = document.createElement('div');
restart.style.marginTop = '8px';
restart.style.fontWeight = '600';
restart.textContent = 'A restart is not normally needed. Log out and back in only if the ydotool service still cannot access /dev/uinput.';
el.appendChild(restart);
}
}
async function refreshHistoryFromBackend() {
try {
const settings = await window.go.main.App.GetSettings();
renderHistoryList(settings.history || []);
} catch (err) {
console.error('Failed to refresh history:', err);
}
}
// Load settings on page load
async function loadSettings() {
try {
const settings = await window.go.main.App.GetSettings();
document.getElementById('groqApiKey').value = settings.groq_api_key || '';
document.getElementById('mistralApiKey').value = settings.mistral_api_key || '';
const shortcutInput = document.getElementById('shortcutInput');
if (shortcutInput) {
shortcutInput.value = settings.shortcut || 'alt+z';
}
linuxShortcutMode = !!settings.linux_press_mode;
if (settings.linux_press_mode) {
const shortcutSection = document.getElementById('shortcutSection');
const ydotoolSection = document.getElementById('linuxYdotoolSection');
const fallbackSection = document.getElementById('linuxFallbackSection');
const cmdInput = document.getElementById('linuxPressCommand');
if (shortcutSection) shortcutSection.style.display = 'none';
if (ydotoolSection) ydotoolSection.style.display = 'block';
if (fallbackSection) fallbackSection.style.display = 'block';
if (cmdInput) {
// Show just the executable name with quotes and the toggle flag
const fullCommand = settings.linux_press_command || '';
const match = fullCommand.match(/^'[^']*' (--action=toggle)$/);
const togglePart = match ? "'wisp-open' " + match[1] : "'wisp-open' --action=toggle";
cmdInput.value = togglePart;
window._linuxFullCommand = togglePart;
}
renderLinuxYdotoolStatus(settings.linux_ydotool_status);
}
document.getElementById('whisperModel').value = settings.whisper_model || 'whisper-large-v3-turbo';
document.getElementById('aiModel').value = settings.ai_model || 'llama-3.3-70b-versatile';
document.getElementById('aiPrompt').value = settings.ai_prompt || '';
document.getElementById('language').value = settings.language || 'en';
document.getElementById('startupToggle').checked = settings.startup || false;
const ver = (settings.app_version && String(settings.app_version).trim()) || 'dev';
document.getElementById('appVersion').textContent = ver === 'dev' ? 'dev' : ('v' + ver);
renderHistoryList(settings.history || []);
updateApiKeyStatus();
} catch (err) {
console.error("Failed to load settings:", err);
}
}
// Load mics
async function loadMics() {
try {
const mics = await window.go.main.App.GetMicrophones();
const select = document.getElementById('micDevice');
mics.forEach(mic => {
if (mic.index !== -1) {
const opt = document.createElement('option');
opt.value = mic.index;
opt.textContent = mic.name;
select.appendChild(opt);
}
});
} catch (err) {
console.error("Failed to load mics:", err);
}
}
function showSaveStatus(message = 'Saved') {
const el = document.getElementById('saveStatus');
el.textContent = message;
el.classList.add('show');
setTimeout(() => el.classList.remove('show'), 2000);
}
window.saveWhisperModel = async function () {
await window.go.main.App.SaveSettings({ whisper_model: document.getElementById('whisperModel').value });
showSaveStatus();
};
window.saveAiModel = async function () {
await window.go.main.App.SaveSettings({ ai_model: document.getElementById('aiModel').value });
showSaveStatus();
};
window.saveLanguage = async function () {
await window.go.main.App.SaveSettings({ language: document.getElementById('language').value });
showSaveStatus();
};
window.saveMicDevice = async function () {
const val = document.getElementById('micDevice').value;
await window.go.main.App.SaveSettings({ microphone_device: val === 'default' ? null : parseInt(val) });
showSaveStatus();
};
window.savePrompt = async function () {
await window.go.main.App.SaveSettings({ ai_prompt: document.getElementById('aiPrompt').value.trim() });
showSaveStatus('Prompt saved');
};
window.copyLinuxPressCommand = async function () {
const cmdInput = document.getElementById('linuxPressCommand');
if (!cmdInput) return;
// Reconstruct the full command from the backend settings
const command = window._linuxFullCommand || cmdInput.value || '';
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(command);
} else {
cmdInput.focus();
cmdInput.select();
document.execCommand('copy');
}
showSaveStatus('Command copied');
} catch (err) {
console.error('Failed to copy Linux command:', err);
}
};
window.toggleStartup = async function () {
await window.go.main.App.ToggleStartup(document.getElementById('startupToggle').checked);
};
window.clearHistory = async function () {
if (confirm('Clear all transcription history?')) {
await window.go.main.App.ClearHistory();
renderHistoryList([]);
}
};
// Check connection status
async function checkConnection() {
try {
const isOnline = await window.go.main.App.CheckOnline();
const statusEl = document.getElementById('connectionStatus');
if (isOnline) {
statusEl.innerHTML = '● Online';
statusEl.style.background = 'var(--green-dim)';
statusEl.style.color = 'var(--green)';
statusEl.style.border = '1px solid rgba(61, 186, 110, 0.2)';
} else {
statusEl.innerHTML = '● Offline \u2014 install local Whisper for transcription';
statusEl.style.background = 'var(--red-dim)';
statusEl.style.color = 'var(--red)';
statusEl.style.border = '1px solid rgba(224, 82, 82, 0.2)';
// Disable cloud AI options when offline
const aiSelect = document.getElementById('aiModel');
for (let opt of aiSelect.options) {
if (opt.value !== 'None' && !opt.value.startsWith('local-')) {
opt.disabled = true;
if (!opt.text.includes('(offline)')) {
opt.text += ' (offline)';
}
}
}
}
} catch (err) {
console.error('Connection check failed:', err);
}
}
// Check whisper installation status
async function checkWhisperStatus() {
try {
const info = await window.go.main.App.GetWhisperInfo();
const whisperSelect = document.getElementById('whisperModel');
// Remove any existing local option
const existingLocal = whisperSelect.querySelector('option[value^="local-"]');
if (existingLocal) existingLocal.remove();
if (info.installed) {
document.getElementById('whisperNotInstalled').style.display = 'none';
document.getElementById('whisperInstalled').style.display = 'block';
document.getElementById('installedModel').textContent = info.model + ' model';
// Add local option to whisper dropdown
const localOption = document.createElement('option');
localOption.value = 'local-' + info.model;
localOption.textContent = '↑ Local \u2014 ' + info.model + ' (offline)';
localOption.style.fontWeight = 'bold';
whisperSelect.insertBefore(localOption, whisperSelect.firstChild);
} else {
document.getElementById('whisperNotInstalled').style.display = 'block';
document.getElementById('whisperInstalled').style.display = 'none';
}
} catch (err) {
console.error('Whisper status check failed:', err);
}
}
// Install whisper
window.installWhisper = async function () {
const model = document.getElementById('whisperModelSelect').value;
const btn = document.getElementById('installBtn');
btn.disabled = true;
btn.textContent = 'Starting installation...';
try {
const result = await window.go.main.App.InstallWhisper(model);
alert(result + '\n\nA terminal window will open showing the installation progress. This may take several minutes depending on your internet speed.');
// Check status after a delay
setTimeout(checkWhisperStatus, 5000);
} catch (err) {
alert('Installation failed: ' + err);
}
btn.disabled = false;
btn.textContent = 'Install Offline Whisper';
};
// Uninstall whisper
window.uninstallWhisper = async function () {
if (!confirm('Uninstall offline Whisper? This will delete the downloaded model.')) {
return;
}
try {
const result = await window.go.main.App.UninstallWhisper();
alert(result);
checkWhisperStatus();
} catch (err) {
alert('Uninstall failed: ' + err);
}
};
// Init
async function initialize() {
try {
const isLinux = navigator.userAgent.toLowerCase().includes('linux');
if (isLinux) {
const offlineSection = document.getElementById('offlineWhisperSection');
if (offlineSection) offlineSection.style.display = 'none';
const startupLabel = document.getElementById('startupLabel');
if (startupLabel) startupLabel.innerText = 'Run on Startup';
}
} catch (err) {
console.error('Failed to get environment:', err);
}
await loadSettings();
await loadMics();
await checkConnection();
await checkWhisperStatus();
if (window.runtime && typeof window.runtime.EventsOn === 'function') {
window.runtime.EventsOn('history:updated', function () {
refreshHistoryFromBackend();
});
// Keep the startup checkbox in sync when toggled from the tray menu
window.runtime.EventsOn('startup:changed', function (enabled) {
document.getElementById('startupToggle').checked = enabled;
});
}
// If Linux, ensure Local Whisper option is removed
if (navigator.userAgent.toLowerCase().includes('linux')) {
const whisperSelect = document.getElementById('whisperModel');
if (whisperSelect) {
Array.from(whisperSelect.options).forEach(opt => {
if (opt.value.startsWith('local-')) opt.remove();
});
}
}
}
initialize();
</script>
</body>
</html>