Initial commit
This commit is contained in:
721
frontend/src/components/SettingsScreen.tsx
Normal file
721
frontend/src/components/SettingsScreen.tsx
Normal file
@@ -0,0 +1,721 @@
|
||||
/**
|
||||
* 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user