Add Mistral AI as inference provider with Voxtral Small transcription model

- Add support for multiple API providers (Groq and Mistral)
- Add collapsible API keys section in settings for both providers
- Label all models with their provider (Groq/Mistral)
- Add Voxtral Small (Mistral) transcription model
- Add Mistral Small and Mistral Medium AI refinement models
- Use user-friendly model names while maintaining correct API model values
- Update transcriber to route requests to correct provider based on model
- Update config to store separate API keys for each provider
- Add Mistral brand colors to CSS
- Style API keys dropdown section

Co-authored-by: jahruz67 <jahruz67@users.noreply.github.com>
This commit is contained in:
Vibe Nuage Agent
2026-07-29 22:06:10 +00:00
co-authored by jahruz67
parent f658ab0de3
commit 9c9ad5ef87
5 changed files with 347 additions and 95 deletions
+104 -39
View File
@@ -24,19 +24,48 @@
<div class="section-group-label">Authentication</div>
<!-- API Key -->
<!-- API Keys Dropdown Section -->
<div class="section">
<label>Groq API Key</label>
<div class="form-control">
<div class="flex-row">
<div class="input-wrapper">
<input type="password" id="apiKey" placeholder="gsk_...">
<button class="eye-btn" onclick="toggleApiKey()" id="eyeBtn">👁️</button>
<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>
<button onclick="saveApiKey()">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>
</details>
</div>
<!-- Global Hotkey (hidden on Linux because Linux uses the system shortcut command below) -->
@@ -74,8 +103,8 @@
<button onclick="copyLinuxPressCommand()">Copy</button>
</div>
</div>
<p class="hint" style="margin-top: 10px;">GNOME: Settings Keyboard Custom Shortcuts Add a shortcut with the copied command.</p>
<p class="hint">KDE: System Settings Shortcuts Command/URL Add a shortcut with the copied command.</p>
<p class="hint" style="margin-top: 10px;">GNOME: Settings  Keyboard  Custom Shortcuts  Add a shortcut with the copied command.</p>
<p class="hint">KDE: System Settings  Shortcuts  Command/URL  Add a shortcut with the copied command.</p>
</details>
</div>
@@ -93,8 +122,9 @@
<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 (Recommended)</option>
<option value="whisper-large-v3">whisper-large-v3</option>
<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>
@@ -116,12 +146,14 @@
<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">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>
<option value="openai/gpt-oss-20b">openai/gpt-oss-20b (low reasoning)</option>
<option value="llama-3.3-70b-versatile">llama-3.3-70b-versatile</option>
<option value="llama-3.1-8b-instant">llama-3.1-8b-instant (faster)</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>
@@ -189,7 +221,7 @@
<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="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>
@@ -208,18 +240,56 @@
</div>
<script type="module">
let apiKeyVisible = false;
let groqApiKeyVisible = false;
let mistralApiKeyVisible = false;
let linuxShortcutMode = false;
// Toggle API key visibility
window.toggleApiKey = function () {
const input = document.getElementById('apiKey');
const btn = document.getElementById('eyeBtn');
apiKeyVisible = !apiKeyVisible;
input.type = apiKeyVisible ? 'text' : 'password';
btn.textContent = apiKeyVisible ? '🙈' : '👁️';
// 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) {
@@ -397,7 +467,8 @@
try {
const settings = await window.go.main.App.GetSettings();
document.getElementById('apiKey').value = settings.api_key || '';
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';
@@ -434,6 +505,7 @@
document.getElementById('appVersion').textContent = ver === 'dev' ? 'dev' : ('v' + ver);
renderHistoryList(settings.history || []);
updateApiKeyStatus();
} catch (err) {
console.error("Failed to load settings:", err);
}
@@ -457,13 +529,6 @@
}
}
// Save functions
window.saveApiKey = async function () {
const key = document.getElementById('apiKey').value.trim();
await window.go.main.App.SaveSettings({ api_key: key });
showSaveStatus();
};
function showSaveStatus(message = 'Saved') {
const el = document.getElementById('saveStatus');
el.textContent = message;
@@ -540,7 +605,7 @@
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.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)';
@@ -580,7 +645,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 \u2014 ' + info.model + ' (offline)';
localOption.style.fontWeight = 'bold';
whisperSelect.insertBefore(localOption, whisperSelect.firstChild);
} else {
+98 -17
View File
@@ -18,6 +18,8 @@
--red-dim: rgba(224, 82, 82, 0.12);
--radius: 10px;
--radius-sm: 7px;
--mistral: #8a5cf0;
--mistral-dim: rgba(138, 92, 240, 0.15);
color-scheme: dark;
}
@@ -35,14 +37,14 @@ body {
-webkit-font-smoothing: antialiased;
}
/* ── Layout ── */
/* Layout */
.container {
max-width: 680px;
margin: 0 auto;
padding: 32px 28px 60px;
}
/* ── Header ── */
/* Header */
h1 {
font-size: 20px;
font-weight: 600;
@@ -64,7 +66,7 @@ h1 {
flex-wrap: nowrap;
}
/* ── Section cards ── */
/* Section cards */
.section {
background: var(--surface);
border: 1px solid var(--border);
@@ -78,7 +80,7 @@ h1 {
border-color: var(--border-hover);
}
/* ── Labels ── */
/* Labels */
label {
display: block;
font-size: 13px;
@@ -96,7 +98,7 @@ label {
line-height: 1.5;
}
/* ── Inputs ── */
/* Inputs */
input[type="text"],
input[type="password"],
select,
@@ -157,12 +159,12 @@ input[type="checkbox"] {
flex-shrink: 0;
}
/* ── Form control width ── */
/* Form control width */
.form-control {
width: 100%;
}
/* ── Input wrapper (for eye-btn) ── */
/* Input wrapper (for eye-btn) */
.input-wrapper {
position: relative;
flex: 1;
@@ -197,7 +199,7 @@ input[type="checkbox"] {
box-shadow: none !important;
}
/* ── Buttons ── */
/* Buttons */
button {
background: var(--accent);
color: #fff;
@@ -254,7 +256,7 @@ button:active {
opacity: 1 !important;
}
/* ── Save status toast ── */
/* Save status toast */
.save-status {
position: fixed;
bottom: 24px;
@@ -279,7 +281,7 @@ button:active {
opacity: 1;
}
/* ── History ── */
/* History */
.history-list {
max-height: 240px;
overflow-y: auto;
@@ -308,7 +310,7 @@ button:active {
display: block;
}
/* ── Inline status badges (online/offline) ── */
/* Inline status badges (online/offline) */
#connectionStatus {
border-radius: var(--radius-sm) !important;
font-size: 13px !important;
@@ -333,16 +335,95 @@ button:active {
background: var(--accent-hover) !important;
}
/* ── Offline Whisper section separator ── */
/* Offline Whisper section separator */
#offlineWhisperSection {
border-top: 1px solid var(--border) !important;
margin-top: 20px !important;
padding-top: 20px !important;
}
/* API Keys Dropdown Section */
.api-keys-details {
color: var(--text);
}
.api-keys-summary {
cursor: pointer;
font-weight: 500;
font-size: 13px;
color: var(--text);
padding: 2px 0;
user-select: none;
list-style: none;
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
/* ── Collapsible fallback details ── */
.api-keys-summary::-webkit-details-marker {
display: none;
}
.api-keys-summary::after {
content: '\u25b6';
font-size: 10px;
color: var(--text-3);
transition: transform 0.15s ease;
display: inline-block;
margin-left: 8px;
}
.api-keys-details[open] > .api-keys-summary::after {
transform: rotate(90deg);
}
.api-keys-summary:hover {
color: var(--text);
}
.api-keys-content {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--border);
}
.api-key-section {
margin-bottom: 12px;
}
.api-key-section:last-child {
margin-bottom: 0;
}
/* Provider badges for model options */
select option[data-provider]::before {
content: attr(data-provider);
display: inline-block;
padding: 2px 6px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
margin-right: 8px;
opacity: 0.8;
}
select option[data-provider="groq"]::before {
background: var(--accent-dim);
color: var(--accent);
}
select option[data-provider="mistral"]::before {
background: var(--mistral-dim);
color: var(--mistral);
}
select option[data-provider="local"]::before {
background: var(--green-dim);
color: var(--green);
}
/* Collapsible fallback details */
.fallback-details {
color: var(--text);
}
@@ -365,7 +446,7 @@ button:active {
}
.fallback-summary::before {
content: '';
content: '\u25b6';
font-size: 10px;
color: var(--text-3);
transition: transform 0.15s ease;
@@ -380,7 +461,7 @@ button:active {
color: var(--text);
}
/* ── Section group header divider ── */
/* Section group header divider */
.section-group-label {
font-size: 11px;
font-weight: 600;
@@ -390,11 +471,11 @@ button:active {
margin: 22px 0 8px;
}
/* ── Scrollbar ── */
/* Scrollbar */
::-webkit-scrollbar { width: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
background: var(--surface-2);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover { background: var(--border-hover); }
::-webkit-scrollbar-thumb:hover { background: var(--border-hover); }