Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ A code this build knows but an older backend does not is resolved back to Englis

`configStore.getConfig()` resolves the language on the way *out*, not on the way in. The disk holds whatever some build wrote - a code a later release dropped, or one an older release never knew - and every consumer reads through `getConfig`, so that is the single place an unknown code can be stopped before it reaches the ASR URL and three request bodies. `test/language.test.mjs` pins it.

**The picker stays live mid-interview**, unlike Model, because an interview that switches language is the case it exists for and not one the candidate can prepare for by restarting. The two halves of the setting move at different speeds and `useInterviewLanguage` is where that is reconciled. Suggestions need nothing: every request reads the config store as it is built, so the next one already follows. The ASR carries its language as a *connection* parameter, so `liveTranscriptionService.setLanguage()` tears both sockets down and re-opens them - a second or two of gap, and whatever utterance was mid-flight is orphaned, which is why the button shows a spinner rather than pretending the change was instant and why the menu says so before the user commits.
**The picker stays live mid-interview**, because an interview that switches language is the case it exists for and not one the candidate can prepare for by restarting. The two halves of the setting move at different speeds and `useInterviewLanguage` is where that is reconciled. Suggestions need nothing: every request reads the config store as it is built, so the next one already follows. The ASR carries its language as a *connection* parameter, so `liveTranscriptionService.setLanguage()` tears both sockets down and re-opens them - a second or two of gap, and whatever utterance was mid-flight is orphaned, which is why the button shows a spinner rather than pretending the change was instant and why the menu says so before the user commits.

Three guards in `AudioWsStream` make that safe, and all three protect against the same failure - two sockets on one channel, one of them orphaned and still relaying audio into a dead session. `ws.onclose` ignores a close from a socket that is no longer `this.ws`, since that is the tail of a replacement rather than a disconnect; and the `switching` flag suppresses the ordinary backoff reconnect for the close `setLanguage` causes itself, which it then handles immediately instead of after `WS_RETRY_BASE_DELAY_MS`. `connectWebSocket` rebuilds the URL per attempt rather than capturing it, which is what lets a reconnect pick up the new language at all.

Expand Down
17 changes: 0 additions & 17 deletions src/main/api/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,10 @@ import {
GenerateActionSuggestionRequest,
GenerateLiveSuggestionRequest,
GenerateSummarizeRequest,
LLMConfigValidationResult,
LLMModelInfo,
LLMRequest,
} from '../types/llm.js';
import { ApiClient, ApiResponse } from './client.js';

export class LLMApi extends ApiClient {
/**
* Validate LLM Config
*/
async validate(request: LLMRequest): Promise<ApiResponse<LLMConfigValidationResult>> {
return this.post<LLMConfigValidationResult>('/api/llm/validate', request);
}

/**
* List Supported Models
*/
async listModels(): Promise<ApiResponse<LLMModelInfo[]>> {
return this.get<LLMModelInfo[]>('/api/llm/models');
}

/**
* Generate Live Suggestions
*/
Expand Down
2 changes: 0 additions & 2 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { registerAuthHandlers } from './ipc/auth.js';
import { registerAutoUpdaterHandlers } from './ipc/auto-updater.js';
import { registerConfigHandlers } from './ipc/config.js';
import { registerExternalHandlers } from './ipc/external.js';
import { registerLLMHandlers } from './ipc/llm.js';
import { registerPaymentHandlers } from './ipc/payment.js';
import { registerPermissionHandlers } from './ipc/permissions.js';
import { registerActionSuggestionHandlers } from './ipc/suggestion-action.js';
Expand Down Expand Up @@ -206,7 +205,6 @@ app.whenReady().then(async () => {
registerAuthHandlers();
registerAccountHandlers();
registerPaymentHandlers();
registerLLMHandlers();
registerPermissionHandlers();
registerTranscriptHandlers();
registerLiveSuggestionHandlers();
Expand Down
38 changes: 0 additions & 38 deletions src/main/ipc/llm.ts

This file was deleted.

6 changes: 0 additions & 6 deletions src/main/preload.cts
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,6 @@ const electronApi = {
getCredits: () => ipcRenderer.invoke('payment:get-credits'),
},

llm: {
listModels: () => ipcRenderer.invoke('llm:list-models'),
validate: (config: Record<string, unknown> | null) =>
ipcRenderer.invoke('llm:validate', config),
},

appState: {
get: () => ipcRenderer.invoke('app:get-state'),
update: (updates: Record<string, unknown>) => ipcRenderer.invoke('app:update-state', updates),
Expand Down
1 change: 0 additions & 1 deletion src/main/services/suggestion-action.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,6 @@ export class ActionSuggestionService {
const interviewConfig = appStateService.getState().interviewConfig;

const payload: GenerateActionSuggestionRequest = {
config: conf.llmConf,
profile_data: interviewConfig.profileData,
context: interviewConfig.context,
transcripts: transcripts.slice(-TRANSCRIPT_UPLOAD_LIMIT),
Expand Down
1 change: 0 additions & 1 deletion src/main/services/suggestion-live.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,6 @@ class LiveSuggestionService {
try {
const interviewConfig = appStateService.getState().interviewConfig;
const requestBody: GenerateLiveSuggestionRequest = {
config: conf.llmConf,
profile_data: interviewConfig.profileData,
context: interviewConfig.context,
transcripts: transcripts.slice(-TRANSCRIPT_UPLOAD_LIMIT),
Expand Down
1 change: 0 additions & 1 deletion src/main/services/tools.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ class ToolsService {
// Call the API to generate the summary text
const conf = configStore.getConfig();
const response = await this.llmApi.generateSummary({
config: conf.llmConf,
username,
transcripts,
// The exported report is written in the interview's language too. A Spanish interview
Expand Down
17 changes: 12 additions & 5 deletions src/main/store/config.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import ElectronStore from 'electron-store';

import { OPACITY_DEFAULT } from '../consts.js';
import { DEFAULT_LANGUAGE, Language, resolveLanguage } from '../types/language.js';
import { LLMConfig } from '../types/llm.js';

// Runtime configuration (matches Config type in frontend)
export interface RuntimeConfig {
Expand All @@ -19,8 +18,6 @@ export interface RuntimeConfig {
password: string;
audioInputDeviceName: string;

llmConf: LLMConfig | null;

// panel auto-scroll preferences
autoScrollLiveSuggestions: boolean;
autoScrollActionSuggestions: boolean;
Expand Down Expand Up @@ -54,8 +51,6 @@ const DEFAULT_RUNTIME_CONFIG: RuntimeConfig = {
password: '',
audioInputDeviceName: '',

llmConf: null,

// default autoscroll preferences are enabled
autoScrollLiveSuggestions: true,
autoScrollActionSuggestions: true,
Expand Down Expand Up @@ -275,6 +270,18 @@ export const configStore = new ConfigStore();
}
})(); // migration block

// One-time cleanup: `llmConf` backed the removed bring-your-own-API-key feature and could hold a
// real provider key in plaintext on disk. `RuntimeConfig` no longer declares it, but every read
// and write here spreads the raw stored object through - nothing strips a key TypeScript no
// longer knows about - so a leftover value would otherwise survive on an upgraded install forever.
(() => {
const raw = configStore.getStoredRuntime() as (StoredRuntime & { llmConf?: unknown }) | undefined;
if (raw && 'llmConf' in raw) {
delete raw.llmConf;
configStore.setStoredRuntime(raw);
}
})(); // llmConf scrub

// Read (but do not yet delete) any leftover local copy, so AccountService can migrate it
// onto the account. Deleting here unconditionally would destroy the only copy whenever the
// first launch after upgrade happens to be offline.
Expand Down
48 changes: 0 additions & 48 deletions src/main/types/llm.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,7 @@
import { Transcript } from './app-state.js';
import { Language } from './language.js';

export enum LLMProvider {
OPENAI = 'openai',
ANTHROPIC = 'anthropic',
GROQ = 'groq',
GOOGLE = 'google',
}

export enum LLMModality {
TEXT_INPUT = 'text_input',
IMAGE_INPUT = 'image_input',
TEXT_OUTPUT = 'text_output',
IMAGE_OUTPUT = 'image_output',
AUDIO_INPUT = 'audio_input',
AUDIO_OUTPUT = 'audio_output',
}

export interface LLMModelInfo {
id: string;
provider: LLMProvider;
name: string;
description: string;
modalities: LLMModality[];
vision_capable: boolean;
context_window: number;
max_output_tokens: number;
pricing_input: number;
pricing_output: number;
supports_streaming: boolean;
supports_function_calling: boolean;
supports_json_mode: boolean;
release_date: string | null;
}

export interface LLMConfig {
provider: LLMProvider;
apikey: string;
model: string;
}

export interface LLMConfigValidationResult {
provider_ok: boolean;
apikey_ok: boolean;
model_ok: boolean;
error: string;
}

export interface LLMRequest {
config: LLMConfig | null;

/**
* Interview language. Carried on the shared base so the three request kinds cannot drift, and
* defaulted server-side, so omitting it against an older deployment still means English.
Expand Down
12 changes: 5 additions & 7 deletions src/renderer/components/custom/control-panel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import PermissionGateDialog from '../permission-gate-dialog';
import ZoomControl from '../zoom-control';
import { AudioGroup } from './audio-group';
import { LanguageGroup } from './language-group';
import { LLMGroup } from './llm-group';
import { MainGroup } from './main-group';
import { ProfessionalModeGroup } from './professional-mode-group';
import { ToolsGroup } from './tools-group';
Expand Down Expand Up @@ -203,19 +202,18 @@ export default function ControlPanel() {

<div className="h-5 w-px bg-border" aria-hidden="true" />

{/* What the session runs on. Only the model locks while the assistant runs; audio and
language stay live, because both are things an interview can get wrong in progress
and neither can be fixed by restarting without losing the transcript. Language sits
here rather than with the presentation toggles because it is an input as much as an
output: it picks the speech model before it picks the answer's language. */}
{/* What the session runs on. Both stay live while the assistant runs, because audio and
language are things an interview can get wrong in progress and neither can be fixed
by restarting without losing the transcript. Language sits here rather than with the
presentation toggles because it is an input as much as an output: it picks the speech
model before it picks the answer's language. */}
<div className="flex items-center gap-1">
<AudioGroup
audioInputDevices={audioInputDevices}
audioInputDeviceNotFound={audioInputDeviceNotFound}
getDisabled={getDisabled}
/>
<LanguageGroup getDisabled={getDisabled} />
<LLMGroup getDisabled={getDisabled} />
</div>

{/* What the interview produces: how suggestions read, and what to do with the session */}
Expand Down
Loading