feat(desktop): add i18n infrastructure with react-intl (#8105)
This commit is contained in:
@@ -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)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user