native first
This commit is contained in:
@@ -612,7 +612,7 @@ export class CheatingDaddyApp extends LitElement {
|
||||
|
||||
async handleStart() {
|
||||
const prefs = await cheatingDaddy.storage.getPreferences();
|
||||
const providerMode = prefs.providerMode || "byok";
|
||||
const providerMode = prefs.providerMode || "local";
|
||||
|
||||
if (providerMode === "local") {
|
||||
const success = await cheatingDaddy.initializeLocal(this.selectedProfile);
|
||||
|
||||
@@ -1,143 +1,160 @@
|
||||
import { html, css, LitElement } from '../../assets/lit-core-2.7.4.min.js';
|
||||
import { unifiedPageStyles } from './sharedPageStyles.js';
|
||||
import { html, css, LitElement } from "../../assets/lit-core-2.7.4.min.js";
|
||||
import { unifiedPageStyles } from "./sharedPageStyles.js";
|
||||
|
||||
export class AICustomizeView extends LitElement {
|
||||
static styles = [
|
||||
unifiedPageStyles,
|
||||
css`
|
||||
.unified-page {
|
||||
height: 100%;
|
||||
}
|
||||
.unified-wrap {
|
||||
height: 100%;
|
||||
}
|
||||
section.surface {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.form-grid {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.form-group.vertical {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
textarea.control {
|
||||
flex: 1;
|
||||
resize: none;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
`,
|
||||
static styles = [
|
||||
unifiedPageStyles,
|
||||
css`
|
||||
.unified-page {
|
||||
height: 100%;
|
||||
}
|
||||
.unified-wrap {
|
||||
height: 100%;
|
||||
}
|
||||
section.surface {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.form-grid {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.form-group.vertical {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
textarea.control {
|
||||
flex: 1;
|
||||
resize: none;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
static properties = {
|
||||
selectedProfile: { type: String },
|
||||
onProfileChange: { type: Function },
|
||||
_context: { state: true },
|
||||
_providerMode: { state: true },
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.selectedProfile = "interview";
|
||||
this.onProfileChange = () => {};
|
||||
this._context = "";
|
||||
this._providerMode = "local";
|
||||
this._loadFromStorage();
|
||||
}
|
||||
|
||||
async _loadFromStorage() {
|
||||
try {
|
||||
const prefs = await cheatingDaddy.storage.getPreferences();
|
||||
this._context = prefs.customPrompt || "";
|
||||
this._providerMode = prefs.providerMode || "local";
|
||||
this.requestUpdate();
|
||||
} catch (error) {
|
||||
console.error("Error loading AI customize storage:", error);
|
||||
}
|
||||
}
|
||||
|
||||
_handleProfileChange(e) {
|
||||
this.onProfileChange(e.target.value);
|
||||
}
|
||||
|
||||
async _handleProviderModeChange(e) {
|
||||
this._providerMode = e.target.value;
|
||||
await cheatingDaddy.storage.updatePreference(
|
||||
"providerMode",
|
||||
this._providerMode,
|
||||
);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
async _saveContext(val) {
|
||||
this._context = val;
|
||||
await cheatingDaddy.storage.updatePreference("customPrompt", val);
|
||||
}
|
||||
|
||||
_getProfileName(profile) {
|
||||
const names = {
|
||||
interview: "Job Interview",
|
||||
sales: "Sales Call",
|
||||
meeting: "Business Meeting",
|
||||
presentation: "Presentation",
|
||||
negotiation: "Negotiation",
|
||||
exam: "Exam Assistant",
|
||||
};
|
||||
return names[profile] || profile;
|
||||
}
|
||||
|
||||
render() {
|
||||
const profiles = [
|
||||
{ value: "interview", label: "Job Interview" },
|
||||
{ value: "sales", label: "Sales Call" },
|
||||
{ value: "meeting", label: "Business Meeting" },
|
||||
{ value: "presentation", label: "Presentation" },
|
||||
{ value: "negotiation", label: "Negotiation" },
|
||||
{ value: "exam", label: "Exam Assistant" },
|
||||
];
|
||||
|
||||
static properties = {
|
||||
selectedProfile: { type: String },
|
||||
onProfileChange: { type: Function },
|
||||
_context: { state: true },
|
||||
_providerMode: { state: true },
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.selectedProfile = 'interview';
|
||||
this.onProfileChange = () => {};
|
||||
this._context = '';
|
||||
this._providerMode = 'byok';
|
||||
this._loadFromStorage();
|
||||
}
|
||||
|
||||
async _loadFromStorage() {
|
||||
try {
|
||||
const prefs = await cheatingDaddy.storage.getPreferences();
|
||||
this._context = prefs.customPrompt || '';
|
||||
this._providerMode = prefs.providerMode || 'byok';
|
||||
this.requestUpdate();
|
||||
} catch (error) {
|
||||
console.error('Error loading AI customize storage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
_handleProfileChange(e) {
|
||||
this.onProfileChange(e.target.value);
|
||||
}
|
||||
|
||||
async _handleProviderModeChange(e) {
|
||||
this._providerMode = e.target.value;
|
||||
await cheatingDaddy.storage.updatePreference('providerMode', this._providerMode);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
async _saveContext(val) {
|
||||
this._context = val;
|
||||
await cheatingDaddy.storage.updatePreference('customPrompt', val);
|
||||
}
|
||||
|
||||
_getProfileName(profile) {
|
||||
const names = {
|
||||
interview: 'Job Interview',
|
||||
sales: 'Sales Call',
|
||||
meeting: 'Business Meeting',
|
||||
presentation: 'Presentation',
|
||||
negotiation: 'Negotiation',
|
||||
exam: 'Exam Assistant',
|
||||
};
|
||||
return names[profile] || profile;
|
||||
}
|
||||
|
||||
render() {
|
||||
const profiles = [
|
||||
{ value: 'interview', label: 'Job Interview' },
|
||||
{ value: 'sales', label: 'Sales Call' },
|
||||
{ value: 'meeting', label: 'Business Meeting' },
|
||||
{ value: 'presentation', label: 'Presentation' },
|
||||
{ value: 'negotiation', label: 'Negotiation' },
|
||||
{ value: 'exam', label: 'Exam Assistant' },
|
||||
];
|
||||
|
||||
return html`
|
||||
<div class="unified-page">
|
||||
<div class="unified-wrap">
|
||||
<div>
|
||||
<div class="page-title">AI Context</div>
|
||||
</div>
|
||||
|
||||
<section class="surface">
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Regime</label>
|
||||
<select class="control" .value=${this._providerMode} @change=${this._handleProviderModeChange}>
|
||||
<option value="byok">BYOK (API Keys)</option>
|
||||
<option value="local">Local AI (Ollama)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Profile</label>
|
||||
<select class="control" .value=${this.selectedProfile} @change=${this._handleProfileChange}>
|
||||
${profiles.map(profile => html`<option value=${profile.value}>${profile.label}</option>`)}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group vertical">
|
||||
<label class="form-label">Custom Instructions</label>
|
||||
<textarea
|
||||
class="control"
|
||||
placeholder="Resume details, role requirements, constraints..."
|
||||
.value=${this._context}
|
||||
@input=${e => this._saveContext(e.target.value)}
|
||||
></textarea>
|
||||
<div class="form-help">Sent as context at session start. Keep it short.</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
return html`
|
||||
<div class="unified-page">
|
||||
<div class="unified-wrap">
|
||||
<div>
|
||||
<div class="page-title">AI Context</div>
|
||||
</div>
|
||||
|
||||
<section class="surface">
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Regime</label>
|
||||
<select
|
||||
class="control"
|
||||
.value=${this._providerMode}
|
||||
@change=${this._handleProviderModeChange}
|
||||
>
|
||||
<option value="byok">BYOK (API Keys)</option>
|
||||
<option value="local">Local AI (LM Studio)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Profile</label>
|
||||
<select
|
||||
class="control"
|
||||
.value=${this.selectedProfile}
|
||||
@change=${this._handleProfileChange}
|
||||
>
|
||||
${profiles.map(
|
||||
(profile) =>
|
||||
html`<option value=${profile.value}>
|
||||
${profile.label}
|
||||
</option>`,
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group vertical">
|
||||
<label class="form-label">Custom Instructions</label>
|
||||
<textarea
|
||||
class="control"
|
||||
placeholder="Resume details, role requirements, constraints..."
|
||||
.value=${this._context}
|
||||
@input=${(e) => this._saveContext(e.target.value)}
|
||||
></textarea>
|
||||
<div class="form-help">
|
||||
Sent as context at session start. Keep it short.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('ai-customize-view', AICustomizeView);
|
||||
customElements.define("ai-customize-view", AICustomizeView);
|
||||
|
||||
@@ -208,7 +208,7 @@ export class CustomizeView extends LitElement {
|
||||
this.onImageQualityChange = () => {};
|
||||
this.onLayoutModeChange = () => {};
|
||||
this.googleSearchEnabled = true;
|
||||
this.providerMode = "byok";
|
||||
this.providerMode = "local";
|
||||
this.isClearing = false;
|
||||
this.isRestoring = false;
|
||||
this.clearStatusMessage = "";
|
||||
@@ -232,7 +232,7 @@ export class CustomizeView extends LitElement {
|
||||
cheatingDaddy.storage.getKeybinds(),
|
||||
]);
|
||||
this.googleSearchEnabled = prefs.googleSearchEnabled ?? true;
|
||||
this.providerMode = prefs.providerMode || "byok";
|
||||
this.providerMode = prefs.providerMode || "local";
|
||||
this.backgroundTransparency = prefs.backgroundTransparency ?? 0.8;
|
||||
this.fontSize = prefs.fontSize ?? 20;
|
||||
this.audioMode = prefs.audioMode ?? "speaker_only";
|
||||
@@ -664,7 +664,7 @@ export class CustomizeView extends LitElement {
|
||||
@change=${this.handleProviderModeChange}
|
||||
>
|
||||
<option value="byok">BYOK (API Keys)</option>
|
||||
<option value="local">Local AI (Ollama)</option>
|
||||
<option value="local">Local AI (LM Studio)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+165
-138
@@ -501,6 +501,11 @@ export class MainView extends LitElement {
|
||||
_whisperModel: { state: true },
|
||||
_customWhisperModel: { state: true },
|
||||
_showLocalHelp: { state: true },
|
||||
_localLlmBaseUrl: { state: true },
|
||||
_localLlmModel: { state: true },
|
||||
_localLlmApiKey: { state: true },
|
||||
_localSttUrl: { state: true },
|
||||
_localSttLanguage: { state: true },
|
||||
};
|
||||
|
||||
constructor() {
|
||||
@@ -513,7 +518,7 @@ export class MainView extends LitElement {
|
||||
this.whisperDownloading = false;
|
||||
this.whisperProgress = null;
|
||||
|
||||
this._mode = "byok";
|
||||
this._mode = "local";
|
||||
this._token = "";
|
||||
this._geminiKey = "";
|
||||
this._groqKey = "";
|
||||
@@ -528,6 +533,11 @@ export class MainView extends LitElement {
|
||||
this._tokenError = false;
|
||||
this._keyError = false;
|
||||
this._showLocalHelp = false;
|
||||
this._localLlmBaseUrl = "http://127.0.0.1:1234/v1";
|
||||
this._localLlmModel = "";
|
||||
this._localLlmApiKey = "";
|
||||
this._localSttUrl = "ws://127.0.0.1:8765/v1/asr/stream";
|
||||
this._localSttLanguage = "en-US";
|
||||
this._ollamaHost = "http://127.0.0.1:11434";
|
||||
this._ollamaModel = "llama3.1";
|
||||
this._whisperModel = "Xenova/whisper-small";
|
||||
@@ -549,7 +559,7 @@ export class MainView extends LitElement {
|
||||
cheatingDaddy.storage.getCredentials().catch(() => ({})),
|
||||
]);
|
||||
|
||||
this._mode = prefs.providerMode || "byok";
|
||||
this._mode = prefs.providerMode || "local";
|
||||
|
||||
// Load keys
|
||||
this._token = "";
|
||||
@@ -571,6 +581,13 @@ export class MainView extends LitElement {
|
||||
this._responseProvider = prefs.responseProvider || "gemini";
|
||||
|
||||
// Load local AI settings
|
||||
this._localLlmBaseUrl =
|
||||
prefs.localLlmBaseUrl || "http://127.0.0.1:1234/v1";
|
||||
this._localLlmModel = prefs.localLlmModel || "";
|
||||
this._localLlmApiKey = prefs.localLlmApiKey || "";
|
||||
this._localSttUrl =
|
||||
prefs.localSttUrl || "ws://127.0.0.1:8765/v1/asr/stream";
|
||||
this._localSttLanguage = prefs.localSttLanguage || "en-US";
|
||||
this._ollamaHost = prefs.ollamaHost || "http://127.0.0.1:11434";
|
||||
this._ollamaModel = prefs.ollamaModel || "llama3.1";
|
||||
this._whisperModel = prefs.whisperModel || "Xenova/whisper-small";
|
||||
@@ -917,6 +934,51 @@ export class MainView extends LitElement {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
async _saveLocalLlmBaseUrl(val) {
|
||||
this._localLlmBaseUrl = val;
|
||||
await cheatingDaddy.storage.updatePreference("localLlmBaseUrl", val);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
async _saveLocalLlmModel(val) {
|
||||
this._localLlmModel = val;
|
||||
await cheatingDaddy.storage.updatePreference("localLlmModel", val);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
async _saveLocalLlmApiKey(val) {
|
||||
this._localLlmApiKey = val;
|
||||
await cheatingDaddy.storage.updatePreference("localLlmApiKey", val);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
async _saveLocalSttUrl(val) {
|
||||
this._localSttUrl = val;
|
||||
await cheatingDaddy.storage.updatePreference("localSttUrl", val);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
async _saveLocalSttLanguage(val) {
|
||||
this._localSttLanguage = val;
|
||||
await cheatingDaddy.storage.updatePreference("localSttLanguage", val);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
_isLoopbackUrl(value) {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
const hostname = url.hostname.toLowerCase();
|
||||
return (
|
||||
hostname === "localhost" ||
|
||||
hostname === "127.0.0.1" ||
|
||||
hostname === "::1" ||
|
||||
hostname === "[::1]"
|
||||
);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async _saveOllamaHost(val) {
|
||||
this._ollamaHost = val;
|
||||
await cheatingDaddy.storage.updatePreference("ollamaHost", val);
|
||||
@@ -1002,8 +1064,13 @@ export class MainView extends LitElement {
|
||||
return;
|
||||
}
|
||||
} else if (this._mode === "local") {
|
||||
// Local mode doesn't need API keys, just Ollama host
|
||||
if (!this._ollamaHost.trim()) {
|
||||
if (
|
||||
!this._localLlmBaseUrl.trim() ||
|
||||
!this._localLlmModel.trim() ||
|
||||
!this._localSttUrl.trim()
|
||||
) {
|
||||
this._keyError = true;
|
||||
this.requestUpdate();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1258,105 +1325,88 @@ export class MainView extends LitElement {
|
||||
// ── Local AI mode ──
|
||||
|
||||
_renderLocalMode() {
|
||||
const llmIsLocal = this._isLoopbackUrl(this._localLlmBaseUrl);
|
||||
const sttIsLocal = this._isLoopbackUrl(this._localSttUrl);
|
||||
|
||||
return html`
|
||||
<div class="form-group">
|
||||
<label class="form-label">Ollama Host</label>
|
||||
<label class="form-label">LM Studio Base URL</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="http://127.0.0.1:11434"
|
||||
.value=${this._ollamaHost}
|
||||
@input=${(e) => this._saveOllamaHost(e.target.value)}
|
||||
/>
|
||||
<div class="form-hint">Ollama must be running locally</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Ollama Model</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="llama3.1"
|
||||
.value=${this._ollamaModel}
|
||||
@input=${(e) => this._saveOllamaModel(e.target.value)}
|
||||
placeholder="http://127.0.0.1:1234/v1"
|
||||
.value=${this._localLlmBaseUrl}
|
||||
@input=${(e) => this._saveLocalLlmBaseUrl(e.target.value)}
|
||||
class=${this._keyError && !this._localLlmBaseUrl.trim()
|
||||
? "error"
|
||||
: ""}
|
||||
/>
|
||||
<div class="form-hint">
|
||||
Run
|
||||
<code
|
||||
style="font-family: var(--font-mono); font-size: 11px; background: var(--bg-elevated); padding: 1px 4px; border-radius: 3px;"
|
||||
>ollama pull ${this._ollamaModel}</code
|
||||
>
|
||||
first
|
||||
LM Studio local server endpoint for OpenAI-compatible chat
|
||||
${!llmIsLocal
|
||||
? html`<span style="color: var(--warning, #d97706);">
|
||||
· not a localhost URL
|
||||
</span>`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="whisper-label-row">
|
||||
<label class="form-label">Whisper Model</label>
|
||||
${this.whisperDownloading
|
||||
? html`<div class="whisper-spinner"></div>`
|
||||
<label class="form-label">LM Studio Model ID</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="gemma-4 or the exact loaded model id"
|
||||
.value=${this._localLlmModel}
|
||||
@input=${(e) => this._saveLocalLlmModel(e.target.value)}
|
||||
class=${this._keyError && !this._localLlmModel.trim() ? "error" : ""}
|
||||
/>
|
||||
<div class="form-hint">
|
||||
Manual only: use the model identifier shown in LM Studio
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">LM Studio API Key</label>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Optional for most local LM Studio setups"
|
||||
.value=${this._localLlmApiKey}
|
||||
@input=${(e) => this._saveLocalLlmApiKey(e.target.value)}
|
||||
/>
|
||||
<div class="form-hint">
|
||||
Leave blank unless your local server requires a token
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Nemotron ASR Sidecar URL</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="ws://127.0.0.1:8765/v1/asr/stream"
|
||||
.value=${this._localSttUrl}
|
||||
@input=${(e) => this._saveLocalSttUrl(e.target.value)}
|
||||
class=${this._keyError && !this._localSttUrl.trim() ? "error" : ""}
|
||||
/>
|
||||
<div class="form-hint">
|
||||
External streaming STT service that accepts 16 kHz mono PCM
|
||||
${!sttIsLocal
|
||||
? html`<span style="color: var(--warning, #d97706);">
|
||||
· not a localhost URL
|
||||
</span>`
|
||||
: ""}
|
||||
</div>
|
||||
<select
|
||||
.value=${this._whisperModel}
|
||||
@change=${(e) => this._saveWhisperModel(e.target.value)}
|
||||
>
|
||||
<option
|
||||
value="Xenova/whisper-tiny"
|
||||
?selected=${this._whisperModel === "Xenova/whisper-tiny"}
|
||||
>
|
||||
Tiny (fastest, least accurate)
|
||||
</option>
|
||||
<option
|
||||
value="Xenova/whisper-base"
|
||||
?selected=${this._whisperModel === "Xenova/whisper-base"}
|
||||
>
|
||||
Base
|
||||
</option>
|
||||
<option
|
||||
value="Xenova/whisper-small"
|
||||
?selected=${this._whisperModel === "Xenova/whisper-small"}
|
||||
>
|
||||
Small (recommended)
|
||||
</option>
|
||||
<option
|
||||
value="Xenova/whisper-medium"
|
||||
?selected=${this._whisperModel === "Xenova/whisper-medium"}
|
||||
>
|
||||
Medium (most accurate, slowest)
|
||||
</option>
|
||||
<option
|
||||
value="__custom__"
|
||||
?selected=${this._whisperModel === "__custom__"}
|
||||
>
|
||||
Custom HuggingFace model...
|
||||
</option>
|
||||
</select>
|
||||
${this._whisperModel === "__custom__"
|
||||
? html`
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g. onnx-community/whisper-large-v3-turbo"
|
||||
.value=${this._customWhisperModel}
|
||||
@change=${(e) => this._saveCustomWhisperModel(e.target.value)}
|
||||
@input=${(e) => {
|
||||
this._customWhisperModel = e.target.value;
|
||||
}}
|
||||
style="margin-top: 6px;"
|
||||
/>
|
||||
<div class="form-hint">
|
||||
Enter a HuggingFace model ID compatible with
|
||||
@huggingface/transformers speech-to-text pipeline
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="form-hint">
|
||||
${this.whisperDownloading
|
||||
? "Downloading model..."
|
||||
: "Downloaded automatically on first use"}
|
||||
</div>
|
||||
`}
|
||||
${this.whisperDownloading && this.whisperProgress
|
||||
? this._renderWhisperProgress()
|
||||
: ""}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">STT Language</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="en-US"
|
||||
.value=${this._localSttLanguage}
|
||||
@input=${(e) => this._saveLocalSttLanguage(e.target.value)}
|
||||
/>
|
||||
<div class="form-hint">
|
||||
First local sidecar target is English streaming ASR
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${this._renderStartButton()}
|
||||
@@ -1458,74 +1508,51 @@ export class MainView extends LitElement {
|
||||
return html`
|
||||
<div class="help-content">
|
||||
<div class="help-section">
|
||||
<div class="help-section-title">What is Ollama?</div>
|
||||
<div class="help-section-title">LM Studio</div>
|
||||
<div class="help-section-text">
|
||||
Ollama lets you run large language models locally on your machine.
|
||||
Everything stays on your computer — no data leaves your device.
|
||||
LM Studio runs the answer model locally and exposes an
|
||||
OpenAI-compatible server for streaming responses.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-section">
|
||||
<div class="help-section-title">Install Ollama</div>
|
||||
<div class="help-section-title">Start LM Studio server</div>
|
||||
<div class="help-section-text">
|
||||
Download from
|
||||
<span
|
||||
class="help-link"
|
||||
@click=${() => this.onExternalLink("https://ollama.com/download")}
|
||||
>ollama.com/download</span
|
||||
>
|
||||
and install it.
|
||||
Download LM Studio, load a model, then start the local server from
|
||||
the Developer tab. The default endpoint is:
|
||||
</div>
|
||||
<code class="help-code">http://127.0.0.1:1234/v1</code>
|
||||
</div>
|
||||
|
||||
<div class="help-section">
|
||||
<div class="help-section-title">Model ID</div>
|
||||
<div class="help-section-text">
|
||||
Enter the exact model identifier shown by LM Studio. Gemma 4 is the
|
||||
reference target, but any loaded compatible model can be used.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-section">
|
||||
<div class="help-section-title">Ollama must be running</div>
|
||||
<div class="help-section-title">Nemotron ASR sidecar</div>
|
||||
<div class="help-section-text">
|
||||
Ollama needs to be running before you start a session. If it's not
|
||||
running, open your terminal and type:
|
||||
Speech-to-text runs as a separate local streaming service. The app
|
||||
connects to:
|
||||
</div>
|
||||
<code class="help-code">ollama serve</code>
|
||||
<code class="help-code">ws://127.0.0.1:8765/v1/asr/stream</code>
|
||||
</div>
|
||||
|
||||
<div class="help-section">
|
||||
<div class="help-section-title">Pull a model</div>
|
||||
<div class="help-section-title">Screenshots</div>
|
||||
<div class="help-section-text">
|
||||
Download a model before first use:
|
||||
</div>
|
||||
<code class="help-code">ollama pull gemma3:4b</code>
|
||||
</div>
|
||||
|
||||
<div class="help-section">
|
||||
<div class="help-section-title">Recommended models</div>
|
||||
<div class="help-models">
|
||||
<div class="help-model">
|
||||
<span class="help-model-name">gemma3:4b</span
|
||||
><span>4B — fast, multimodal (images + text)</span>
|
||||
</div>
|
||||
<div class="help-model">
|
||||
<span class="help-model-name">mistral-small</span
|
||||
><span>8B — solid all-rounder, text only</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="help-section-text">
|
||||
gemma3:4b and above supports images — screenshots will work with
|
||||
these models.
|
||||
Manual screenshots are sent to the same local LM Studio model. Use a
|
||||
vision-capable model for screen analysis.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-section">
|
||||
<div class="help-warn">
|
||||
Avoid "thinking" models (e.g. deepseek-r1, qwq). Local inference is
|
||||
already slower — a thinking model adds extra delay before
|
||||
responding.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-section">
|
||||
<div class="help-section-title">Whisper</div>
|
||||
<div class="help-section-text">
|
||||
The Whisper speech-to-text model is downloaded automatically the
|
||||
first time you start a session. This is a one-time download.
|
||||
Non-local endpoints are allowed, but they may send audio transcripts
|
||||
or screenshots outside this machine.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user