feat(desktop): add i18n infrastructure with react-intl (#8105)

This commit is contained in:
jh-block
2026-03-27 19:39:50 +01:00
committed by GitHub
parent d596113e72
commit a6765fbcd9
172 changed files with 13793 additions and 2054 deletions
@@ -10,6 +10,62 @@ import {
} from '../../../api';
import { toastError } from '../../../toasts';
import { errorMessage } from '../../../utils/conversionUtils';
import { defineMessages, useIntl } from '../../../i18n';
const i18n = defineMessages({
searchHuggingFace: {
id: 'huggingFaceModelSearch.searchHuggingFace',
defaultMessage: 'Search HuggingFace',
},
searchPlaceholder: {
id: 'huggingFaceModelSearch.searchPlaceholder',
defaultMessage: 'Search for GGUF models...',
},
loadingVariants: {
id: 'huggingFaceModelSearch.loadingVariants',
defaultMessage: 'Loading variants...',
},
recommended: {
id: 'huggingFaceModelSearch.recommended',
defaultMessage: 'Recommended',
},
download: {
id: 'huggingFaceModelSearch.download',
defaultMessage: 'Download',
},
directDownload: {
id: 'huggingFaceModelSearch.directDownload',
defaultMessage: 'Direct Download',
},
directDownloadDescription: {
id: 'huggingFaceModelSearch.directDownloadDescription',
defaultMessage: 'Specify a model directly: {format}',
},
directDownloadFailed: {
id: 'huggingFaceModelSearch.directDownloadFailed',
defaultMessage: 'Direct download failed',
},
directDownloadErrorMsg: {
id: 'huggingFaceModelSearch.directDownloadErrorMsg',
defaultMessage: 'Failed to start the download. Check the spec: {error}',
},
noGgufModels: {
id: 'huggingFaceModelSearch.noGgufModels',
defaultMessage: 'No GGUF models found for this query.',
},
searchError: {
id: 'huggingFaceModelSearch.searchError',
defaultMessage: 'Search error: {details}',
},
searchNoData: {
id: 'huggingFaceModelSearch.searchNoData',
defaultMessage: 'Search returned no data.',
},
searchFailed: {
id: 'huggingFaceModelSearch.searchFailed',
defaultMessage: 'Search failed. Please try again.',
},
});
const formatBytes = (bytes: number): string => {
if (bytes === 0) return 'unknown';
@@ -35,6 +91,7 @@ interface Props {
}
export const HuggingFaceModelSearch = ({ onDownloadStarted }: Props) => {
const intl = useIntl();
const [query, setQuery] = useState('');
const [results, setResults] = useState<HfModelInfo[]>([]);
const [expandedRepo, setExpandedRepo] = useState<string | null>(null);
@@ -93,22 +150,22 @@ export const HuggingFaceModelSearch = ({ onDownloadStarted }: Props) => {
});
if (validResults.length === 0) {
setError('No GGUF models found for this query.');
setError(intl.formatMessage(i18n.noGgufModels));
}
} else {
console.error('Search response:', response);
const errMsg = response.error
? `Search error: ${JSON.stringify(response.error)}`
: 'Search returned no data.';
? intl.formatMessage(i18n.searchError, { details: JSON.stringify(response.error) })
: intl.formatMessage(i18n.searchNoData);
setError(errMsg);
}
} catch (e) {
console.error('Search failed:', e);
setError('Search failed. Please try again.');
setError(intl.formatMessage(i18n.searchFailed));
} finally {
setSearching(false);
}
}, []);
}, [intl]);
const handleQueryChange = (value: string) => {
setQuery(value);
@@ -189,8 +246,8 @@ export const HuggingFaceModelSearch = ({ onDownloadStarted }: Props) => {
}
} catch (e) {
toastError({
title: 'Direct download failed',
msg: 'Failed to start the download. Check the spec: ' + errorMessage(e),
title: intl.formatMessage(i18n.directDownloadFailed),
msg: intl.formatMessage(i18n.directDownloadErrorMsg, { error: errorMessage(e) }),
});
} finally {
setDownloading((prev) => {
@@ -204,14 +261,14 @@ export const HuggingFaceModelSearch = ({ onDownloadStarted }: Props) => {
return (
<div className="space-y-4">
<div>
<h4 className="text-sm font-medium text-text-default mb-2">Search HuggingFace</h4>
<h4 className="text-sm font-medium text-text-default mb-2">{intl.formatMessage(i18n.searchHuggingFace)}</h4>
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted" />
<input
type="text"
value={query}
onChange={(e) => handleQueryChange(e.target.value)}
placeholder="Search for GGUF models..."
placeholder={intl.formatMessage(i18n.searchPlaceholder)}
className="w-full pl-9 pr-4 py-2 text-sm border border-border-subtle rounded-lg bg-background-default text-text-default placeholder:text-text-muted focus:outline-none focus:border-accent-primary"
/>
{searching && (
@@ -260,7 +317,7 @@ export const HuggingFaceModelSearch = ({ onDownloadStarted }: Props) => {
{loadingFiles.has(model.repo_id) && (
<div className="flex items-center gap-2 py-2 text-xs text-text-muted">
<Loader2 className="w-3 h-3 animate-spin" />
Loading variants...
{intl.formatMessage(i18n.loadingVariants)}
</div>
)}
{variants.map((variant, idx) => {
@@ -288,7 +345,7 @@ export const HuggingFaceModelSearch = ({ onDownloadStarted }: Props) => {
{isRecommended && (
<span className="inline-flex items-center gap-1 text-xs bg-blue-500 text-white px-1.5 py-0.5 rounded">
<Star className="w-3 h-3" />
Recommended
{intl.formatMessage(i18n.recommended)}
</span>
)}
</div>
@@ -307,7 +364,7 @@ export const HuggingFaceModelSearch = ({ onDownloadStarted }: Props) => {
) : (
<>
<Download className="w-3 h-3 mr-1" />
Download
{intl.formatMessage(i18n.download)}
</>
)}
</Button>
@@ -323,10 +380,11 @@ export const HuggingFaceModelSearch = ({ onDownloadStarted }: Props) => {
)}
<div>
<h4 className="text-sm font-medium text-text-default mb-2">Direct Download</h4>
<h4 className="text-sm font-medium text-text-default mb-2">{intl.formatMessage(i18n.directDownload)}</h4>
<p className="text-xs text-text-muted mb-2">
Specify a model directly:{' '}
<code className="bg-background-subtle px-1 rounded">user/repo:quantization</code>
{intl.formatMessage(i18n.directDownloadDescription, {
format: 'user/repo:quantization',
})}
</p>
<div className="flex gap-2">
<input
@@ -350,7 +408,7 @@ export const HuggingFaceModelSearch = ({ onDownloadStarted }: Props) => {
) : (
<>
<Download className="w-4 h-4 mr-1" />
Download
{intl.formatMessage(i18n.download)}
</>
)}
</Button>
@@ -2,6 +2,7 @@ import { useState, useEffect, useCallback, useRef } from 'react';
import { Download, Trash2, X, ChevronDown, ChevronUp, Settings2 } from 'lucide-react';
import { Button } from '../../ui/button';
import { useModelAndProvider } from '../../ModelAndProviderContext';
import { defineMessages, useIntl } from '../../../i18n';
import {
listLocalModels,
downloadHfModel,
@@ -16,6 +17,74 @@ import { HuggingFaceModelSearch } from './HuggingFaceModelSearch';
import { ModelSettingsPanel } from './ModelSettingsPanel';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../../ui/dialog';
const i18n = defineMessages({
title: {
id: 'localInferenceSettings.title',
defaultMessage: 'Local Inference Models',
},
description: {
id: 'localInferenceSettings.description',
defaultMessage:
'Download and manage local LLM models for inference without API keys. Search HuggingFace for any GGUF model or use the featured picks below.',
},
downloading: {
id: 'localInferenceSettings.downloading',
defaultMessage: 'Downloading',
},
downloadedModels: {
id: 'localInferenceSettings.downloadedModels',
defaultMessage: 'Downloaded Models',
},
featuredModels: {
id: 'localInferenceSettings.featuredModels',
defaultMessage: 'Featured Models',
},
recommended: {
id: 'localInferenceSettings.recommended',
defaultMessage: 'Recommended',
},
download: {
id: 'localInferenceSettings.download',
defaultMessage: 'Download',
},
showRecommendedOnly: {
id: 'localInferenceSettings.showRecommendedOnly',
defaultMessage: 'Show recommended only',
},
showAllFeatured: {
id: 'localInferenceSettings.showAllFeatured',
defaultMessage: 'Show all featured ({count} more)',
},
modelSettings: {
id: 'localInferenceSettings.modelSettings',
defaultMessage: 'Model Settings',
},
noModels: {
id: 'localInferenceSettings.noModels',
defaultMessage: 'No models available',
},
downloadProgress: {
id: 'localInferenceSettings.downloadProgress',
defaultMessage: '{downloaded} / {total} ({percent}%)',
},
remaining: {
id: 'localInferenceSettings.remaining',
defaultMessage: '{time} remaining',
},
downloadFailed: {
id: 'localInferenceSettings.downloadFailed',
defaultMessage: 'Download failed',
},
deleteConfirm: {
id: 'localInferenceSettings.deleteConfirm',
defaultMessage: 'Delete this model? You can re-download it later.',
},
modelSettingsTitle: {
id: 'localInferenceSettings.modelSettingsTitle',
defaultMessage: 'Model settings',
},
});
const formatBytes = (bytes: number): string => {
if (bytes < 1024) return `${bytes}B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)}KB`;
@@ -24,6 +93,7 @@ const formatBytes = (bytes: number): string => {
};
export const LocalInferenceSettings = () => {
const intl = useIntl();
const [models, setModels] = useState<LocalModelResponse[]>([]);
const [downloads, setDownloads] = useState<Map<string, DownloadProgress>>(new Map());
const [showAllFeatured, setShowAllFeatured] = useState(false);
@@ -146,7 +216,7 @@ export const LocalInferenceSettings = () => {
};
const handleDeleteModel = async (modelId: string) => {
if (!window.confirm('Delete this model? You can re-download it later.')) return;
if (!window.confirm(intl.formatMessage(i18n.deleteConfirm))) return;
try {
await deleteLocalModel({ path: { model_id: modelId } });
const updatedModels = await loadModels();
@@ -183,17 +253,16 @@ export const LocalInferenceSettings = () => {
return (
<div className="space-y-6">
<div>
<h3 className="text-text-default font-medium">Local Inference Models</h3>
<h3 className="text-text-default font-medium">{intl.formatMessage(i18n.title)}</h3>
<p className="text-xs text-text-muted max-w-2xl mt-1">
Download and manage local LLM models for inference without API keys. Search HuggingFace
for any GGUF model or use the featured picks below.
{intl.formatMessage(i18n.description)}
</p>
</div>
{/* Active Downloads */}
{downloads.size > 0 && (
<div ref={downloadSectionRef}>
<h4 className="text-sm font-medium text-text-default mb-2">Downloading</h4>
<h4 className="text-sm font-medium text-text-default mb-2">{intl.formatMessage(i18n.downloading)}</h4>
<div className="space-y-2">
{Array.from(downloads.entries()).map(([modelId, progress]) => {
if (progress.status === 'completed') return null;
@@ -227,17 +296,21 @@ export const LocalInferenceSettings = () => {
</div>
<div className="flex justify-between text-xs text-text-muted">
<span>
{formatBytes(progress.bytes_downloaded)} /{' '}
{formatBytes(progress.total_bytes)} (
{progress.progress_percent.toFixed(0)}%)
{intl.formatMessage(i18n.downloadProgress, {
downloaded: formatBytes(progress.bytes_downloaded),
total: formatBytes(progress.total_bytes),
percent: progress.progress_percent.toFixed(0),
})}
</span>
<span className="flex gap-2">
{progress.eta_seconds != null && progress.eta_seconds > 0 && (
<span>
{progress.eta_seconds < 60
? `${Math.round(progress.eta_seconds)}s`
: `${Math.round(progress.eta_seconds / 60)}m`}{' '}
remaining
{intl.formatMessage(i18n.remaining, {
time:
progress.eta_seconds < 60
? `${Math.round(progress.eta_seconds)}s`
: `${Math.round(progress.eta_seconds / 60)}m`,
})}
</span>
)}
{progress.speed_bps != null && progress.speed_bps > 0 && (
@@ -249,7 +322,7 @@ export const LocalInferenceSettings = () => {
)}
{progress.status === 'failed' && (
<p className="text-xs text-destructive">
{progress.error || 'Download failed'}
{progress.error || intl.formatMessage(i18n.downloadFailed)}
</p>
)}
</div>
@@ -262,7 +335,7 @@ export const LocalInferenceSettings = () => {
{/* Downloaded Models */}
{downloadedModels.length > 0 && (
<div>
<h4 className="text-sm font-medium text-text-default mb-2">Downloaded Models</h4>
<h4 className="text-sm font-medium text-text-default mb-2">{intl.formatMessage(i18n.downloadedModels)}</h4>
<div className="space-y-2">
{downloadedModels.map((model) => {
const isSelected = selectedModelId === model.id;
@@ -289,7 +362,7 @@ export const LocalInferenceSettings = () => {
</span>
{model.recommended && (
<span className="text-xs bg-blue-500 text-white px-2 py-0.5 rounded">
Recommended
{intl.formatMessage(i18n.recommended)}
</span>
)}
</div>
@@ -298,7 +371,7 @@ export const LocalInferenceSettings = () => {
variant="ghost"
size="sm"
onClick={() => setSettingsOpenFor(model.id)}
title="Model settings"
title={intl.formatMessage(i18n.modelSettingsTitle)}
>
<Settings2 className="w-4 h-4" />
</Button>
@@ -322,7 +395,7 @@ export const LocalInferenceSettings = () => {
{/* Featured Models (not yet downloaded) */}
{displayedFeatured.length > 0 && (
<div>
<h4 className="text-sm font-medium text-text-default mb-2">Featured Models</h4>
<h4 className="text-sm font-medium text-text-default mb-2">{intl.formatMessage(i18n.featuredModels)}</h4>
<div className="space-y-2">
{displayedFeatured.map((model) => (
<div
@@ -338,7 +411,7 @@ export const LocalInferenceSettings = () => {
</span>
{model.recommended && (
<span className="text-xs bg-blue-500 text-white px-2 py-0.5 rounded">
Recommended
{intl.formatMessage(i18n.recommended)}
</span>
)}
</div>
@@ -349,7 +422,7 @@ export const LocalInferenceSettings = () => {
onClick={() => startFeaturedDownload(model.id)}
>
<Download className="w-4 h-4 mr-1" />
Download
{intl.formatMessage(i18n.download)}
</Button>
</div>
</div>
@@ -366,12 +439,14 @@ export const LocalInferenceSettings = () => {
{showAllFeatured ? (
<>
<ChevronUp className="w-4 h-4 mr-1" />
Show recommended only
{intl.formatMessage(i18n.showRecommendedOnly)}
</>
) : (
<>
<ChevronDown className="w-4 h-4 mr-1" />
Show all featured ({notDownloadedModels.length - displayedFeatured.length} more)
{intl.formatMessage(i18n.showAllFeatured, {
count: notDownloadedModels.length - displayedFeatured.length,
})}
</>
)}
</Button>
@@ -385,7 +460,7 @@ export const LocalInferenceSettings = () => {
</div>
{models.length === 0 && (
<div className="text-center py-6 text-text-muted text-sm">No models available</div>
<div className="text-center py-6 text-text-muted text-sm">{intl.formatMessage(i18n.noModels)}</div>
)}
<Dialog
@@ -396,7 +471,7 @@ export const LocalInferenceSettings = () => {
>
<DialogContent className="max-h-[80vh] overflow-y-auto sm:max-w-xl">
<DialogHeader>
<DialogTitle>Model Settings</DialogTitle>
<DialogTitle>{intl.formatMessage(i18n.modelSettings)}</DialogTitle>
<p className="text-sm text-text-muted">{settingsOpenFor || ''}</p>
</DialogHeader>
{settingsOpenFor && <ModelSettingsPanel modelId={settingsOpenFor} />}
@@ -8,6 +8,171 @@ import {
type ModelSettings,
type SamplingConfig,
} from '../../../api';
import { defineMessages, useIntl } from '../../../i18n';
const i18n = defineMessages({
loadingSettings: {
id: 'modelSettingsPanel.loadingSettings',
defaultMessage: 'Loading settings...',
},
saving: {
id: 'modelSettingsPanel.saving',
defaultMessage: 'Saving...',
},
reset: {
id: 'modelSettingsPanel.reset',
defaultMessage: 'Reset',
},
resetToDefaults: {
id: 'modelSettingsPanel.resetToDefaults',
defaultMessage: 'Reset to defaults',
},
contextAndGeneration: {
id: 'modelSettingsPanel.contextAndGeneration',
defaultMessage: 'Context & Generation',
},
contextSize: {
id: 'modelSettingsPanel.contextSize',
defaultMessage: 'Context size',
},
contextSizeDescription: {
id: 'modelSettingsPanel.contextSizeDescription',
defaultMessage: 'Max context window (0 = model default)',
},
maxOutputTokens: {
id: 'modelSettingsPanel.maxOutputTokens',
defaultMessage: 'Max output tokens',
},
maxOutputTokensDescription: {
id: 'modelSettingsPanel.maxOutputTokensDescription',
defaultMessage: 'Cap on generated tokens',
},
samplingStrategy: {
id: 'modelSettingsPanel.samplingStrategy',
defaultMessage: 'Sampling Strategy',
},
temperature: {
id: 'modelSettingsPanel.temperature',
defaultMessage: 'Temperature',
},
topK: {
id: 'modelSettingsPanel.topK',
defaultMessage: 'Top K',
},
topP: {
id: 'modelSettingsPanel.topP',
defaultMessage: 'Top P',
},
minP: {
id: 'modelSettingsPanel.minP',
defaultMessage: 'Min P',
},
seed: {
id: 'modelSettingsPanel.seed',
defaultMessage: 'Seed',
},
tauTargetEntropy: {
id: 'modelSettingsPanel.tauTargetEntropy',
defaultMessage: 'Tau (target entropy)',
},
etaLearningRate: {
id: 'modelSettingsPanel.etaLearningRate',
defaultMessage: 'Eta (learning rate)',
},
repetitionPenalty: {
id: 'modelSettingsPanel.repetitionPenalty',
defaultMessage: 'Repetition Penalty',
},
repeatPenalty: {
id: 'modelSettingsPanel.repeatPenalty',
defaultMessage: 'Repeat penalty',
},
repeatPenaltyDescription: {
id: 'modelSettingsPanel.repeatPenaltyDescription',
defaultMessage: '1.0 = off',
},
repeatWindow: {
id: 'modelSettingsPanel.repeatWindow',
defaultMessage: 'Repeat window',
},
repeatWindowDescription: {
id: 'modelSettingsPanel.repeatWindowDescription',
defaultMessage: 'Tokens to look back',
},
frequencyPenalty: {
id: 'modelSettingsPanel.frequencyPenalty',
defaultMessage: 'Frequency penalty',
},
frequencyPenaltyDescription: {
id: 'modelSettingsPanel.frequencyPenaltyDescription',
defaultMessage: '0.0 = off',
},
presencePenalty: {
id: 'modelSettingsPanel.presencePenalty',
defaultMessage: 'Presence penalty',
},
presencePenaltyDescription: {
id: 'modelSettingsPanel.presencePenaltyDescription',
defaultMessage: '0.0 = off',
},
performance: {
id: 'modelSettingsPanel.performance',
defaultMessage: 'Performance',
},
batchSize: {
id: 'modelSettingsPanel.batchSize',
defaultMessage: 'Batch size',
},
batchSizeDescription: {
id: 'modelSettingsPanel.batchSizeDescription',
defaultMessage: 'Prompt processing batch',
},
gpuLayers: {
id: 'modelSettingsPanel.gpuLayers',
defaultMessage: 'GPU layers',
},
gpuLayersDescription: {
id: 'modelSettingsPanel.gpuLayersDescription',
defaultMessage: 'Layers to offload to GPU',
},
threads: {
id: 'modelSettingsPanel.threads',
defaultMessage: 'Threads',
},
threadsDescription: {
id: 'modelSettingsPanel.threadsDescription',
defaultMessage: 'CPU threads for generation',
},
lockModelInRam: {
id: 'modelSettingsPanel.lockModelInRam',
defaultMessage: 'Lock model in RAM (mlock)',
},
lockModelInRamDescription: {
id: 'modelSettingsPanel.lockModelInRamDescription',
defaultMessage: 'Prevent model from being swapped to disk',
},
flashAttention: {
id: 'modelSettingsPanel.flashAttention',
defaultMessage: 'Flash attention',
},
flashAttentionDescription: {
id: 'modelSettingsPanel.flashAttentionDescription',
defaultMessage: 'Enable flash attention optimization',
},
toolCalling: {
id: 'modelSettingsPanel.toolCalling',
defaultMessage: 'Tool Calling',
},
nativeToolCalling: {
id: 'modelSettingsPanel.nativeToolCalling',
defaultMessage: 'Native tool calling',
},
nativeToolCallingDescription: {
id: 'modelSettingsPanel.nativeToolCallingDescription',
defaultMessage:
"Use the model's built-in tool-call format instead of the shell-command emulator. Enable for large models that reliably support tool calling.",
},
});
const DEFAULT_SETTINGS: ModelSettings = {
context_size: null,
@@ -138,6 +303,7 @@ function SelectField<T extends string>({
}
export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
const intl = useIntl();
const [settings, setSettings] = useState<ModelSettings>(DEFAULT_SETTINGS);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
@@ -201,26 +367,26 @@ export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
};
if (loading) {
return <div className="py-2 text-xs text-text-muted">Loading settings...</div>;
return <div className="py-2 text-xs text-text-muted">{intl.formatMessage(i18n.loadingSettings)}</div>;
}
return (
<div className="space-y-4">
<div className="flex items-center justify-end">
{saving && <span className="text-xs text-text-muted mr-auto">Saving...</span>}
<Button variant="ghost" size="sm" onClick={resetDefaults} title="Reset to defaults">
{saving && <span className="text-xs text-text-muted mr-auto">{intl.formatMessage(i18n.saving)}</span>}
<Button variant="ghost" size="sm" onClick={resetDefaults} title={intl.formatMessage(i18n.resetToDefaults)}>
<RotateCcw className="w-3.5 h-3.5 mr-1" />
<span className="text-xs">Reset</span>
<span className="text-xs">{intl.formatMessage(i18n.reset)}</span>
</Button>
</div>
{/* Context & Generation */}
<div className="space-y-2">
<h5 className="text-xs font-medium text-text-default">Context & Generation</h5>
<h5 className="text-xs font-medium text-text-default">{intl.formatMessage(i18n.contextAndGeneration)}</h5>
<div className="grid grid-cols-2 gap-3">
<NumberField
label="Context size"
description="Max context window (0 = model default)"
label={intl.formatMessage(i18n.contextSize)}
description={intl.formatMessage(i18n.contextSizeDescription)}
value={settings.context_size}
onChange={(v) => updateField('context_size', v)}
placeholder="Auto"
@@ -228,8 +394,8 @@ export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
allowNull
/>
<NumberField
label="Max output tokens"
description="Cap on generated tokens"
label={intl.formatMessage(i18n.maxOutputTokens)}
description={intl.formatMessage(i18n.maxOutputTokensDescription)}
value={settings.max_output_tokens}
onChange={(v) => updateField('max_output_tokens', v)}
placeholder="No limit"
@@ -242,7 +408,7 @@ export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
{/* Sampling */}
<div className="space-y-2">
<SelectField
label="Sampling Strategy"
label={intl.formatMessage(i18n.samplingStrategy)}
value={samplingType}
options={[
{ value: 'Greedy' as SamplingType, label: 'Greedy' },
@@ -255,7 +421,7 @@ export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
{samplingType === 'Temperature' && settings.sampling?.type === 'Temperature' && (
<div className="grid grid-cols-2 gap-3">
<NumberField
label="Temperature"
label={intl.formatMessage(i18n.temperature)}
value={settings.sampling.temperature}
onChange={(v) => updateSampling({ temperature: v ?? 0.8 })}
min={0}
@@ -263,13 +429,13 @@ export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
step={0.05}
/>
<NumberField
label="Top K"
label={intl.formatMessage(i18n.topK)}
value={settings.sampling.top_k}
onChange={(v) => updateSampling({ top_k: v ?? 40 })}
min={0}
/>
<NumberField
label="Top P"
label={intl.formatMessage(i18n.topP)}
value={settings.sampling.top_p}
onChange={(v) => updateSampling({ top_p: v ?? 0.95 })}
min={0}
@@ -277,7 +443,7 @@ export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
step={0.01}
/>
<NumberField
label="Min P"
label={intl.formatMessage(i18n.minP)}
value={settings.sampling.min_p}
onChange={(v) => updateSampling({ min_p: v ?? 0.05 })}
min={0}
@@ -285,7 +451,7 @@ export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
step={0.01}
/>
<NumberField
label="Seed"
label={intl.formatMessage(i18n.seed)}
value={settings.sampling.seed}
onChange={(v) => updateSampling({ seed: v })}
placeholder="Random"
@@ -298,14 +464,14 @@ export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
{samplingType === 'MirostatV2' && settings.sampling?.type === 'MirostatV2' && (
<div className="grid grid-cols-2 gap-3">
<NumberField
label="Tau (target entropy)"
label={intl.formatMessage(i18n.tauTargetEntropy)}
value={settings.sampling.tau}
onChange={(v) => updateSampling({ tau: v ?? 5.0 })}
min={0}
step={0.1}
/>
<NumberField
label="Eta (learning rate)"
label={intl.formatMessage(i18n.etaLearningRate)}
value={settings.sampling.eta}
onChange={(v) => updateSampling({ eta: v ?? 0.1 })}
min={0}
@@ -313,7 +479,7 @@ export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
step={0.01}
/>
<NumberField
label="Seed"
label={intl.formatMessage(i18n.seed)}
value={settings.sampling.seed}
onChange={(v) => updateSampling({ seed: v })}
placeholder="Random"
@@ -326,26 +492,26 @@ export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
{/* Repetition Penalty */}
<div className="space-y-2">
<h5 className="text-xs font-medium text-text-default">Repetition Penalty</h5>
<h5 className="text-xs font-medium text-text-default">{intl.formatMessage(i18n.repetitionPenalty)}</h5>
<div className="grid grid-cols-2 gap-3">
<NumberField
label="Repeat penalty"
description="1.0 = off"
label={intl.formatMessage(i18n.repeatPenalty)}
description={intl.formatMessage(i18n.repeatPenaltyDescription)}
value={settings.repeat_penalty}
onChange={(v) => updateField('repeat_penalty', v ?? 1.0)}
min={0}
step={0.05}
/>
<NumberField
label="Repeat window"
description="Tokens to look back"
label={intl.formatMessage(i18n.repeatWindow)}
description={intl.formatMessage(i18n.repeatWindowDescription)}
value={settings.repeat_last_n}
onChange={(v) => updateField('repeat_last_n', v ?? 64)}
min={0}
/>
<NumberField
label="Frequency penalty"
description="0.0 = off"
label={intl.formatMessage(i18n.frequencyPenalty)}
description={intl.formatMessage(i18n.frequencyPenaltyDescription)}
value={settings.frequency_penalty}
onChange={(v) => updateField('frequency_penalty', v ?? 0.0)}
min={0}
@@ -353,8 +519,8 @@ export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
step={0.05}
/>
<NumberField
label="Presence penalty"
description="0.0 = off"
label={intl.formatMessage(i18n.presencePenalty)}
description={intl.formatMessage(i18n.presencePenaltyDescription)}
value={settings.presence_penalty}
onChange={(v) => updateField('presence_penalty', v ?? 0.0)}
min={0}
@@ -366,11 +532,11 @@ export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
{/* Performance */}
<div className="space-y-2">
<h5 className="text-xs font-medium text-text-default">Performance</h5>
<h5 className="text-xs font-medium text-text-default">{intl.formatMessage(i18n.performance)}</h5>
<div className="grid grid-cols-2 gap-3">
<NumberField
label="Batch size"
description="Prompt processing batch"
label={intl.formatMessage(i18n.batchSize)}
description={intl.formatMessage(i18n.batchSizeDescription)}
value={settings.n_batch}
onChange={(v) => updateField('n_batch', v)}
placeholder="Auto"
@@ -378,8 +544,8 @@ export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
allowNull
/>
<NumberField
label="GPU layers"
description="Layers to offload to GPU"
label={intl.formatMessage(i18n.gpuLayers)}
description={intl.formatMessage(i18n.gpuLayersDescription)}
value={settings.n_gpu_layers}
onChange={(v) => updateField('n_gpu_layers', v)}
placeholder="All"
@@ -387,8 +553,8 @@ export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
allowNull
/>
<NumberField
label="Threads"
description="CPU threads for generation"
label={intl.formatMessage(i18n.threads)}
description={intl.formatMessage(i18n.threadsDescription)}
value={settings.n_threads}
onChange={(v) => updateField('n_threads', v)}
placeholder="Auto"
@@ -397,14 +563,14 @@ export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
/>
</div>
<ToggleField
label="Lock model in RAM (mlock)"
description="Prevent model from being swapped to disk"
label={intl.formatMessage(i18n.lockModelInRam)}
description={intl.formatMessage(i18n.lockModelInRamDescription)}
value={settings.use_mlock ?? false}
onChange={(v) => updateField('use_mlock', v)}
/>
<SelectField
label="Flash attention"
description="Enable flash attention optimization"
label={intl.formatMessage(i18n.flashAttention)}
description={intl.formatMessage(i18n.flashAttentionDescription)}
value={
settings.flash_attention === null || settings.flash_attention === undefined
? 'auto'
@@ -422,10 +588,10 @@ export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
</div>
{/* Tool Calling */}
<div className="space-y-2">
<h5 className="text-xs font-medium text-text-default">Tool Calling</h5>
<h5 className="text-xs font-medium text-text-default">{intl.formatMessage(i18n.toolCalling)}</h5>
<ToggleField
label="Native tool calling"
description="Use the model's built-in tool-call format instead of the shell-command emulator. Enable for large models that reliably support tool calling."
label={intl.formatMessage(i18n.nativeToolCalling)}
description={intl.formatMessage(i18n.nativeToolCallingDescription)}
value={settings.native_tool_calling ?? false}
onChange={(v) => updateField('native_tool_calling', v)}
/>