feat: add backend configuration and windows utilities, and initialize frontend dependencies

This commit is contained in:
jahruz67
2026-05-15 10:39:30 -07:00
parent e645102d3a
commit b0f4ea3e6f
43 changed files with 931 additions and 541 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
:root{--bg-color: #0b0f1a;--card-bg: rgba(30, 41, 59, .7);--input-bg: rgba(51, 65, 85, .5);--text-color: #f8fafc;--text-muted: #94a3b8;--primary-color: #3b82f6;--primary-hover: #2563eb;--accent-color: #6366f1;--border-color: rgba(75, 85, 99, .4);--success-color: #10b981;--glass-border: rgba(255, 255, 255, .1);color-scheme:dark}body{background:radial-gradient(circle at top right,#1e293b,#0b0f1a);color:var(--text-color);font-family:Inter,system-ui,-apple-system,sans-serif;margin:0;padding:30px;min-height:100vh;letter-spacing:-.01em}.container{max-width:720px;margin:0 auto}h1{font-size:28px;font-weight:700;background:linear-gradient(135deg,#60a5fa,#a78bfa);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:30px}.section{background:var(--card-bg);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);padding:24px;border-radius:16px;margin-bottom:24px;border:1px solid var(--glass-border);box-shadow:0 10px 15px -3px #0003;transition:transform .2s ease}.section:hover{border-color:#fff3}label{display:block;color:var(--text-muted);font-size:13px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;margin-bottom:10px}input[type=checkbox]{accent-color:var(--primary-color);width:18px;height:18px;cursor:pointer}input[type=text],input[type=password],select,textarea{width:100%;background:var(--input-bg);border:1px solid var(--border-color);color:#fff;padding:12px 14px;border-radius:10px;font-size:14px;transition:all .2s ease;box-sizing:border-box}select{appearance:none;-webkit-appearance:none;background-image:url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right 14px center;background-size:16px;padding-right:40px}select option{background:var(--bg-color);color:#fff}textarea{resize:vertical;min-height:80px}input:focus,select:focus,textarea:focus{outline:none;border-color:var(--primary-color);background:rgba(51,65,85,.8);box-shadow:0 0 0 3px #3b82f633}.form-control{max-width:480px;width:100%}.input-wrapper{position:relative;flex:1;min-width:0}.input-wrapper input{width:100%;padding-right:44px;box-sizing:border-box;display:block}.eye-btn{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:transparent!important;border:none!important;box-shadow:none!important;cursor:pointer;padding:4px!important;font-size:16px;opacity:.5;transition:opacity .2s;line-height:1;display:flex;align-items:center;justify-content:center}.eye-btn:hover{opacity:1;transform:translateY(-50%) scale(1.1)}.save-status{position:fixed;bottom:30px;right:30px;background:var(--success-color);color:#fff;padding:12px 24px;border-radius:12px;font-weight:600;box-shadow:0 10px 15px -3px #0003;transform:translateY(100px);opacity:0;transition:all .3s cubic-bezier(.4,0,.2,1);z-index:1000}.save-status.show{transform:translateY(0);opacity:1}button{background-color:var(--primary-color);color:#fff;border:none;padding:10px 20px;border-radius:10px;cursor:pointer;font-weight:600;font-size:14px;transition:all .2s cubic-bezier(.4,0,.2,1);box-shadow:0 4px 6px -1px #0003;white-space:nowrap;flex-shrink:0}button:hover{transform:translateY(-1px);box-shadow:0 10px 15px -3px #0000004d;background-color:var(--primary-hover)}button:active{transform:translateY(0)}button[style*="background: transparent"]{background:transparent!important;box-shadow:none!important;text-decoration:underline;opacity:.7}button[style*="background: transparent"]:hover{opacity:1}.flex-row{display:flex;gap:12px;align-items:center;flex-wrap:nowrap}.flex-between{display:flex;justify-content:space-between;align-items:center}.history-list{max-height:250px;overflow-y:auto;margin-top:10px;padding-right:5px}.history-item{background:rgba(255,255,255,.03);padding:14px;border-radius:12px;margin-bottom:10px;border:1px solid rgba(255,255,255,.05);transition:all .2s ease}.history-item:hover{background:rgba(255,255,255,.06);transform:translate(2px)}.history-time{color:var(--primary-color);font-size:11px;font-weight:700;margin-bottom:6px;display:block}::-webkit-scrollbar{width:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--border-color);border-radius:10px}::-webkit-scrollbar-thumb:hover{background:var(--text-muted)}
File diff suppressed because one or more lines are too long
+65 -63
View File
@@ -4,25 +4,28 @@
<head>
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title>wis-free-v3 Settings</title>
<title>Wisp Settings</title>
<script type="module" crossorigin src="/assets/index.c31e3052.js"></script>
<link rel="stylesheet" href="/assets/index.d8635aeb.css">
<script type="module" crossorigin src="/assets/index.fbf781c1.js"></script>
<link rel="stylesheet" href="/assets/index.6e77aa4d.css">
</head>
<body>
<div id="app" class="container">
<!-- Header -->
<div class="flex-between" style="margin-bottom: 30px;">
<h1 style="margin: 0;">wis-free-v3 Settings</h1>
<div id="appVersion" style="color: var(--text-muted); font-size: 12px;"></div>
<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">Settings Saved!</div>
<div id="saveStatus" class="save-status">Saved</div>
<!-- Settings Form -->
<div id="settingsForm">
<div class="section-group-label">Authentication</div>
<!-- API Key -->
<div class="section">
<label>Groq API Key</label>
@@ -35,15 +38,14 @@
<button onclick="saveApiKey()">Save</button>
</div>
</div>
<p style="font-size: 12px; color: var(--text-muted); margin-top: 8px;">
Get your free API key at <a href="https://console.groq.com/keys" target="_blank"
style="color: var(--primary);">console.groq.com/keys</a>
</p>
<p class="hint">Get your free key at <a href="https://console.groq.com/keys" target="_blank" style="color: var(--accent);">console.groq.com/keys</a></p>
</div>
<div class="section-group-label">Input</div>
<!-- Shortcut -->
<div class="section">
<label>Shortcut (Hold to Record)</label>
<label>Hotkey</label>
<div class="form-control">
<div class="flex-row">
<div class="input-wrapper">
@@ -52,11 +54,19 @@
<button onclick="recordShortcut()" id="recordBtn">Record</button>
</div>
</div>
<p style="font-size: 12px; color: var(--text-muted); margin-top: 8px;">
Click Record, then press your desired key combination (e.g., Ctrl+X)
</p>
<p class="hint">Click Record, then press your desired key combination (e.g. Ctrl+X).</p>
</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>
@@ -68,19 +78,22 @@
<!-- Language -->
<div class="section">
<label>Transcription Language</label>
<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 style="font-size: 11px; color: var(--text-muted); margin-top: 5px;">Specifying language reduces latency and improves accuracy.</p>
<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 (Text Refinement)</label>
<label>AI Model</label>
<select id="aiModel" onchange="saveAiModel()" class="form-control">
<option value="None">None (Skip Refinement)</option>
<option value="None">None — skip refinement</option>
<option value="openai/gpt-oss-120b-high">openai/gpt-oss-120b (high reasoning)</option>
<option value="openai/gpt-oss-120b">openai/gpt-oss-120b (low reasoning)</option>
<option value="openai/gpt-oss-20b-high">openai/gpt-oss-20b (high reasoning)</option>
@@ -90,91 +103,80 @@
</select>
</div>
<!-- Microphone -->
<div class="section">
<label>Microphone</label>
<select id="micDevice" onchange="saveMicDevice()" class="form-control">
<option value="default">System Default</option>
</select>
</div>
<!-- AI Prompt -->
<div class="section">
<label>AI Prompt (System Message)</label>
<textarea id="aiPrompt" rows="3" placeholder="Custom instructions for text refinement..."
class="form-control"></textarea>
<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">
<span id="startupLabel" style="font-weight: 500;">Run on System Startup</span>
<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;">
<span>Enable</span>
</label>
</div>
<!-- History -->
<div class="section">
<div class="flex-between" style="margin-bottom: 10px;">
<div class="flex-between" style="margin-bottom: 12px;">
<label style="margin: 0;">Transcription History</label>
<button onclick="clearHistory()"
style="background: transparent; color: #f87171; padding: 0; font-size: 12px;">Clear</button>
<button onclick="clearHistory()" class="btn-link">Clear all</button>
</div>
<div id="historyList" class="history-list">
<div style="text-align: center; color: var(--text-muted); padding: 20px;">No history yet.</div>
<div style="text-align: center; color: var(--text-2); padding: 24px 0; font-size: 13px;">No history yet.</div>
</div>
</div>
<!-- Offline Mode -->
<div id="offlineWhisperSection" class="section"
style="margin-top: 30px; padding-top: 20px; border-top: 1px solid var(--border-color);">
<!-- Offline Whisper -->
<div id="offlineWhisperSection" class="section" style="margin-top: 20px; padding-top: 20px; border-top: 1px solid var(--border);">
<label>Offline Whisper</label>
<!-- Online/Offline Status -->
<div id="connectionStatus"
style="margin-bottom: 15px; padding: 10px; border-radius: 8px; text-align: center;">
<!-- 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 style="font-size: 13px; color: var(--text-muted); margin-bottom: 15px;">
<p class="hint" style="margin-bottom: 14px;">
Install offline transcription for use without internet. Uses GPU acceleration if available.
</p>
<div style="margin-bottom: 15px;">
<label style="font-size: 12px;">Model Size:</label>
<select id="whisperModelSelect" style="margin-top: 5px;" 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) - High Quality</option>
<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"
style="background: linear-gradient(135deg, #6366f1, #8b5cf6); padding: 12px 24px; font-size: 14px; width: 100%;">
🚀 Install Offline Whisper
<button onclick="installWhisper()" id="installBtn">
Install Offline Whisper
</button>
</div>
<div id="whisperInstalled" style="display: none;">
<div
style="background: rgba(34, 197, 94, 0.1); border: 1px solid rgba(34, 197, 94, 0.3); border-radius: 8px; padding: 15px; margin-bottom: 15px;">
<div style="color: #22c55e; font-weight: 500;">✓ Offline Whisper Installed</div>
<div style="font-size: 12px; color: var(--text-muted); margin-top: 5px;">
Model: <span id="installedModel">-</span>
<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 style="font-size: 12px; color: var(--text-muted); margin-bottom: 15px;">
Select "Local - Whisper" in the Whisper Model dropdown above to use offline transcription.
<p class="hint" style="margin-bottom: 14px;">
Select "Local Whisper" in the Whisper Model dropdown above to use offline transcription.
</p>
<button onclick="uninstallWhisper()"
style="background: transparent; border: 1px solid #f87171; color: #f87171; padding: 10px 20px; font-size: 13px;">
🗑️ Uninstall Offline Whisper
<button onclick="uninstallWhisper()" class="btn-danger">
Uninstall Offline Whisper
</button>
</div>
</div>
+84 -89
View File
@@ -4,23 +4,26 @@
<head>
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title>wis-free-v3 Settings</title>
<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: 30px;">
<h1 style="margin: 0;">wis-free-v3 Settings</h1>
<div id="appVersion" style="color: var(--text-muted); font-size: 12px;"></div>
<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">Settings Saved!</div>
<div id="saveStatus" class="save-status">Saved</div>
<!-- Settings Form -->
<div id="settingsForm">
<div class="section-group-label">Authentication</div>
<!-- API Key -->
<div class="section">
<label>Groq API Key</label>
@@ -33,15 +36,14 @@
<button onclick="saveApiKey()">Save</button>
</div>
</div>
<p style="font-size: 12px; color: var(--text-muted); margin-top: 8px;">
Get your free API key at <a href="https://console.groq.com/keys" target="_blank"
style="color: var(--primary);">console.groq.com/keys</a>
</p>
<p class="hint">Get your free key at <a href="https://console.groq.com/keys" target="_blank" style="color: var(--accent);">console.groq.com/keys</a></p>
</div>
<div class="section-group-label">Input</div>
<!-- Shortcut -->
<div class="section">
<label>Shortcut (Hold to Record)</label>
<label>Hotkey</label>
<div class="form-control">
<div class="flex-row">
<div class="input-wrapper">
@@ -50,11 +52,19 @@
<button onclick="recordShortcut()" id="recordBtn">Record</button>
</div>
</div>
<p style="font-size: 12px; color: var(--text-muted); margin-top: 8px;">
Click Record, then press your desired key combination (e.g., Ctrl+X)
</p>
<p class="hint">Click Record, then press your desired key combination (e.g. Ctrl+X).</p>
</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>
@@ -66,19 +76,22 @@
<!-- Language -->
<div class="section">
<label>Transcription Language</label>
<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 style="font-size: 11px; color: var(--text-muted); margin-top: 5px;">Specifying language reduces latency and improves accuracy.</p>
<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 (Text Refinement)</label>
<label>AI Model</label>
<select id="aiModel" onchange="saveAiModel()" class="form-control">
<option value="None">None (Skip Refinement)</option>
<option value="None">None — skip refinement</option>
<option value="openai/gpt-oss-120b-high">openai/gpt-oss-120b (high reasoning)</option>
<option value="openai/gpt-oss-120b">openai/gpt-oss-120b (low reasoning)</option>
<option value="openai/gpt-oss-20b-high">openai/gpt-oss-20b (high reasoning)</option>
@@ -88,91 +101,80 @@
</select>
</div>
<!-- Microphone -->
<div class="section">
<label>Microphone</label>
<select id="micDevice" onchange="saveMicDevice()" class="form-control">
<option value="default">System Default</option>
</select>
</div>
<!-- AI Prompt -->
<div class="section">
<label>AI Prompt (System Message)</label>
<textarea id="aiPrompt" rows="3" placeholder="Custom instructions for text refinement..."
class="form-control"></textarea>
<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">
<span id="startupLabel" style="font-weight: 500;">Run on System Startup</span>
<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;">
<span>Enable</span>
</label>
</div>
<!-- History -->
<div class="section">
<div class="flex-between" style="margin-bottom: 10px;">
<div class="flex-between" style="margin-bottom: 12px;">
<label style="margin: 0;">Transcription History</label>
<button onclick="clearHistory()"
style="background: transparent; color: #f87171; padding: 0; font-size: 12px;">Clear</button>
<button onclick="clearHistory()" class="btn-link">Clear all</button>
</div>
<div id="historyList" class="history-list">
<div style="text-align: center; color: var(--text-muted); padding: 20px;">No history yet.</div>
<div style="text-align: center; color: var(--text-2); padding: 24px 0; font-size: 13px;">No history yet.</div>
</div>
</div>
<!-- Offline Mode -->
<div id="offlineWhisperSection" class="section"
style="margin-top: 30px; padding-top: 20px; border-top: 1px solid var(--border-color);">
<!-- Offline Whisper -->
<div id="offlineWhisperSection" class="section" style="margin-top: 20px; padding-top: 20px; border-top: 1px solid var(--border);">
<label>Offline Whisper</label>
<!-- Online/Offline Status -->
<div id="connectionStatus"
style="margin-bottom: 15px; padding: 10px; border-radius: 8px; text-align: center;">
<!-- 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 style="font-size: 13px; color: var(--text-muted); margin-bottom: 15px;">
<p class="hint" style="margin-bottom: 14px;">
Install offline transcription for use without internet. Uses GPU acceleration if available.
</p>
<div style="margin-bottom: 15px;">
<label style="font-size: 12px;">Model Size:</label>
<select id="whisperModelSelect" style="margin-top: 5px;" 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) - High Quality</option>
<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"
style="background: linear-gradient(135deg, #6366f1, #8b5cf6); padding: 12px 24px; font-size: 14px; width: 100%;">
🚀 Install Offline Whisper
<button onclick="installWhisper()" id="installBtn">
Install Offline Whisper
</button>
</div>
<div id="whisperInstalled" style="display: none;">
<div
style="background: rgba(34, 197, 94, 0.1); border: 1px solid rgba(34, 197, 94, 0.3); border-radius: 8px; padding: 15px; margin-bottom: 15px;">
<div style="color: #22c55e; font-weight: 500;">✓ Offline Whisper Installed</div>
<div style="font-size: 12px; color: var(--text-muted); margin-top: 5px;">
Model: <span id="installedModel">-</span>
<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 style="font-size: 12px; color: var(--text-muted); margin-bottom: 15px;">
Select "Local - Whisper" in the Whisper Model dropdown above to use offline transcription.
<p class="hint" style="margin-bottom: 14px;">
Select "Local Whisper" in the Whisper Model dropdown above to use offline transcription.
</p>
<button onclick="uninstallWhisper()"
style="background: transparent; border: 1px solid #f87171; color: #f87171; padding: 10px 20px; font-size: 13px;">
🗑️ Uninstall Offline Whisper
<button onclick="uninstallWhisper()" class="btn-danger">
Uninstall Offline Whisper
</button>
</div>
</div>
@@ -292,7 +294,7 @@
function renderHistoryList(history) {
const container = document.getElementById('historyList');
if (!history || history.length === 0) {
container.innerHTML = '<div style="text-align: center; color: var(--text-muted); padding: 20px;">No history yet.</div>';
container.innerHTML = '<div style="text-align: center; color: var(--text-2); padding: 24px 0; font-size: 13px;">No history yet.</div>';
return;
}
container.innerHTML = '';
@@ -371,7 +373,7 @@
showSaveStatus();
};
function showSaveStatus(message = 'Settings Saved!') {
function showSaveStatus(message = 'Saved') {
const el = document.getElementById('saveStatus');
el.textContent = message;
el.classList.add('show');
@@ -390,7 +392,7 @@
window.saveLanguage = async function () {
await window.go.main.App.SaveSettings({ language: document.getElementById('language').value });
showSaveStatus('Language Saved!');
showSaveStatus();
};
window.saveMicDevice = async function () {
@@ -401,7 +403,7 @@
window.savePrompt = async function () {
await window.go.main.App.SaveSettings({ ai_prompt: document.getElementById('aiPrompt').value.trim() });
showSaveStatus('Prompt Saved!');
showSaveStatus('Prompt saved');
};
window.toggleStartup = async function () {
@@ -409,7 +411,7 @@
};
window.clearHistory = async function () {
if (confirm('Clear all history?')) {
if (confirm('Clear all transcription history?')) {
await window.go.main.App.ClearHistory();
renderHistoryList([]);
}
@@ -422,19 +424,19 @@
const statusEl = document.getElementById('connectionStatus');
if (isOnline) {
statusEl.innerHTML = '🟢 Online - Cloud transcription available';
statusEl.style.background = 'rgba(34, 197, 94, 0.1)';
statusEl.style.color = '#22c55e';
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 - Install local Whisper for transcription';
statusEl.style.background = 'rgba(239, 68, 68, 0.1)';
statusEl.style.color = '#ef4444';
statusEl.innerHTML = ' Offline — 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');
const whisperSelect = document.getElementById('whisperModel');
// Add offline notice to options
for (let opt of aiSelect.options) {
if (opt.value !== 'None' && !opt.value.startsWith('local-')) {
opt.disabled = true;
@@ -467,7 +469,7 @@
// Add local option to whisper dropdown
const localOption = document.createElement('option');
localOption.value = 'local-' + info.model;
localOption.textContent = '🖥️ Local - ' + info.model + ' (offline)';
localOption.textContent = ' Local ' + info.model + ' (offline)';
localOption.style.fontWeight = 'bold';
whisperSelect.insertBefore(localOption, whisperSelect.firstChild);
} else {
@@ -498,12 +500,12 @@
}
btn.disabled = false;
btn.textContent = '🚀 Install Offline Whisper';
btn.textContent = 'Install Offline Whisper';
};
// Uninstall whisper
window.uninstallWhisper = async function () {
if (!confirm('Are you sure you want to uninstall offline Whisper? This will delete the downloaded model.')) {
if (!confirm('Uninstall offline Whisper? This will delete the downloaded model.')) {
return;
}
@@ -518,21 +520,14 @@
// Init
async function initialize() {
// Check platform definitively using userAgent to guarantee it fires immediately without Wails timing issues
try {
const isLinux = navigator.userAgent.toLowerCase().includes('linux');
if (isLinux) {
// Hide Offline Whisper section
const offlineSection = document.getElementById('offlineWhisperSection');
if (offlineSection) {
offlineSection.style.display = 'none';
}
// Clean up 'Windows' reference in startup text specifically for Linux OS
if (offlineSection) offlineSection.style.display = 'none';
const startupLabel = document.getElementById('startupLabel');
if (startupLabel) {
startupLabel.innerText = 'Run on Linux OS Startup';
}
if (startupLabel) startupLabel.innerText = 'Run on Startup';
}
} catch (err) {
console.error('Failed to get environment:', err);
@@ -548,8 +543,8 @@
refreshHistoryFromBackend();
});
}
// If Linux, ensure Local Whisper option is removed from Whisper Select as a secondary guard
// If Linux, ensure Local Whisper option is removed
if (navigator.userAgent.toLowerCase().includes('linux')) {
const whisperSelect = document.getElementById('whisperModel');
if (whisperSelect) {
+16 -1
View File
@@ -1 +1,16 @@
../esbuild/bin/esbuild
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../esbuild/bin/esbuild" "$@"
else
exec node "$basedir/../esbuild/bin/esbuild" "$@"
fi
+17
View File
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\esbuild\bin\esbuild" %*
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../esbuild/bin/esbuild" $args
} else {
& "$basedir/node$exe" "$basedir/../esbuild/bin/esbuild" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../esbuild/bin/esbuild" $args
} else {
& "node$exe" "$basedir/../esbuild/bin/esbuild" $args
}
$ret=$LASTEXITCODE
}
exit $ret
+16 -1
View File
@@ -1 +1,16 @@
../nanoid/bin/nanoid.cjs
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../nanoid/bin/nanoid.cjs" "$@"
else
exec node "$basedir/../nanoid/bin/nanoid.cjs" "$@"
fi
+17
View File
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\nanoid\bin\nanoid.cjs" %*
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args
} else {
& "$basedir/node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args
} else {
& "node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args
}
$ret=$LASTEXITCODE
}
exit $ret
+16 -1
View File
@@ -1 +1,16 @@
../resolve/bin/resolve
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../resolve/bin/resolve" "$@"
else
exec node "$basedir/../resolve/bin/resolve" "$@"
fi
+17
View File
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\resolve\bin\resolve" %*
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../resolve/bin/resolve" $args
} else {
& "$basedir/node$exe" "$basedir/../resolve/bin/resolve" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../resolve/bin/resolve" $args
} else {
& "node$exe" "$basedir/../resolve/bin/resolve" $args
}
$ret=$LASTEXITCODE
}
exit $ret
+16 -1
View File
@@ -1 +1,16 @@
../rollup/dist/bin/rollup
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../rollup/dist/bin/rollup" "$@"
else
exec node "$basedir/../rollup/dist/bin/rollup" "$@"
fi
+17
View File
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\rollup\dist\bin\rollup" %*
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../rollup/dist/bin/rollup" $args
} else {
& "$basedir/node$exe" "$basedir/../rollup/dist/bin/rollup" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../rollup/dist/bin/rollup" $args
} else {
& "node$exe" "$basedir/../rollup/dist/bin/rollup" $args
}
$ret=$LASTEXITCODE
}
exit $ret
+16 -1
View File
@@ -1 +1,16 @@
../vite/bin/vite.js
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../vite/bin/vite.js" "$@"
else
exec node "$basedir/../vite/bin/vite.js" "$@"
fi
+17
View File
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\vite\bin\vite.js" %*
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../vite/bin/vite.js" $args
} else {
& "$basedir/node$exe" "$basedir/../vite/bin/vite.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../vite/bin/vite.js" $args
} else {
& "node$exe" "$basedir/../vite/bin/vite.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret
+4 -4
View File
@@ -42,10 +42,10 @@
"esbuild-windows-arm64": "0.15.18"
}
},
"node_modules/esbuild-linux-64": {
"node_modules/esbuild-windows-64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz",
"integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==",
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz",
"integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==",
"cpu": [
"x64"
],
@@ -53,7 +53,7 @@
"license": "MIT",
"optional": true,
"os": [
"linux"
"win32"
],
"engines": {
"node": ">=12"
-3
View File
@@ -1,3 +0,0 @@
# esbuild
This is the Linux 64-bit binary for esbuild, a JavaScript bundler and minifier. See https://github.com/evanw/esbuild for details.
+3
View File
@@ -0,0 +1,3 @@
# esbuild
This is the Windows 64-bit binary for esbuild, a JavaScript bundler and minifier. See https://github.com/evanw/esbuild for details.
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env node
// Unfortunately even though npm shims "bin" commands on Windows with auto-
// generated forwarding scripts, it doesn't strip the ".exe" from the file name
// first. So it's possible to publish executables via npm on all platforms
// except Windows. I consider this a npm bug.
//
// My workaround is to add this script as another layer of indirection. It'll
// be slower because node has to boot up just to shell out to the actual exe,
// but Windows is somewhat of a second-class platform to npm so it's the best
// I can do I think.
const esbuild_exe = require.resolve('esbuild-windows-64/esbuild.exe');
const child_process = require('child_process');
child_process.spawnSync(esbuild_exe, process.argv.slice(2), { stdio: 'inherit' });
Binary file not shown.
@@ -1,7 +1,7 @@
{
"name": "esbuild-linux-64",
"name": "esbuild-windows-64",
"version": "0.15.18",
"description": "The Linux 64-bit binary for esbuild, a JavaScript bundler.",
"description": "The Windows 64-bit binary for esbuild, a JavaScript bundler.",
"repository": "https://github.com/evanw/esbuild",
"license": "MIT",
"preferUnplugged": true,
@@ -9,7 +9,7 @@
"node": ">=12"
},
"os": [
"linux"
"win32"
],
"cpu": [
"x64"
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
5fbf12469d224a93954efecb5886e8a6
be0c7dc3573b2470ab6a8ec4c48845b6
+256 -169
View File
@@ -1,125 +1,168 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');
:root {
--bg-color: #0b0f1a;
--card-bg: rgba(30, 41, 59, 0.7);
--input-bg: rgba(51, 65, 85, 0.5);
--text-color: #f8fafc;
--text-muted: #94a3b8;
--primary-color: #3b82f6;
--primary-hover: #2563eb;
--accent-color: #6366f1;
--border-color: rgba(75, 85, 99, 0.4);
--success-color: #10b981;
--glass-border: rgba(255, 255, 255, 0.1);
--bg: #111113;
--surface: #1a1a1e;
--surface-2: #222226;
--border: rgba(255, 255, 255, 0.07);
--border-hover: rgba(255, 255, 255, 0.13);
--text: #e8e8ea;
--text-2: #8a8a90;
--text-3: #555560;
--accent: #e8622a;
--accent-dim: rgba(232, 98, 42, 0.15);
--accent-hover: #f07040;
--green: #3dba6e;
--green-dim: rgba(61, 186, 110, 0.12);
--red: #e05252;
--red-dim: rgba(224, 82, 82, 0.12);
--radius: 10px;
--radius-sm: 7px;
color-scheme: dark;
}
* { box-sizing: border-box; }
body {
background: radial-gradient(circle at top right, #1e293b, #0b0f1a);
color: var(--text-color);
background: var(--bg);
color: var(--text);
font-family: 'Inter', system-ui, -apple-system, sans-serif;
font-size: 14px;
margin: 0;
padding: 30px;
padding: 0;
min-height: 100vh;
letter-spacing: -0.01em;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
/* ── Layout ── */
.container {
max-width: 720px;
max-width: 680px;
margin: 0 auto;
padding: 32px 28px 60px;
}
/* ── Header ── */
h1 {
font-size: 28px;
font-weight: 700;
background: linear-gradient(135deg, #60a5fa, #a78bfa);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 30px;
font-size: 20px;
font-weight: 600;
color: var(--text);
margin: 0;
letter-spacing: -0.02em;
}
.flex-between {
display: flex;
justify-content: space-between;
align-items: center;
}
.flex-row {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: nowrap;
}
/* ── Section cards ── */
.section {
background: var(--card-bg);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
padding: 24px;
border-radius: 16px;
margin-bottom: 24px;
border: 1px solid var(--glass-border);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.2);
transition: transform 0.2s ease;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px 22px;
margin-bottom: 10px;
transition: border-color 0.15s ease;
}
.section:hover {
border-color: rgba(255, 255, 255, 0.2);
border-color: var(--border-hover);
}
/* ── Labels ── */
label {
display: block;
color: var(--text-muted);
font-size: 13px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 10px;
font-weight: 500;
color: var(--text);
margin-bottom: 9px;
text-transform: none;
letter-spacing: normal;
}
input[type="checkbox"] {
accent-color: var(--primary-color);
width: 18px;
height: 18px;
cursor: pointer;
.hint {
font-size: 12px;
color: var(--text-2);
margin-top: 7px;
line-height: 1.5;
}
/* ── Inputs ── */
input[type="text"],
input[type="password"],
select,
textarea {
width: 100%;
background: var(--input-bg);
border: 1px solid var(--border-color);
color: white;
padding: 12px 14px;
border-radius: 10px;
font-size: 14px;
transition: all 0.2s ease;
box-sizing: border-box;
}
select {
appearance: none;
background: var(--surface-2);
border: 1px solid var(--border);
color: var(--text);
padding: 9px 12px;
border-radius: var(--radius-sm);
font-size: 13.5px;
font-family: inherit;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
outline: none;
-webkit-appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 14px center;
background-size: 16px;
padding-right: 40px;
}
select option {
background: var(--bg-color);
color: white;
}
textarea {
resize: vertical;
min-height: 80px;
appearance: none;
}
input:focus,
select:focus,
textarea:focus {
outline: none;
border-color: var(--primary-color);
background: rgba(51, 65, 85, 0.8);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-dim);
}
input::placeholder,
textarea::placeholder {
color: var(--text-3);
}
/* Custom select arrow */
select {
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23555560' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='4 6 8 10 12 6'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 11px center;
background-size: 14px;
padding-right: 34px;
cursor: pointer;
}
select option {
background: var(--surface-2);
color: var(--text);
}
textarea {
resize: vertical;
min-height: 80px;
line-height: 1.6;
}
input[type="checkbox"] {
accent-color: var(--accent);
width: 16px;
height: 16px;
cursor: pointer;
border-radius: 4px;
flex-shrink: 0;
}
/* ── Form control width ── */
.form-control {
max-width: 480px;
width: 100%;
}
/* ── Input wrapper (for eye-btn) ── */
.input-wrapper {
position: relative;
flex: 1;
@@ -127,10 +170,7 @@ textarea:focus {
}
.input-wrapper input {
width: 100%;
padding-right: 44px;
box-sizing: border-box;
display: block;
padding-right: 40px;
}
.eye-btn {
@@ -142,35 +182,96 @@ textarea:focus {
border: none !important;
box-shadow: none !important;
cursor: pointer;
padding: 4px !important;
font-size: 16px;
opacity: 0.5;
transition: opacity 0.2s;
padding: 2px !important;
font-size: 15px;
opacity: 0.35;
transition: opacity 0.15s;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
}
.eye-btn:hover {
opacity: 1;
transform: translateY(-50%) scale(1.1);
opacity: 0.8;
transform: translateY(-50%);
box-shadow: none !important;
}
/* ── Buttons ── */
button {
background: var(--accent);
color: #fff;
border: none;
padding: 9px 16px;
border-radius: var(--radius-sm);
cursor: pointer;
font-family: inherit;
font-weight: 500;
font-size: 13.5px;
transition: background 0.15s ease, transform 0.1s ease;
white-space: nowrap;
flex-shrink: 0;
}
button:hover {
background: var(--accent-hover);
transform: none;
box-shadow: none;
}
button:active {
transform: scale(0.97);
}
.btn-link {
background: transparent !important;
border: none !important;
box-shadow: none !important;
color: var(--red) !important;
padding: 0 !important;
font-size: 12px !important;
text-decoration: none;
opacity: 0.75;
min-height: 0;
}
.btn-link:hover {
opacity: 1;
transform: none;
background: transparent !important;
}
.btn-danger {
background: transparent !important;
border: 1px solid var(--red) !important;
color: var(--red) !important;
box-shadow: none !important;
font-size: 13px !important;
}
.btn-danger:hover {
background: var(--red-dim) !important;
opacity: 1 !important;
}
/* ── Save status toast ── */
.save-status {
position: fixed;
bottom: 30px;
right: 30px;
background: var(--success-color);
color: white;
padding: 12px 24px;
border-radius: 12px;
font-weight: 600;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.2);
transform: translateY(100px);
bottom: 24px;
right: 24px;
background: var(--surface-2);
border: 1px solid var(--border-hover);
color: var(--green);
padding: 10px 18px;
border-radius: var(--radius);
font-weight: 500;
font-size: 13px;
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
transform: translateY(16px);
opacity: 0;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 1000;
pointer-events: none;
}
.save-status.show {
@@ -178,98 +279,84 @@ textarea:focus {
opacity: 1;
}
button {
background-color: var(--primary-color);
color: white;
border: none;
padding: 10px 20px;
border-radius: 10px;
cursor: pointer;
font-weight: 600;
font-size: 14px;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.2);
white-space: nowrap;
flex-shrink: 0;
}
button:hover {
transform: translateY(-1px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3);
background-color: var(--primary-hover);
}
button:active {
transform: translateY(0);
}
button[style*="background: transparent"] {
background: transparent !important;
box-shadow: none !important;
text-decoration: underline;
opacity: 0.7;
}
button[style*="background: transparent"]:hover {
opacity: 1;
}
.flex-row {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: nowrap;
}
.flex-between {
display: flex;
justify-content: space-between;
align-items: center;
}
/* ── History ── */
.history-list {
max-height: 250px;
max-height: 240px;
overflow-y: auto;
margin-top: 10px;
padding-right: 5px;
margin-top: 4px;
}
.history-item {
background: rgba(255, 255, 255, 0.03);
padding: 14px;
border-radius: 12px;
margin-bottom: 10px;
border: 1px solid rgba(255, 255, 255, 0.05);
transition: all 0.2s ease;
background: var(--surface-2);
border: 1px solid var(--border);
padding: 11px 14px;
border-radius: var(--radius-sm);
margin-bottom: 6px;
transition: border-color 0.15s;
}
.history-item:hover {
background: rgba(255, 255, 255, 0.06);
transform: translateX(2px);
border-color: var(--border-hover);
transform: none;
}
.history-time {
color: var(--primary-color);
color: var(--text-2);
font-size: 11px;
font-weight: 700;
margin-bottom: 6px;
font-weight: 500;
margin-bottom: 4px;
display: block;
}
/* Scrollbar excellence */
::-webkit-scrollbar {
width: 6px;
/* ── Inline status badges (online/offline) ── */
#connectionStatus {
border-radius: var(--radius-sm) !important;
font-size: 13px !important;
font-weight: 500;
}
::-webkit-scrollbar-track {
background: transparent;
/* Installed whisper card */
#whisperInstalled > div {
border-radius: var(--radius-sm) !important;
}
/* Install button override */
#installBtn {
background: var(--accent) !important;
border-radius: var(--radius-sm) !important;
font-size: 14px !important;
padding: 11px 20px !important;
width: 100%;
}
#installBtn:hover {
background: var(--accent-hover) !important;
}
/* ── Offline Whisper section separator ── */
#offlineWhisperSection {
border-top: 1px solid var(--border) !important;
margin-top: 20px !important;
padding-top: 20px !important;
}
/* ── Section group header divider ── */
.section-group-label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.07em;
color: var(--text-3);
margin: 22px 0 8px;
}
/* ── Scrollbar ── */
::-webkit-scrollbar { width: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
background: var(--border-color);
background: var(--surface-2);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-muted);
}
::-webkit-scrollbar-thumb:hover { background: var(--border-hover); }
+1 -82
View File
@@ -246,85 +246,4 @@ export function OnFileDropOff() :void
export function CanResolveFilePaths(): boolean;
// Resolves file paths for an array of files
export function ResolveFilePaths(files: File[]): void
// Notification types
export interface NotificationOptions {
id: string;
title: string;
subtitle?: string; // macOS and Linux only
body?: string;
categoryId?: string;
data?: { [key: string]: any };
}
export interface NotificationAction {
id?: string;
title?: string;
destructive?: boolean; // macOS-specific
}
export interface NotificationCategory {
id?: string;
actions?: NotificationAction[];
hasReplyField?: boolean;
replyPlaceholder?: string;
replyButtonTitle?: string;
}
// [InitializeNotifications](https://wails.io/docs/reference/runtime/notification#initializenotifications)
// Initializes the notification service for the application.
// This must be called before sending any notifications.
export function InitializeNotifications(): Promise<void>;
// [CleanupNotifications](https://wails.io/docs/reference/runtime/notification#cleanupnotifications)
// Cleans up notification resources and releases any held connections.
export function CleanupNotifications(): Promise<void>;
// [IsNotificationAvailable](https://wails.io/docs/reference/runtime/notification#isnotificationavailable)
// Checks if notifications are available on the current platform.
export function IsNotificationAvailable(): Promise<boolean>;
// [RequestNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#requestnotificationauthorization)
// Requests notification authorization from the user (macOS only).
export function RequestNotificationAuthorization(): Promise<boolean>;
// [CheckNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#checknotificationauthorization)
// Checks the current notification authorization status (macOS only).
export function CheckNotificationAuthorization(): Promise<boolean>;
// [SendNotification](https://wails.io/docs/reference/runtime/notification#sendnotification)
// Sends a basic notification with the given options.
export function SendNotification(options: NotificationOptions): Promise<void>;
// [SendNotificationWithActions](https://wails.io/docs/reference/runtime/notification#sendnotificationwithactions)
// Sends a notification with action buttons. Requires a registered category.
export function SendNotificationWithActions(options: NotificationOptions): Promise<void>;
// [RegisterNotificationCategory](https://wails.io/docs/reference/runtime/notification#registernotificationcategory)
// Registers a notification category that can be used with SendNotificationWithActions.
export function RegisterNotificationCategory(category: NotificationCategory): Promise<void>;
// [RemoveNotificationCategory](https://wails.io/docs/reference/runtime/notification#removenotificationcategory)
// Removes a previously registered notification category.
export function RemoveNotificationCategory(categoryId: string): Promise<void>;
// [RemoveAllPendingNotifications](https://wails.io/docs/reference/runtime/notification#removeallpendingnotifications)
// Removes all pending notifications from the notification center.
export function RemoveAllPendingNotifications(): Promise<void>;
// [RemovePendingNotification](https://wails.io/docs/reference/runtime/notification#removependingnotification)
// Removes a specific pending notification by its identifier.
export function RemovePendingNotification(identifier: string): Promise<void>;
// [RemoveAllDeliveredNotifications](https://wails.io/docs/reference/runtime/notification#removealldeliverednotifications)
// Removes all delivered notifications from the notification center.
export function RemoveAllDeliveredNotifications(): Promise<void>;
// [RemoveDeliveredNotification](https://wails.io/docs/reference/runtime/notification#removedeliverednotification)
// Removes a specific delivered notification by its identifier.
export function RemoveDeliveredNotification(identifier: string): Promise<void>;
// [RemoveNotification](https://wails.io/docs/reference/runtime/notification#removenotification)
// Removes a notification by its identifier (cross-platform convenience function).
export function RemoveNotification(identifier: string): Promise<void>;
export function ResolveFilePaths(files: File[]): void
-56
View File
@@ -239,60 +239,4 @@ export function CanResolveFilePaths() {
export function ResolveFilePaths(files) {
return window.runtime.ResolveFilePaths(files);
}
export function InitializeNotifications() {
return window.runtime.InitializeNotifications();
}
export function CleanupNotifications() {
return window.runtime.CleanupNotifications();
}
export function IsNotificationAvailable() {
return window.runtime.IsNotificationAvailable();
}
export function RequestNotificationAuthorization() {
return window.runtime.RequestNotificationAuthorization();
}
export function CheckNotificationAuthorization() {
return window.runtime.CheckNotificationAuthorization();
}
export function SendNotification(options) {
return window.runtime.SendNotification(options);
}
export function SendNotificationWithActions(options) {
return window.runtime.SendNotificationWithActions(options);
}
export function RegisterNotificationCategory(category) {
return window.runtime.RegisterNotificationCategory(category);
}
export function RemoveNotificationCategory(categoryId) {
return window.runtime.RemoveNotificationCategory(categoryId);
}
export function RemoveAllPendingNotifications() {
return window.runtime.RemoveAllPendingNotifications();
}
export function RemovePendingNotification(identifier) {
return window.runtime.RemovePendingNotification(identifier);
}
export function RemoveAllDeliveredNotifications() {
return window.runtime.RemoveAllDeliveredNotifications();
}
export function RemoveDeliveredNotification(identifier) {
return window.runtime.RemoveDeliveredNotification(identifier);
}
export function RemoveNotification(identifier) {
return window.runtime.RemoveNotification(identifier);
}