feat: add ModelCatalogEntry type, enhance EffectiveServerUrl with host/port details, and refine project context logging.
This commit is contained in:
@@ -109,21 +109,28 @@ pub fn get_effective_server_url(state: State<'_, Arc<AppState>>) -> EffectiveSer
|
||||
let prefs = state.preferences.read();
|
||||
let cfg = config();
|
||||
|
||||
// Check if preferences override the default
|
||||
let prefs_url = format!("{}:{}", prefs.server_host, prefs.server_port);
|
||||
let default_url = &cfg.server.default_address;
|
||||
|
||||
// If preferences explicitly customized, use them
|
||||
if prefs.server_address_customized && !prefs.server_host.is_empty() {
|
||||
return EffectiveServerUrl {
|
||||
url: prefs_url,
|
||||
url: format!("{}:{}", prefs.server_host, prefs.server_port),
|
||||
host: prefs.server_host.clone(),
|
||||
port: prefs.server_port.clone(),
|
||||
source: ServerAddressSource::Preferences,
|
||||
};
|
||||
}
|
||||
|
||||
// Otherwise, use config (which tracks env vs default)
|
||||
let default_url = cfg.server.default_address.clone();
|
||||
let (h, p) = if let Some(pos) = default_url.find(':') {
|
||||
(&default_url[..pos], &default_url[pos + 1..])
|
||||
} else {
|
||||
(&default_url[..], "")
|
||||
};
|
||||
|
||||
EffectiveServerUrl {
|
||||
url: default_url.clone(),
|
||||
host: h.to_string(),
|
||||
port: p.to_string(),
|
||||
source: cfg.server.address_source,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +94,10 @@ pub enum ServerAddressSource {
|
||||
pub struct EffectiveServerUrl {
|
||||
/// The server URL
|
||||
pub url: String,
|
||||
/// Parsed host part
|
||||
pub host: String,
|
||||
/// Parsed port part
|
||||
pub port: String,
|
||||
/// Source of the URL
|
||||
pub source: ServerAddressSource,
|
||||
}
|
||||
|
||||
@@ -19,14 +19,10 @@ describe('TauriTranscriptionStream', () => {
|
||||
let stream: TauriTranscriptionStream;
|
||||
|
||||
beforeEach(() => {
|
||||
const invokeMock = vi
|
||||
.fn<Parameters<TauriInvoke>, ReturnType<TauriInvoke>>()
|
||||
.mockResolvedValue(undefined);
|
||||
const listenMock = vi
|
||||
.fn<Parameters<TauriListen>, ReturnType<TauriListen>>()
|
||||
.mockResolvedValue(() => {});
|
||||
mockInvoke = invokeMock;
|
||||
mockListen = listenMock;
|
||||
const invokeMock = vi.fn(async () => undefined);
|
||||
const listenMock = vi.fn(async () => () => {});
|
||||
mockInvoke = invokeMock as unknown as TauriInvoke;
|
||||
mockListen = listenMock as unknown as TauriListen;
|
||||
stream = new TauriTranscriptionStream('meeting-123', mockInvoke, mockListen);
|
||||
});
|
||||
|
||||
@@ -44,7 +40,7 @@ describe('TauriTranscriptionStream', () => {
|
||||
|
||||
const expectedPayload: Record<string, unknown> = {
|
||||
meeting_id: 'meeting-123',
|
||||
audio_data: expect.arrayContaining([expect.any(Number), expect.any(Number)]),
|
||||
audio_data: expect.arrayContaining([expect.any(Number), expect.any(Number)]) as unknown,
|
||||
timestamp: 1.5,
|
||||
sample_rate: 48000,
|
||||
channels: 2,
|
||||
@@ -57,10 +53,8 @@ describe('TauriTranscriptionStream', () => {
|
||||
|
||||
it('resets consecutive failures on successful send', async () => {
|
||||
const errorCallback = vi.fn();
|
||||
const failingInvoke = vi
|
||||
.fn<Parameters<TauriInvoke>, ReturnType<TauriInvoke>>()
|
||||
.mockRejectedValue(new Error('Network error'));
|
||||
const failingStream = new TauriTranscriptionStream('meeting-123', failingInvoke, mockListen);
|
||||
const failingInvoke = vi.fn(async () => { throw new Error('Network error'); });
|
||||
const failingStream = new TauriTranscriptionStream('meeting-123', failingInvoke as unknown as TauriInvoke, mockListen);
|
||||
failingStream.onError(errorCallback);
|
||||
|
||||
// Send twice (below threshold of 3)
|
||||
@@ -86,9 +80,8 @@ describe('TauriTranscriptionStream', () => {
|
||||
it('emits error after threshold consecutive failures', async () => {
|
||||
const errorCallback = vi.fn();
|
||||
const failingInvoke = vi
|
||||
.fn<Parameters<TauriInvoke>, ReturnType<TauriInvoke>>()
|
||||
.mockRejectedValue(new Error('Connection lost'));
|
||||
const failingStream = new TauriTranscriptionStream('meeting-123', failingInvoke, mockListen);
|
||||
.fn(async () => { throw new Error('Connection lost'); });
|
||||
const failingStream = new TauriTranscriptionStream('meeting-123', failingInvoke as unknown as TauriInvoke, mockListen);
|
||||
failingStream.onError(errorCallback);
|
||||
|
||||
// Send enough chunks to exceed threshold
|
||||
@@ -107,17 +100,15 @@ describe('TauriTranscriptionStream', () => {
|
||||
// The StreamingQueue reports failures with its own message format
|
||||
const expectedError: Record<string, unknown> = {
|
||||
code: 'stream_send_failed',
|
||||
message: expect.stringContaining('consecutive failures'),
|
||||
message: expect.stringContaining('consecutive failures') as unknown as string,
|
||||
};
|
||||
expect(errorCallback).toHaveBeenCalledWith(expectedError);
|
||||
});
|
||||
|
||||
it('only emits error once even with more failures', async () => {
|
||||
const errorCallback = vi.fn();
|
||||
const failingInvoke = vi
|
||||
.fn<Parameters<TauriInvoke>, ReturnType<TauriInvoke>>()
|
||||
.mockRejectedValue(new Error('Network error'));
|
||||
const failingStream = new TauriTranscriptionStream('meeting-123', failingInvoke, mockListen);
|
||||
const failingInvoke = vi.fn(async () => { throw new Error('Network error'); });
|
||||
const failingStream = new TauriTranscriptionStream('meeting-123', failingInvoke as unknown as TauriInvoke, mockListen);
|
||||
failingStream.onError(errorCallback);
|
||||
|
||||
// Send many chunks
|
||||
@@ -157,7 +148,7 @@ describe('TauriTranscriptionStream', () => {
|
||||
expect(mockAddClientLog).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
level: 'error',
|
||||
message: expect.stringContaining('operation failed'),
|
||||
message: expect.stringContaining('operation failed') as unknown as string,
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -177,8 +168,8 @@ describe('TauriTranscriptionStream', () => {
|
||||
|
||||
it('emits error on close failure', async () => {
|
||||
const errorCallback = vi.fn();
|
||||
const failingInvoke = vi.fn().mockRejectedValue(new Error('Failed to stop'));
|
||||
const failingStream = new TauriTranscriptionStream('meeting-123', failingInvoke, mockListen);
|
||||
const failingInvoke = vi.fn(async () => { throw new Error('Failed to stop'); });
|
||||
const failingStream = new TauriTranscriptionStream('meeting-123', failingInvoke as unknown as TauriInvoke, mockListen);
|
||||
failingStream.onError(errorCallback);
|
||||
|
||||
// close() re-throws errors, so we need to catch it
|
||||
@@ -188,7 +179,7 @@ describe('TauriTranscriptionStream', () => {
|
||||
|
||||
const expectedError: Record<string, unknown> = {
|
||||
code: 'stream_close_failed',
|
||||
message: expect.stringContaining('Failed to stop'),
|
||||
message: expect.stringContaining('Failed to stop') as unknown as string,
|
||||
};
|
||||
expect(errorCallback).toHaveBeenCalledWith(expectedError);
|
||||
});
|
||||
@@ -196,8 +187,8 @@ describe('TauriTranscriptionStream', () => {
|
||||
it('logs close errors to clientLog', async () => {
|
||||
const mockAddClientLog = vi.mocked(addClientLog);
|
||||
mockAddClientLog.mockClear();
|
||||
const failingInvoke = vi.fn().mockRejectedValue(new Error('Stop failed'));
|
||||
const failingStream = new TauriTranscriptionStream('meeting-123', failingInvoke, mockListen);
|
||||
const failingInvoke = vi.fn(async () => { throw new Error('Stop failed'); });
|
||||
const failingStream = new TauriTranscriptionStream('meeting-123', failingInvoke as unknown as TauriInvoke, mockListen);
|
||||
|
||||
// close() re-throws errors, so we need to catch it
|
||||
await failingStream.close().catch(() => {
|
||||
@@ -207,9 +198,9 @@ describe('TauriTranscriptionStream', () => {
|
||||
expect(mockAddClientLog).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
level: 'error',
|
||||
source: 'api',
|
||||
source: 'api' as const,
|
||||
message: 'Tauri stream stop_recording failed',
|
||||
})
|
||||
}) as unknown as Parameters<typeof addClientLog>[0]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -263,6 +263,10 @@ export type ServerAddressSource = 'environment' | 'preferences' | 'default';
|
||||
export interface EffectiveServerUrl {
|
||||
/** The server URL (e.g., "127.0.0.1:<port>") */
|
||||
url: string;
|
||||
/** Parsed host part */
|
||||
host: string;
|
||||
/** Parsed port part */
|
||||
port: string;
|
||||
/** Source of the URL configuration */
|
||||
source: ServerAddressSource;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ export type {
|
||||
AITemplate,
|
||||
AITone,
|
||||
AIVerbosity,
|
||||
ModelCatalogEntry,
|
||||
SummarizationOptions,
|
||||
TranscriptionProviderConfig,
|
||||
TranscriptionProviderType,
|
||||
|
||||
@@ -15,6 +15,7 @@ import { CustomIntegrationDialog, TestAllButton } from './custom-integration-dia
|
||||
import { groupIntegrationsByType } from './helpers';
|
||||
import { IntegrationSettingsProvider } from './integration-settings-context';
|
||||
import { IntegrationItem } from './integration-item';
|
||||
import type { Integration } from '@/api/types';
|
||||
import type { IntegrationsSectionProps } from './types';
|
||||
import { useIntegrationHandlers } from './use-integration-handlers';
|
||||
|
||||
@@ -134,7 +135,7 @@ export function IntegrationsSection({
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{Object.entries(groupedIntegrations).map(([type, items]) => (
|
||||
{(Object.entries(groupedIntegrations) as [string, Integration[]][]).map(([type, items]) => (
|
||||
<TabsContent key={type} value={type} className="space-y-3 mt-4">
|
||||
{items.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
|
||||
@@ -8,10 +8,10 @@ import type { CreateProjectRequest, Project, UpdateProjectRequest } from '@/api/
|
||||
import { ProjectContext, type ProjectContextValue } from '@/contexts/project-state';
|
||||
import { projectStorageKey } from '@/contexts/storage';
|
||||
import { useWorkspace } from '@/contexts/workspace-state';
|
||||
import { debug } from '@/lib/debug';
|
||||
import { errorLog } from '@/lib/debug';
|
||||
import { readStorageRaw, writeStorageRaw } from '@/lib/storage-utils';
|
||||
|
||||
const log = debug('project-context');
|
||||
const logError = errorLog('project-context');
|
||||
|
||||
function readStoredProjectId(workspaceId: string): string | null {
|
||||
const value = readStorageRaw(projectStorageKey(workspaceId), '');
|
||||
@@ -128,7 +128,7 @@ export function ProjectProvider({ children }: { children: React.ReactNode }) {
|
||||
.catch((err) => {
|
||||
// Failed to persist active project - context state already updated
|
||||
// Log for debugging but don't fail the operation
|
||||
log('Failed to persist active project to server:', err);
|
||||
logError('Failed to persist active project to server:', err);
|
||||
});
|
||||
},
|
||||
[currentWorkspace]
|
||||
|
||||
@@ -207,8 +207,8 @@ describe('AnthropicStrategy', () => {
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
'x-api-key': 'sk-ant-test',
|
||||
'anthropic-version': expect.any(String),
|
||||
}),
|
||||
'anthropic-version': expect.any(String) as unknown,
|
||||
}) as unknown,
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -266,7 +266,7 @@ describe('OllamaStrategy', () => {
|
||||
'http://localhost:11434/api/generate',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: expect.stringContaining('llama2'),
|
||||
body: expect.stringContaining('llama2') as unknown,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -318,7 +318,7 @@ function cleanupTechnicalMessage(message: string): string {
|
||||
// e.g., "getPreferences: received" → "Preferences received"
|
||||
cleaned = cleaned.replace(
|
||||
/^(get|set|load|save|fetch|create|delete|update)([A-Z][a-zA-Z]*):\s*/i,
|
||||
(_, _verb, noun) => {
|
||||
(_: string, _verb: string, noun: string) => {
|
||||
const readableNoun = noun.replace(/([A-Z])/g, ' $1').trim();
|
||||
return `${readableNoun} `;
|
||||
}
|
||||
@@ -334,8 +334,8 @@ function cleanupTechnicalMessage(message: string): string {
|
||||
|
||||
// Convert snake_case segments to readable text
|
||||
// Only for isolated snake_case words, not in the middle of sentences
|
||||
cleaned = cleaned.replace(/\b([a-z]+)_([a-z]+)(?:_([a-z]+))?\b/gi, (_match, p1, p2, p3) => {
|
||||
const parts = [p1, p2, p3].filter(Boolean);
|
||||
cleaned = cleaned.replace(/\b([a-z]+)_([a-z]+)(?:_([a-z]+))?\b/gi, (_match: string, p1: string, p2: string, p3: string | undefined) => {
|
||||
const parts = [p1, p2, p3].filter((p): p is string => !!p);
|
||||
return parts.map((p: string) => p.charAt(0).toUpperCase() + p.slice(1).toLowerCase()).join(' ');
|
||||
});
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ vi.mock('@/hooks/use-guarded-mutation', () => ({
|
||||
useGuardedMutation: () => ({ guard }),
|
||||
}));
|
||||
|
||||
const toast = vi.fn<unknown[], void>();
|
||||
const toast = vi.fn<(...args: unknown[]) => void>();
|
||||
vi.mock('@/hooks/use-toast', () => ({
|
||||
toast: (...args: unknown[]) => toast(...args),
|
||||
}));
|
||||
@@ -206,7 +206,7 @@ vi.mock('@/components/recording', () => ({
|
||||
{showPanel ? (
|
||||
'visible'
|
||||
) : (
|
||||
<button type="button" title="Expand notes panel" onClick={() => setShowNotesPanel(true)}>
|
||||
<button type="button" title="Expand notes panel" onClick={() => { setShowNotesPanel(true); }}>
|
||||
Expand Notes
|
||||
</button>
|
||||
)}
|
||||
@@ -217,7 +217,7 @@ vi.mock('@/components/recording', () => ({
|
||||
{showPanel ? (
|
||||
'visible'
|
||||
) : (
|
||||
<button type="button" title="Expand stats panel" onClick={() => setShowStatsPanel(true)}>
|
||||
<button type="button" title="Expand stats panel" onClick={() => { setShowStatsPanel(true); }}>
|
||||
Expand Stats
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
|
||||
import { Loader2, Tags } from 'lucide-react';
|
||||
|
||||
import type { NamedEntity } from '@/api/types';
|
||||
import type { ExtractedEntity } from '@/api/types';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { iconWithMargin } from '@/lib/styles';
|
||||
import { ENTITY_CATEGORY_COLORS } from '@/types/entity';
|
||||
|
||||
interface EntitiesPanelProps {
|
||||
entities: NamedEntity[];
|
||||
entities: ExtractedEntity[];
|
||||
isExtracting: boolean;
|
||||
meetingState: string;
|
||||
onExtract: (force: boolean) => void;
|
||||
@@ -60,7 +60,7 @@ export function EntitiesPanel({
|
||||
>
|
||||
{entity.category}
|
||||
</span>
|
||||
{entity.isPinned && <span className="text-xs text-amber-500">★</span>}
|
||||
{entity.is_pinned && <span className="text-xs text-amber-500">★</span>}
|
||||
</div>
|
||||
<p className="text-sm font-medium">{entity.text}</p>
|
||||
{entity.confidence !== undefined && (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AIFormat, AITone, AIVerbosity, ExportFormat, ServerInfo } from '@/api/types';
|
||||
import type { AIConfig, AIFormat, AITone, AIVerbosity, ExportFormat, ServerInfo } from '@/api/types';
|
||||
import {
|
||||
AdvancedLocalAISettings,
|
||||
AIConfigSection,
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
SummarizationSettingsPanel,
|
||||
} from '@/components/settings';
|
||||
import { PROVIDER_ENDPOINTS } from '@/lib/config/provider-endpoints';
|
||||
import { preferences, type AIConfig } from '@/lib/preferences';
|
||||
import { preferences } from '@/lib/preferences';
|
||||
|
||||
interface AITabProps {
|
||||
defaultExportFormat: ExportFormat;
|
||||
@@ -25,9 +25,6 @@ interface AITabProps {
|
||||
|
||||
/** Get Ollama base URL from whichever provider is using it. */
|
||||
function getOllamaBaseUrl(aiConfig: AIConfig): string {
|
||||
if (aiConfig.transcription.provider === 'ollama') {
|
||||
return aiConfig.transcription.base_url;
|
||||
}
|
||||
if (aiConfig.summary.provider === 'ollama') {
|
||||
return aiConfig.summary.base_url;
|
||||
}
|
||||
@@ -53,9 +50,7 @@ export function AITab({
|
||||
// Check if Ollama is selected for one of the provider types
|
||||
const aiConfig = preferences.get().ai_config;
|
||||
const isOllamaSelected =
|
||||
aiConfig.transcription.provider === 'ollama' ||
|
||||
aiConfig.summary.provider === 'ollama' ||
|
||||
aiConfig.embedding.provider === 'ollama';
|
||||
aiConfig.summary.provider === 'ollama' || aiConfig.embedding.provider === 'ollama';
|
||||
|
||||
const ollamaBaseUrl = getOllamaBaseUrl(aiConfig);
|
||||
|
||||
|
||||
@@ -10,13 +10,14 @@ const srcDir = path.resolve(rootDir, 'src');
|
||||
|
||||
const noteflowAlias = () => ({
|
||||
name: 'noteflow-alias',
|
||||
enforce: 'pre',
|
||||
async resolveId(source: string, importer: string | undefined) {
|
||||
enforce: 'pre' as const,
|
||||
async resolveId(this: unknown, source: string, importer: string | undefined) {
|
||||
if (!source.startsWith('@/')) {
|
||||
return null;
|
||||
}
|
||||
const target = path.resolve(srcDir, source.slice(2));
|
||||
return (await this.resolve(target, importer, { skipSelf: true })) ?? target;
|
||||
const resolved = await (this as { resolve: (...args: unknown[]) => Promise<unknown> }).resolve(target, importer, { skipSelf: true });
|
||||
return (resolved as string | null) ?? target;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user