722 lines
31 KiB
TypeScript
722 lines
31 KiB
TypeScript
/**
|
|
* Dedicated settings screen for providers, task model bindings, and catalog controls.
|
|
*/
|
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
|
|
import PathInput from './PathInput';
|
|
import TagInput from './TagInput';
|
|
import type {
|
|
AppSettings,
|
|
AppSettingsUpdate,
|
|
DisplaySettings,
|
|
HandwritingStyleClusteringSettings,
|
|
OcrTaskSettings,
|
|
PredefinedPathEntry,
|
|
PredefinedTagEntry,
|
|
ProviderSettings,
|
|
RoutingTaskSettings,
|
|
SummaryTaskSettings,
|
|
UploadDefaultsSettings,
|
|
} from '../types';
|
|
|
|
interface EditableProvider extends ProviderSettings {
|
|
row_id: string;
|
|
api_key: string;
|
|
clear_api_key: boolean;
|
|
}
|
|
|
|
interface SettingsScreenProps {
|
|
settings: AppSettings | null;
|
|
isSaving: boolean;
|
|
knownTags: string[];
|
|
knownPaths: string[];
|
|
onSave: (payload: AppSettingsUpdate) => Promise<void>;
|
|
onRegisterSaveAction?: (action: (() => Promise<void>) | null) => void;
|
|
}
|
|
|
|
function clampCardsPerPage(value: number): number {
|
|
return Math.max(1, Math.min(200, value));
|
|
}
|
|
|
|
function parseCardsPerPageInput(input: string, fallback: number): number {
|
|
const parsed = Number.parseInt(input, 10);
|
|
if (Number.isNaN(parsed)) {
|
|
return clampCardsPerPage(fallback);
|
|
}
|
|
return clampCardsPerPage(parsed);
|
|
}
|
|
|
|
/**
|
|
* Renders compact human-oriented settings controls.
|
|
*/
|
|
export default function SettingsScreen({
|
|
settings,
|
|
isSaving,
|
|
knownTags,
|
|
knownPaths,
|
|
onSave,
|
|
onRegisterSaveAction,
|
|
}: SettingsScreenProps): JSX.Element {
|
|
const [providers, setProviders] = useState<EditableProvider[]>([]);
|
|
const [ocrTask, setOcrTask] = useState<OcrTaskSettings | null>(null);
|
|
const [summaryTask, setSummaryTask] = useState<SummaryTaskSettings | null>(null);
|
|
const [routingTask, setRoutingTask] = useState<RoutingTaskSettings | null>(null);
|
|
const [handwritingStyle, setHandwritingStyle] = useState<HandwritingStyleClusteringSettings | null>(null);
|
|
const [predefinedPaths, setPredefinedPaths] = useState<PredefinedPathEntry[]>([]);
|
|
const [predefinedTags, setPredefinedTags] = useState<PredefinedTagEntry[]>([]);
|
|
const [newPredefinedPath, setNewPredefinedPath] = useState<string>('');
|
|
const [newPredefinedTag, setNewPredefinedTag] = useState<string>('');
|
|
const [uploadDefaults, setUploadDefaults] = useState<UploadDefaultsSettings | null>(null);
|
|
const [displaySettings, setDisplaySettings] = useState<DisplaySettings | null>(null);
|
|
const [cardsPerPageInput, setCardsPerPageInput] = useState<string>('12');
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!settings) {
|
|
return;
|
|
}
|
|
setProviders(
|
|
settings.providers.map((provider) => ({
|
|
...provider,
|
|
row_id: `${provider.id}-${Math.random().toString(36).slice(2, 9)}`,
|
|
api_key: '',
|
|
clear_api_key: false,
|
|
})),
|
|
);
|
|
setOcrTask(settings.tasks.ocr_handwriting);
|
|
setSummaryTask(settings.tasks.summary_generation);
|
|
setRoutingTask(settings.tasks.routing_classification);
|
|
setHandwritingStyle(settings.handwriting_style_clustering);
|
|
setPredefinedPaths(settings.predefined_paths);
|
|
setPredefinedTags(settings.predefined_tags);
|
|
setUploadDefaults(settings.upload_defaults);
|
|
setDisplaySettings(settings.display);
|
|
setCardsPerPageInput(String(settings.display.cards_per_page));
|
|
setError(null);
|
|
}, [settings]);
|
|
|
|
const fallbackProviderId = useMemo(() => providers[0]?.id ?? '', [providers]);
|
|
|
|
const addProvider = (): void => {
|
|
const sequence = providers.length + 1;
|
|
setProviders((current) => [
|
|
...current,
|
|
{
|
|
row_id: `provider-row-${Date.now()}-${sequence}`,
|
|
id: `provider-${sequence}`,
|
|
label: `Provider ${sequence}`,
|
|
provider_type: 'openai_compatible',
|
|
base_url: 'http://localhost:11434/v1',
|
|
timeout_seconds: 45,
|
|
api_key_set: false,
|
|
api_key_masked: '',
|
|
api_key: '',
|
|
clear_api_key: false,
|
|
},
|
|
]);
|
|
};
|
|
|
|
const removeProvider = (rowId: string): void => {
|
|
const target = providers.find((provider) => provider.row_id === rowId);
|
|
if (!target || providers.length <= 1) {
|
|
return;
|
|
}
|
|
const remaining = providers.filter((provider) => provider.row_id !== rowId);
|
|
const fallback = remaining[0]?.id ?? '';
|
|
setProviders(remaining);
|
|
if (ocrTask?.provider_id === target.id && fallback) {
|
|
setOcrTask({ ...ocrTask, provider_id: fallback });
|
|
}
|
|
if (summaryTask?.provider_id === target.id && fallback) {
|
|
setSummaryTask({ ...summaryTask, provider_id: fallback });
|
|
}
|
|
if (routingTask?.provider_id === target.id && fallback) {
|
|
setRoutingTask({ ...routingTask, provider_id: fallback });
|
|
}
|
|
};
|
|
|
|
const addPredefinedPath = (): void => {
|
|
const value = newPredefinedPath.trim().replace(/^\/+|\/+$/g, '');
|
|
if (!value) {
|
|
return;
|
|
}
|
|
if (predefinedPaths.some((entry) => entry.value.toLowerCase() === value.toLowerCase())) {
|
|
setNewPredefinedPath('');
|
|
return;
|
|
}
|
|
setPredefinedPaths([...predefinedPaths, { value, global_shared: false }]);
|
|
setNewPredefinedPath('');
|
|
};
|
|
|
|
const addPredefinedTag = (): void => {
|
|
const value = newPredefinedTag.trim();
|
|
if (!value) {
|
|
return;
|
|
}
|
|
if (predefinedTags.some((entry) => entry.value.toLowerCase() === value.toLowerCase())) {
|
|
setNewPredefinedTag('');
|
|
return;
|
|
}
|
|
setPredefinedTags([...predefinedTags, { value, global_shared: false }]);
|
|
setNewPredefinedTag('');
|
|
};
|
|
|
|
const handleSave = useCallback(async (): Promise<void> => {
|
|
if (!ocrTask || !summaryTask || !routingTask || !handwritingStyle || !uploadDefaults || !displaySettings) {
|
|
setError('Settings are not fully loaded yet');
|
|
return;
|
|
}
|
|
if (providers.length === 0) {
|
|
setError('At least one provider is required');
|
|
return;
|
|
}
|
|
|
|
setError(null);
|
|
try {
|
|
const resolvedCardsPerPage = parseCardsPerPageInput(cardsPerPageInput, displaySettings.cards_per_page);
|
|
setDisplaySettings({ ...displaySettings, cards_per_page: resolvedCardsPerPage });
|
|
setCardsPerPageInput(String(resolvedCardsPerPage));
|
|
|
|
await onSave({
|
|
upload_defaults: {
|
|
logical_path: uploadDefaults.logical_path.trim(),
|
|
tags: uploadDefaults.tags,
|
|
},
|
|
display: {
|
|
cards_per_page: resolvedCardsPerPage,
|
|
log_typing_animation_enabled: displaySettings.log_typing_animation_enabled,
|
|
},
|
|
predefined_paths: predefinedPaths,
|
|
predefined_tags: predefinedTags,
|
|
handwriting_style_clustering: {
|
|
enabled: handwritingStyle.enabled,
|
|
embed_model: handwritingStyle.embed_model.trim(),
|
|
neighbor_limit: handwritingStyle.neighbor_limit,
|
|
match_min_similarity: handwritingStyle.match_min_similarity,
|
|
bootstrap_match_min_similarity: handwritingStyle.bootstrap_match_min_similarity,
|
|
bootstrap_sample_size: handwritingStyle.bootstrap_sample_size,
|
|
image_max_side: handwritingStyle.image_max_side,
|
|
},
|
|
providers: providers.map((provider) => ({
|
|
id: provider.id.trim(),
|
|
label: provider.label.trim(),
|
|
provider_type: provider.provider_type,
|
|
base_url: provider.base_url.trim(),
|
|
timeout_seconds: provider.timeout_seconds,
|
|
api_key: provider.api_key.trim() || undefined,
|
|
clear_api_key: provider.clear_api_key,
|
|
})),
|
|
tasks: {
|
|
ocr_handwriting: {
|
|
enabled: ocrTask.enabled,
|
|
provider_id: ocrTask.provider_id,
|
|
model: ocrTask.model.trim(),
|
|
prompt: ocrTask.prompt,
|
|
},
|
|
summary_generation: {
|
|
enabled: summaryTask.enabled,
|
|
provider_id: summaryTask.provider_id,
|
|
model: summaryTask.model.trim(),
|
|
prompt: summaryTask.prompt,
|
|
max_input_tokens: summaryTask.max_input_tokens,
|
|
},
|
|
routing_classification: {
|
|
enabled: routingTask.enabled,
|
|
provider_id: routingTask.provider_id,
|
|
model: routingTask.model.trim(),
|
|
prompt: routingTask.prompt,
|
|
neighbor_count: routingTask.neighbor_count,
|
|
neighbor_min_similarity: routingTask.neighbor_min_similarity,
|
|
auto_apply_confidence_threshold: routingTask.auto_apply_confidence_threshold,
|
|
auto_apply_neighbor_similarity_threshold: routingTask.auto_apply_neighbor_similarity_threshold,
|
|
neighbor_path_override_enabled: routingTask.neighbor_path_override_enabled,
|
|
neighbor_path_override_min_similarity: routingTask.neighbor_path_override_min_similarity,
|
|
neighbor_path_override_min_gap: routingTask.neighbor_path_override_min_gap,
|
|
neighbor_path_override_max_confidence: routingTask.neighbor_path_override_max_confidence,
|
|
},
|
|
},
|
|
});
|
|
} catch (caughtError) {
|
|
setError(caughtError instanceof Error ? caughtError.message : 'Failed to save settings');
|
|
}
|
|
}, [
|
|
cardsPerPageInput,
|
|
displaySettings,
|
|
handwritingStyle,
|
|
ocrTask,
|
|
onSave,
|
|
predefinedPaths,
|
|
predefinedTags,
|
|
providers,
|
|
routingTask,
|
|
summaryTask,
|
|
uploadDefaults,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
if (!onRegisterSaveAction) {
|
|
return;
|
|
}
|
|
if (!settings || !ocrTask || !summaryTask || !routingTask || !handwritingStyle || !uploadDefaults || !displaySettings) {
|
|
onRegisterSaveAction(null);
|
|
return;
|
|
}
|
|
onRegisterSaveAction(() => handleSave());
|
|
return () => onRegisterSaveAction(null);
|
|
}, [displaySettings, handleSave, handwritingStyle, ocrTask, onRegisterSaveAction, routingTask, settings, summaryTask, uploadDefaults]);
|
|
|
|
if (!settings || !ocrTask || !summaryTask || !routingTask || !handwritingStyle || !uploadDefaults || !displaySettings) {
|
|
return (
|
|
<section className="settings-layout">
|
|
<div className="settings-card">
|
|
<h2>Settings</h2>
|
|
<p>Loading settings...</p>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<section className="settings-layout">
|
|
{error && <p className="error-banner">{error}</p>}
|
|
|
|
<div className="settings-card settings-section">
|
|
<div className="settings-section-header">
|
|
<h3>Workspace</h3>
|
|
<p className="small">Defaults and display behavior for document operations.</p>
|
|
</div>
|
|
<div className="settings-field-grid">
|
|
<label className="settings-field settings-field-wide">
|
|
Default Path
|
|
<PathInput
|
|
value={uploadDefaults.logical_path}
|
|
onChange={(nextPath) => setUploadDefaults({ ...uploadDefaults, logical_path: nextPath })}
|
|
suggestions={knownPaths}
|
|
/>
|
|
</label>
|
|
<label className="settings-field settings-field-wide">
|
|
Default Tags
|
|
<TagInput
|
|
value={uploadDefaults.tags}
|
|
onChange={(nextTags) => setUploadDefaults({ ...uploadDefaults, tags: nextTags })}
|
|
suggestions={knownTags}
|
|
/>
|
|
</label>
|
|
<label className="settings-field">
|
|
Cards Per Page
|
|
<input
|
|
type="number"
|
|
min={1}
|
|
max={200}
|
|
value={cardsPerPageInput}
|
|
onChange={(event) => setCardsPerPageInput(event.target.value)}
|
|
/>
|
|
</label>
|
|
<label className="inline-checkbox settings-checkbox-field">
|
|
<input
|
|
type="checkbox"
|
|
checked={displaySettings.log_typing_animation_enabled}
|
|
onChange={(event) =>
|
|
setDisplaySettings({ ...displaySettings, log_typing_animation_enabled: event.target.checked })
|
|
}
|
|
/>
|
|
Processing log typing animation enabled
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="settings-card settings-section">
|
|
<div className="settings-section-header">
|
|
<h3>Catalog Presets</h3>
|
|
<p className="small">Pre-register allowed paths and tags. Global-shared is irreversible.</p>
|
|
</div>
|
|
<div className="settings-catalog-grid">
|
|
<section className="settings-catalog-card">
|
|
<h4>Predefined Paths</h4>
|
|
<div className="settings-catalog-add-row">
|
|
<input
|
|
placeholder="Add path"
|
|
value={newPredefinedPath}
|
|
onChange={(event) => setNewPredefinedPath(event.target.value)}
|
|
/>
|
|
<button type="button" className="secondary-action" onClick={addPredefinedPath}>
|
|
Add
|
|
</button>
|
|
</div>
|
|
<div className="settings-catalog-list">
|
|
{predefinedPaths.map((entry) => (
|
|
<div key={entry.value} className="settings-catalog-row">
|
|
<span>{entry.value}</span>
|
|
<label className="inline-checkbox">
|
|
<input
|
|
type="checkbox"
|
|
checked={entry.global_shared}
|
|
disabled={entry.global_shared}
|
|
onChange={(event) =>
|
|
setPredefinedPaths((current) =>
|
|
current.map((item) =>
|
|
item.value === entry.value
|
|
? { ...item, global_shared: item.global_shared || event.target.checked }
|
|
: item,
|
|
),
|
|
)
|
|
}
|
|
/>
|
|
Global
|
|
</label>
|
|
<button
|
|
type="button"
|
|
className="secondary-action"
|
|
onClick={() => setPredefinedPaths((current) => current.filter((item) => item.value !== entry.value))}
|
|
>
|
|
Remove
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
<section className="settings-catalog-card">
|
|
<h4>Predefined Tags</h4>
|
|
<div className="settings-catalog-add-row">
|
|
<input
|
|
placeholder="Add tag"
|
|
value={newPredefinedTag}
|
|
onChange={(event) => setNewPredefinedTag(event.target.value)}
|
|
/>
|
|
<button type="button" className="secondary-action" onClick={addPredefinedTag}>
|
|
Add
|
|
</button>
|
|
</div>
|
|
<div className="settings-catalog-list">
|
|
{predefinedTags.map((entry) => (
|
|
<div key={entry.value} className="settings-catalog-row">
|
|
<span>{entry.value}</span>
|
|
<label className="inline-checkbox">
|
|
<input
|
|
type="checkbox"
|
|
checked={entry.global_shared}
|
|
disabled={entry.global_shared}
|
|
onChange={(event) =>
|
|
setPredefinedTags((current) =>
|
|
current.map((item) =>
|
|
item.value === entry.value
|
|
? { ...item, global_shared: item.global_shared || event.target.checked }
|
|
: item,
|
|
),
|
|
)
|
|
}
|
|
/>
|
|
Global
|
|
</label>
|
|
<button
|
|
type="button"
|
|
className="secondary-action"
|
|
onClick={() => setPredefinedTags((current) => current.filter((item) => item.value !== entry.value))}
|
|
>
|
|
Remove
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="settings-card settings-section">
|
|
<div className="settings-section-header">
|
|
<h3>Providers</h3>
|
|
<p className="small">Configure OpenAI-compatible model endpoints.</p>
|
|
</div>
|
|
<div className="provider-list">
|
|
{providers.map((provider, index) => (
|
|
<div key={provider.row_id} className="provider-grid">
|
|
<div className="provider-header">
|
|
<h4>{provider.label || `Provider ${index + 1}`}</h4>
|
|
<button
|
|
type="button"
|
|
className="danger-action"
|
|
onClick={() => removeProvider(provider.row_id)}
|
|
disabled={providers.length <= 1 || isSaving}
|
|
>
|
|
Remove
|
|
</button>
|
|
</div>
|
|
<div className="settings-field-grid">
|
|
<label className="settings-field">
|
|
Provider ID
|
|
<input
|
|
value={provider.id}
|
|
onChange={(event) =>
|
|
setProviders((current) =>
|
|
current.map((item) => (item.row_id === provider.row_id ? { ...item, id: event.target.value } : item)),
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
<label className="settings-field">
|
|
Label
|
|
<input
|
|
value={provider.label}
|
|
onChange={(event) =>
|
|
setProviders((current) =>
|
|
current.map((item) =>
|
|
item.row_id === provider.row_id ? { ...item, label: event.target.value } : item,
|
|
),
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
<label className="settings-field">
|
|
Timeout Seconds
|
|
<input
|
|
type="number"
|
|
value={provider.timeout_seconds}
|
|
onChange={(event) => {
|
|
const nextTimeout = Number.parseInt(event.target.value, 10);
|
|
if (Number.isNaN(nextTimeout)) {
|
|
return;
|
|
}
|
|
setProviders((current) =>
|
|
current.map((item) =>
|
|
item.row_id === provider.row_id ? { ...item, timeout_seconds: nextTimeout } : item,
|
|
),
|
|
);
|
|
}}
|
|
/>
|
|
</label>
|
|
<label className="settings-field settings-field-wide">
|
|
Base URL
|
|
<input
|
|
value={provider.base_url}
|
|
onChange={(event) =>
|
|
setProviders((current) =>
|
|
current.map((item) =>
|
|
item.row_id === provider.row_id ? { ...item, base_url: event.target.value } : item,
|
|
),
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
<label className="settings-field settings-field-wide">
|
|
API Key
|
|
<input
|
|
type="password"
|
|
placeholder={provider.api_key_set ? `Stored: ${provider.api_key_masked}` : 'Optional API key'}
|
|
value={provider.api_key}
|
|
onChange={(event) =>
|
|
setProviders((current) =>
|
|
current.map((item) =>
|
|
item.row_id === provider.row_id ? { ...item, api_key: event.target.value } : item,
|
|
),
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
<label className="inline-checkbox settings-checkbox-field">
|
|
<input
|
|
type="checkbox"
|
|
checked={provider.clear_api_key}
|
|
onChange={(event) =>
|
|
setProviders((current) =>
|
|
current.map((item) =>
|
|
item.row_id === provider.row_id ? { ...item, clear_api_key: event.target.checked } : item,
|
|
),
|
|
)
|
|
}
|
|
/>
|
|
Clear Stored API Key
|
|
</label>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="settings-section-actions">
|
|
<button type="button" className="secondary-action" onClick={addProvider} disabled={isSaving}>
|
|
Add Provider
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="settings-card settings-section">
|
|
<div className="settings-section-header">
|
|
<h3>Task Runtime</h3>
|
|
<p className="small">Bind providers and tune OCR, summary, routing, and handwriting style behavior.</p>
|
|
</div>
|
|
|
|
<div className="task-settings-block">
|
|
<div className="task-block-header">
|
|
<h4>OCR Handwriting</h4>
|
|
<label className="inline-checkbox settings-toggle">
|
|
<input type="checkbox" checked={ocrTask.enabled} onChange={(event) => setOcrTask({ ...ocrTask, enabled: event.target.checked })} />
|
|
Enabled
|
|
</label>
|
|
</div>
|
|
<div className="settings-field-grid">
|
|
<label className="settings-field">
|
|
Provider
|
|
<select value={ocrTask.provider_id} onChange={(event) => setOcrTask({ ...ocrTask, provider_id: event.target.value || fallbackProviderId })}>
|
|
{providers.map((provider) => (
|
|
<option key={provider.row_id} value={provider.id}>
|
|
{provider.label} ({provider.id})
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<label className="settings-field">
|
|
Model
|
|
<input value={ocrTask.model} onChange={(event) => setOcrTask({ ...ocrTask, model: event.target.value })} />
|
|
</label>
|
|
<label className="settings-field settings-field-wide">
|
|
OCR Prompt
|
|
<textarea value={ocrTask.prompt} onChange={(event) => setOcrTask({ ...ocrTask, prompt: event.target.value })} />
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="task-settings-block">
|
|
<div className="task-block-header">
|
|
<h4>Summary Generation</h4>
|
|
<label className="inline-checkbox settings-toggle">
|
|
<input type="checkbox" checked={summaryTask.enabled} onChange={(event) => setSummaryTask({ ...summaryTask, enabled: event.target.checked })} />
|
|
Enabled
|
|
</label>
|
|
</div>
|
|
<div className="settings-field-grid">
|
|
<label className="settings-field">
|
|
Provider
|
|
<select value={summaryTask.provider_id} onChange={(event) => setSummaryTask({ ...summaryTask, provider_id: event.target.value || fallbackProviderId })}>
|
|
{providers.map((provider) => (
|
|
<option key={provider.row_id} value={provider.id}>
|
|
{provider.label} ({provider.id})
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<label className="settings-field">
|
|
Model
|
|
<input value={summaryTask.model} onChange={(event) => setSummaryTask({ ...summaryTask, model: event.target.value })} />
|
|
</label>
|
|
<label className="settings-field">
|
|
Max Input Tokens
|
|
<input
|
|
type="number"
|
|
min={512}
|
|
max={64000}
|
|
value={summaryTask.max_input_tokens}
|
|
onChange={(event) => {
|
|
const nextValue = Number.parseInt(event.target.value, 10);
|
|
if (!Number.isNaN(nextValue)) {
|
|
setSummaryTask({ ...summaryTask, max_input_tokens: nextValue });
|
|
}
|
|
}}
|
|
/>
|
|
</label>
|
|
<label className="settings-field settings-field-wide">
|
|
Summary Prompt
|
|
<textarea value={summaryTask.prompt} onChange={(event) => setSummaryTask({ ...summaryTask, prompt: event.target.value })} />
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="task-settings-block">
|
|
<div className="task-block-header">
|
|
<h4>Routing Classification</h4>
|
|
<label className="inline-checkbox settings-toggle">
|
|
<input type="checkbox" checked={routingTask.enabled} onChange={(event) => setRoutingTask({ ...routingTask, enabled: event.target.checked })} />
|
|
Enabled
|
|
</label>
|
|
</div>
|
|
<div className="settings-field-grid">
|
|
<label className="settings-field">
|
|
Provider
|
|
<select value={routingTask.provider_id} onChange={(event) => setRoutingTask({ ...routingTask, provider_id: event.target.value || fallbackProviderId })}>
|
|
{providers.map((provider) => (
|
|
<option key={provider.row_id} value={provider.id}>
|
|
{provider.label} ({provider.id})
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<label className="settings-field">
|
|
Model
|
|
<input value={routingTask.model} onChange={(event) => setRoutingTask({ ...routingTask, model: event.target.value })} />
|
|
</label>
|
|
<label className="settings-field">
|
|
Neighbor Count
|
|
<input type="number" value={routingTask.neighbor_count} onChange={(event) => setRoutingTask({ ...routingTask, neighbor_count: Number.parseInt(event.target.value, 10) || routingTask.neighbor_count })} />
|
|
</label>
|
|
<label className="settings-field">
|
|
Min Neighbor Similarity
|
|
<input type="number" step="0.01" min="0" max="1" value={routingTask.neighbor_min_similarity} onChange={(event) => setRoutingTask({ ...routingTask, neighbor_min_similarity: Number.parseFloat(event.target.value) || routingTask.neighbor_min_similarity })} />
|
|
</label>
|
|
<label className="settings-field">
|
|
Auto Apply Confidence
|
|
<input type="number" step="0.01" min="0" max="1" value={routingTask.auto_apply_confidence_threshold} onChange={(event) => setRoutingTask({ ...routingTask, auto_apply_confidence_threshold: Number.parseFloat(event.target.value) || routingTask.auto_apply_confidence_threshold })} />
|
|
</label>
|
|
<label className="settings-field">
|
|
Auto Apply Neighbor Similarity
|
|
<input type="number" step="0.01" min="0" max="1" value={routingTask.auto_apply_neighbor_similarity_threshold} onChange={(event) => setRoutingTask({ ...routingTask, auto_apply_neighbor_similarity_threshold: Number.parseFloat(event.target.value) || routingTask.auto_apply_neighbor_similarity_threshold })} />
|
|
</label>
|
|
<label className="inline-checkbox settings-checkbox-field">
|
|
<input type="checkbox" checked={routingTask.neighbor_path_override_enabled} onChange={(event) => setRoutingTask({ ...routingTask, neighbor_path_override_enabled: event.target.checked })} />
|
|
Dominant neighbor path override enabled
|
|
</label>
|
|
<label className="settings-field">
|
|
Override Min Similarity
|
|
<input type="number" step="0.01" min="0" max="1" value={routingTask.neighbor_path_override_min_similarity} onChange={(event) => setRoutingTask({ ...routingTask, neighbor_path_override_min_similarity: Number.parseFloat(event.target.value) || routingTask.neighbor_path_override_min_similarity })} />
|
|
</label>
|
|
<label className="settings-field">
|
|
Override Min Gap
|
|
<input type="number" step="0.01" min="0" max="1" value={routingTask.neighbor_path_override_min_gap} onChange={(event) => setRoutingTask({ ...routingTask, neighbor_path_override_min_gap: Number.parseFloat(event.target.value) || routingTask.neighbor_path_override_min_gap })} />
|
|
</label>
|
|
<label className="settings-field">
|
|
Override Max LLM Confidence
|
|
<input type="number" step="0.01" min="0" max="1" value={routingTask.neighbor_path_override_max_confidence} onChange={(event) => setRoutingTask({ ...routingTask, neighbor_path_override_max_confidence: Number.parseFloat(event.target.value) || routingTask.neighbor_path_override_max_confidence })} />
|
|
</label>
|
|
<label className="settings-field settings-field-wide">
|
|
Routing Prompt
|
|
<textarea value={routingTask.prompt} onChange={(event) => setRoutingTask({ ...routingTask, prompt: event.target.value })} />
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="task-settings-block">
|
|
<div className="task-block-header">
|
|
<h4>Handwriting Style Clustering</h4>
|
|
<label className="inline-checkbox settings-toggle">
|
|
<input type="checkbox" checked={handwritingStyle.enabled} onChange={(event) => setHandwritingStyle({ ...handwritingStyle, enabled: event.target.checked })} />
|
|
Enabled
|
|
</label>
|
|
</div>
|
|
<div className="settings-field-grid">
|
|
<label className="settings-field settings-field-wide">
|
|
Typesense Embedding Model Slug
|
|
<input value={handwritingStyle.embed_model} onChange={(event) => setHandwritingStyle({ ...handwritingStyle, embed_model: event.target.value })} />
|
|
</label>
|
|
<label className="settings-field">
|
|
Neighbor Limit
|
|
<input type="number" min={1} max={32} value={handwritingStyle.neighbor_limit} onChange={(event) => setHandwritingStyle({ ...handwritingStyle, neighbor_limit: Number.parseInt(event.target.value, 10) || handwritingStyle.neighbor_limit })} />
|
|
</label>
|
|
<label className="settings-field">
|
|
Match Min Similarity
|
|
<input type="number" step="0.01" min="0" max="1" value={handwritingStyle.match_min_similarity} onChange={(event) => setHandwritingStyle({ ...handwritingStyle, match_min_similarity: Number.parseFloat(event.target.value) || handwritingStyle.match_min_similarity })} />
|
|
</label>
|
|
<label className="settings-field">
|
|
Bootstrap Match Min Similarity
|
|
<input type="number" step="0.01" min="0" max="1" value={handwritingStyle.bootstrap_match_min_similarity} onChange={(event) => setHandwritingStyle({ ...handwritingStyle, bootstrap_match_min_similarity: Number.parseFloat(event.target.value) || handwritingStyle.bootstrap_match_min_similarity })} />
|
|
</label>
|
|
<label className="settings-field">
|
|
Bootstrap Sample Size
|
|
<input type="number" min={1} max={30} value={handwritingStyle.bootstrap_sample_size} onChange={(event) => setHandwritingStyle({ ...handwritingStyle, bootstrap_sample_size: Number.parseInt(event.target.value, 10) || handwritingStyle.bootstrap_sample_size })} />
|
|
</label>
|
|
<label className="settings-field">
|
|
Max Image Side (px)
|
|
<input type="number" min={256} max={4096} value={handwritingStyle.image_max_side} onChange={(event) => setHandwritingStyle({ ...handwritingStyle, image_max_side: Number.parseInt(event.target.value, 10) || handwritingStyle.image_max_side })} />
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|