feat: add credential priority controls and OAuth defaults
This commit is contained in:
1 parent
0f3e5093e8
commit
bbe22a96eb
13 files changed
+582
-416
No files matched your search
@@ -85,10 +85,6 @@ type AgentConfigActionResult = {
|
||||
conflictFiles: string[];
|
||||
};
|
||||
|
||||
type CodexModelCatalogUpdateResult = {
|
||||
outcome: 'updated' | 'unchanged';
|
||||
};
|
||||
|
||||
type ChatGptCloseResult = {
|
||||
wasRunning: boolean;
|
||||
closedProcesses: number;
|
||||
@@ -488,13 +484,11 @@ export function AgentsPage() {
|
||||
const [launchTargetByClient, setLaunchTargetByClient] = useState<Partial<Record<AgentClientId, string>>>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [modelLoading, setModelLoading] = useState(false);
|
||||
const [busyAction, setBusyAction] = useState<'apply' | 'default' | 'clear' | 'launch' | 'close-app' | 'catalog' | null>(null);
|
||||
const [busyAction, setBusyAction] = useState<'apply' | 'default' | 'clear' | 'launch' | 'close-app' | null>(null);
|
||||
const busy = busyAction !== null;
|
||||
const [detectionError, setDetectionError] = useState('');
|
||||
const [modelError, setModelError] = useState('');
|
||||
const [modelSelectionError, setModelSelectionError] = useState('');
|
||||
const [catalogUpdateError, setCatalogUpdateError] = useState('');
|
||||
const [catalogUpdateNotice, setCatalogUpdateNotice] = useState('');
|
||||
const [configurationError, setConfigurationError] = useState('');
|
||||
const [launchError, setLaunchError] = useState('');
|
||||
const [defaultError, setDefaultError] = useState('');
|
||||
@@ -592,8 +586,6 @@ export function AgentsPage() {
|
||||
useEffect(() => {
|
||||
setActiveSubpage(DEFAULT_AGENT_SUBPAGE);
|
||||
setModelSelectionError('');
|
||||
setCatalogUpdateError('');
|
||||
setCatalogUpdateNotice('');
|
||||
setConfigurationError('');
|
||||
setLaunchError('');
|
||||
setDefaultError('');
|
||||
@@ -688,23 +680,6 @@ export function AgentsPage() {
|
||||
void loadModels(selected);
|
||||
};
|
||||
|
||||
const updateCodexModelCatalog = async () => {
|
||||
setCatalogUpdateError('');
|
||||
setCatalogUpdateNotice('');
|
||||
setBusyAction('catalog');
|
||||
try {
|
||||
const result = await invoke<CodexModelCatalogUpdateResult>('update_codex_model_catalog');
|
||||
setCatalogUpdateNotice(result.outcome === 'updated'
|
||||
? t('agents.catalog.updated')
|
||||
: t('agents.catalog.unchanged'));
|
||||
await loadModels('codex');
|
||||
} catch (requestError) {
|
||||
setCatalogUpdateError(String(requestError));
|
||||
} finally {
|
||||
setBusyAction(null);
|
||||
}
|
||||
};
|
||||
|
||||
const reloadStatusesAfterAction = async () => {
|
||||
setDetectionError('');
|
||||
try {
|
||||
@@ -989,31 +964,6 @@ export function AgentsPage() {
|
||||
{modelHint}
|
||||
</span>
|
||||
) : null}
|
||||
{selected === 'codex' ? (
|
||||
<div className="agent-catalog-update-control">
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button compact-button"
|
||||
onClick={() => void updateCodexModelCatalog()}
|
||||
disabled={busy}
|
||||
>
|
||||
<RefreshCw size={15} className={busyAction === 'catalog' ? 'spin' : ''} />
|
||||
{busyAction === 'catalog'
|
||||
? t('agents.catalog.updating')
|
||||
: t('agents.catalog.update')}
|
||||
</button>
|
||||
{catalogUpdateError ? (
|
||||
<span className="agent-inline-message error" role="alert" aria-live="polite">
|
||||
{catalogUpdateError}
|
||||
</span>
|
||||
) : null}
|
||||
{catalogUpdateNotice ? (
|
||||
<span className="agent-inline-message" role="status" aria-live="polite">
|
||||
{catalogUpdateNotice}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<section className={`agent-core-setting-section agent-modification-actions ${activeStatus?.modificationState === 'applied' ? 'enabled' : ''}`}>
|
||||
|
||||
@@ -2,9 +2,10 @@ import { ChangeEvent, useCallback, useEffect, useMemo, useRef, useState } from '
|
||||
import {
|
||||
Check,
|
||||
Copy,
|
||||
Download,
|
||||
FileDown,
|
||||
FolderOpen,
|
||||
LoaderCircle,
|
||||
Pencil,
|
||||
RefreshCw,
|
||||
Search,
|
||||
Trash2,
|
||||
@@ -45,6 +46,8 @@ import {
|
||||
authFileName,
|
||||
dedupeAuthFiles,
|
||||
isRuntimeOnlyAuthFile,
|
||||
normalizeAuthFilePriorityInput,
|
||||
parseAuthFilePriority,
|
||||
} from '../services/authFiles';
|
||||
import {
|
||||
exclusionsForOpenOAuthModels,
|
||||
@@ -57,6 +60,13 @@ import { getCurrentLocale, translate, useI18n } from '../i18n';
|
||||
|
||||
type AuthFile = Record<string, unknown>;
|
||||
|
||||
type PriorityEditor = {
|
||||
fileName: string;
|
||||
originalPriority: number;
|
||||
value: string;
|
||||
error: string;
|
||||
};
|
||||
|
||||
const providerIcons: Record<string, string> = {
|
||||
antigravity: antigravityIcon,
|
||||
claude: claudeIcon,
|
||||
@@ -144,6 +154,7 @@ export function AuthFileManagementPage() {
|
||||
const [error, setError] = useState('');
|
||||
const [notice, setNotice] = useState('');
|
||||
const [copied, setCopied] = useState('');
|
||||
const [priorityEditor, setPriorityEditor] = useState<PriorityEditor | null>(null);
|
||||
const [oauthModelProvider, setOauthModelProvider] = useState('');
|
||||
const [oauthModelProviderLabel, setOauthModelProviderLabel] = useState('');
|
||||
const [oauthModels, setOauthModels] = useState<OAuthModelDefinition[]>([]);
|
||||
@@ -361,6 +372,51 @@ export function AuthFileManagementPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const openPriorityEditor = (file: AuthFile) => {
|
||||
const priority = parseAuthFilePriority(file.priority);
|
||||
setPriorityEditor({
|
||||
fileName: fileName(file),
|
||||
originalPriority: priority ?? 0,
|
||||
value: priority === undefined || priority === 0 ? '' : String(priority),
|
||||
error: '',
|
||||
});
|
||||
};
|
||||
|
||||
const closePriorityEditor = () => {
|
||||
if (!busy) setPriorityEditor(null);
|
||||
};
|
||||
|
||||
const savePriority = async () => {
|
||||
if (!priorityEditor || busy) return;
|
||||
const priority = normalizeAuthFilePriorityInput(priorityEditor.value);
|
||||
if (priority === null) {
|
||||
setPriorityEditor((current) => current
|
||||
? { ...current, error: t('authFiles.priority.invalid') }
|
||||
: current);
|
||||
return;
|
||||
}
|
||||
if (priority === priorityEditor.originalPriority) {
|
||||
setPriorityEditor(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const name = priorityEditor.fileName;
|
||||
setBusy(true);
|
||||
setError('');
|
||||
try {
|
||||
await managementApi.patch('/auth-files/fields', { name, priority });
|
||||
setPriorityEditor(null);
|
||||
setNotice(t('authFiles.priority.updated', { name }));
|
||||
await loadFiles();
|
||||
} catch (requestError) {
|
||||
setPriorityEditor((current) => current
|
||||
? { ...current, error: String(requestError) }
|
||||
: current);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteFile = async (file: AuthFile) => {
|
||||
const name = fileName(file);
|
||||
if (isRuntimeOnly(file)) {
|
||||
@@ -396,23 +452,11 @@ export function AuthFileManagementPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const downloadFile = async (file: AuthFile) => {
|
||||
const name = fileName(file);
|
||||
if (isRuntimeOnly(file)) {
|
||||
setError(t('authFiles.runtimeDownloadError'));
|
||||
return;
|
||||
}
|
||||
const openAuthFilesDirectory = async () => {
|
||||
setBusy(true);
|
||||
setError('');
|
||||
try {
|
||||
const content = await managementApi.downloadAuthFile(name);
|
||||
const url = URL.createObjectURL(new Blob([content], { type: 'application/json' }));
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.download = name;
|
||||
anchor.click();
|
||||
URL.revokeObjectURL(url);
|
||||
setNotice(t('authFiles.downloaded'));
|
||||
await managementApi.openAuthFilesDirectory();
|
||||
} catch (requestError) {
|
||||
setError(String(requestError));
|
||||
} finally {
|
||||
@@ -485,6 +529,7 @@ export function AuthFileManagementPage() {
|
||||
const name = fileName(file);
|
||||
const icon = providerIcons[providerKey(file)] ?? geminiIcon;
|
||||
const disabled = readBoolean(file, 'disabled');
|
||||
const priority = parseAuthFilePriority(file.priority) ?? 0;
|
||||
return (
|
||||
<article className={`real-auth-file-row ${disabled ? 'disabled' : ''}`} key={`${name}-${readString(file, 'auth_index', 'authIndex')}`}>
|
||||
<img src={icon} alt="" className="provider-logo" />
|
||||
@@ -500,8 +545,9 @@ export function AuthFileManagementPage() {
|
||||
<div className="auth-file-actions">
|
||||
{quotaProviderForFile(file) ? <button type="button" className="secondary-button compact-button" onClick={() => void refreshQuota(file)} disabled={busy || disabled || quotas[quotaKey(file)]?.status === 'loading'}>{disabled ? t('authFiles.status.disabled') : quotas[quotaKey(file)]?.status === 'loading' ? t('authFiles.quota.querying') : quotas[quotaKey(file)]?.status === 'success' ? t('authFiles.quota.refresh') : t('authFiles.quota.fetch')}</button> : null}
|
||||
{providerKey(file) ? <button type="button" className="secondary-button compact-button" onClick={() => void openOauthModels(file)} disabled={busy} title={t('authFiles.models.settings')}>{t('authFiles.models.button')}</button> : null}
|
||||
<button type="button" className="secondary-button compact-button auth-file-priority-button" onClick={() => openPriorityEditor(file)} disabled={busy} title={t('authFiles.priority.hint')}><Pencil size={14} />{t('authFiles.priority.button', { priority })}</button>
|
||||
<button type="button" className="icon-button quiet" onClick={() => void copyName(name)} disabled={busy} title={t('authFiles.copyName')}>{copied === name ? <Check size={16} /> : <Copy size={16} />}</button>
|
||||
<button type="button" className="icon-button quiet" onClick={() => void downloadFile(file)} disabled={busy || isRuntimeOnly(file)} title={t('common.download')}><Download size={16} /></button>
|
||||
<button type="button" className="icon-button quiet" onClick={() => void openAuthFilesDirectory()} disabled={busy} title={t('authFiles.openDirectory')}><FolderOpen size={16} /></button>
|
||||
<button type="button" className="secondary-button compact-button" onClick={() => void toggleStatus(file)} disabled={busy}>{disabled ? t('common.enable') : t('common.disable')}</button>
|
||||
<button type="button" className="icon-button danger" onClick={() => void deleteFile(file)} disabled={busy || isRuntimeOnly(file)} title={t('common.delete')}><Trash2 size={16} /></button>
|
||||
</div>
|
||||
@@ -514,6 +560,51 @@ export function AuthFileManagementPage() {
|
||||
</section>
|
||||
{runtimeCount > 0 ? <p className="page-footnote">{t('authFiles.runtimeFootnote', { count: runtimeCount })}</p> : null}
|
||||
|
||||
{priorityEditor ? (
|
||||
<div className="config-dialog-backdrop" onMouseDown={(event) => event.currentTarget === event.target && closePriorityEditor()}>
|
||||
<form
|
||||
className="config-dialog auth-priority-dialog"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="auth-priority-title"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
void savePriority();
|
||||
}}
|
||||
>
|
||||
<div className="config-dialog-heading">
|
||||
<div><Pencil size={19} /><h2 id="auth-priority-title">{t('authFiles.priority.title')}</h2></div>
|
||||
<button type="button" className="icon-button quiet" onClick={closePriorityEditor} disabled={busy} title={t('common.close')}><X size={18} /></button>
|
||||
</div>
|
||||
<div className="auth-priority-target" title={priorityEditor.fileName}>{priorityEditor.fileName}</div>
|
||||
<label className="auth-priority-field" htmlFor="auth-file-priority-input">
|
||||
<span>{t('authFiles.priority.label')}</span>
|
||||
<input
|
||||
id="auth-file-priority-input"
|
||||
autoFocus
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
value={priorityEditor.value}
|
||||
placeholder={t('authFiles.priority.placeholder')}
|
||||
disabled={busy}
|
||||
aria-invalid={Boolean(priorityEditor.error)}
|
||||
onChange={(event) => setPriorityEditor((current) => current
|
||||
? { ...current, value: event.currentTarget.value, error: '' }
|
||||
: current)}
|
||||
/>
|
||||
<small>{t('authFiles.priority.hint')}</small>
|
||||
</label>
|
||||
<div className={`config-form-message ${priorityEditor.error ? 'error' : ''}`} role={priorityEditor.error ? 'alert' : undefined}>
|
||||
{priorityEditor.error || ' '}
|
||||
</div>
|
||||
<div className="config-dialog-actions two-actions">
|
||||
<button type="button" className="secondary-button" onClick={closePriorityEditor} disabled={busy}>{t('common.cancel')}</button>
|
||||
<button type="submit" className="primary-button" disabled={busy}>{busy ? <LoaderCircle size={16} className="spin" /> : <Check size={16} />}{busy ? t('common.saving') : t('common.save')}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{oauthModelProvider ? (
|
||||
<div className="model-discovery-backdrop" onMouseDown={(event) => event.currentTarget === event.target && !oauthModelSaving && closeOauthModels()}>
|
||||
<section className="model-discovery-dialog" role="dialog" aria-modal="true" aria-labelledby="oauth-model-title">
|
||||
|
||||
@@ -48,6 +48,10 @@ type CoreInstallTask = {
|
||||
result: CoreInstallResult | null;
|
||||
};
|
||||
|
||||
type CodexModelCatalogUpdateResult = {
|
||||
outcome: 'updated' | 'unchanged';
|
||||
};
|
||||
|
||||
type MessageType = 'info' | 'success' | 'error';
|
||||
type CoreProcessCommand = 'start_core_process' | 'stop_core_process' | 'restart_core_process';
|
||||
|
||||
@@ -131,6 +135,9 @@ export function KernelPage({ view = 'home' }: { view?: KernelView }) {
|
||||
} | null>(null);
|
||||
const [message, setMessage] = useState('');
|
||||
const [messageType, setMessageType] = useState<MessageType>('info');
|
||||
const [catalogUpdating, setCatalogUpdating] = useState(false);
|
||||
const [catalogUpdateError, setCatalogUpdateError] = useState('');
|
||||
const [catalogUpdateNotice, setCatalogUpdateNotice] = useState('');
|
||||
const [progress, setProgress] = useState<CoreInstallTask | null>(null);
|
||||
const [installDialogOpen, setInstallDialogOpen] = useState(false);
|
||||
const [cancellingInstall, setCancellingInstall] = useState(false);
|
||||
@@ -551,6 +558,22 @@ export function KernelPage({ view = 'home' }: { view?: KernelView }) {
|
||||
}
|
||||
};
|
||||
|
||||
const updateCodexModelCatalog = async () => {
|
||||
setCatalogUpdating(true);
|
||||
setCatalogUpdateError('');
|
||||
setCatalogUpdateNotice('');
|
||||
try {
|
||||
const result = await invoke<CodexModelCatalogUpdateResult>('update_codex_model_catalog');
|
||||
setCatalogUpdateNotice(result.outcome === 'updated'
|
||||
? t('appUpdate.catalog.updated')
|
||||
: t('appUpdate.catalog.unchanged'));
|
||||
} catch (error) {
|
||||
setCatalogUpdateError(String(error));
|
||||
} finally {
|
||||
setCatalogUpdating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const latestVersion = latest?.version ?? '';
|
||||
const currentVersion = coreStatus?.currentVersion ?? '';
|
||||
const coreInstalled = Boolean(coreStatus?.installed);
|
||||
@@ -779,6 +802,18 @@ export function KernelPage({ view = 'home' }: { view?: KernelView }) {
|
||||
: t('appUpdate.phase.checking')}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="panel-detail-row">
|
||||
<dt>{t('appUpdate.catalog.label')}</dt>
|
||||
<dd
|
||||
className={catalogUpdateError ? 'error' : catalogUpdateNotice ? 'success' : ''}
|
||||
role={catalogUpdateError ? 'alert' : undefined}
|
||||
aria-live="polite"
|
||||
>
|
||||
{catalogUpdateError
|
||||
|| catalogUpdateNotice
|
||||
|| t('appUpdate.catalog.description')}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<div className="button-row panel-action-row software-update-actions">
|
||||
@@ -804,6 +839,17 @@ export function KernelPage({ view = 'home' }: { view?: KernelView }) {
|
||||
{t('appUpdate.openRelease')} <ExternalLink size={14} aria-hidden="true" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button"
|
||||
disabled={catalogUpdating || appUpdateTask.running}
|
||||
onClick={() => void updateCodexModelCatalog()}
|
||||
title={t('appUpdate.catalog.updateHint')}
|
||||
>
|
||||
{catalogUpdating
|
||||
? t('appUpdate.catalog.updating')
|
||||
: t('appUpdate.catalog.update')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -13,6 +13,12 @@ import codexIcon from '../assets/icons/codex.svg';
|
||||
import grokIcon from '../assets/icons/grok.svg';
|
||||
import kimiIcon from '../assets/icons/kimi-light.svg';
|
||||
import { useI18n } from '../i18n';
|
||||
import {
|
||||
changedOAuthAuthFileNames,
|
||||
snapshotAuthFiles,
|
||||
type AuthFileSnapshot,
|
||||
} from '../services/authFiles';
|
||||
import { managementApi, responseList } from '../services/managementApi';
|
||||
|
||||
type OAuthProviderId = 'codex' | 'claude' | 'antigravity' | 'kimi' | 'xai';
|
||||
type OAuthFlowStatus = 'idle' | 'waiting' | 'success' | 'error';
|
||||
@@ -69,6 +75,7 @@ export function OAuthLoginPage() {
|
||||
const pollingTimers = useRef<Partial<Record<OAuthProviderId, number>>>({});
|
||||
const pollingRequests = useRef<Partial<Record<OAuthProviderId, boolean>>>({});
|
||||
const successResetTimers = useRef<Partial<Record<OAuthProviderId, number>>>({});
|
||||
const credentialSnapshots = useRef<Partial<Record<OAuthProviderId, AuthFileSnapshot>>>({});
|
||||
const noticeTimerRef = useRef<number | null>(null);
|
||||
|
||||
const showNotice = useCallback((message: string, tone: 'success' | 'error' | 'info') => {
|
||||
@@ -118,6 +125,7 @@ export function OAuthLoginPage() {
|
||||
const resetProviderAttempt = useCallback(
|
||||
(provider: OAuthProviderId) => {
|
||||
clearProviderTimers(provider);
|
||||
delete credentialSnapshots.current[provider];
|
||||
setStates((current) => ({ ...current, [provider]: { status: 'idle' } }));
|
||||
},
|
||||
[clearProviderTimers],
|
||||
@@ -144,6 +152,24 @@ export function OAuthLoginPage() {
|
||||
[clearProviderTimers, resetProviderAttempt, updateProviderState],
|
||||
);
|
||||
|
||||
const captureCredentialSnapshot = useCallback(async (provider: OAuthProviderId) => {
|
||||
const payload = await managementApi.get('/auth-files');
|
||||
credentialSnapshots.current[provider] = snapshotAuthFiles(responseList(payload, 'files'));
|
||||
}, []);
|
||||
|
||||
const applyDefaultCredentialPriority = useCallback(async (provider: OAuthProviderId) => {
|
||||
const before = credentialSnapshots.current[provider];
|
||||
delete credentialSnapshots.current[provider];
|
||||
if (!before) return;
|
||||
|
||||
const payload = await managementApi.get('/auth-files');
|
||||
const names = changedOAuthAuthFileNames(before, responseList(payload, 'files'), provider);
|
||||
await Promise.all(names.map((name) => managementApi.patch('/auth-files/fields', {
|
||||
name,
|
||||
priority: 0,
|
||||
})));
|
||||
}, []);
|
||||
|
||||
const startPolling = useCallback(
|
||||
(provider: OAuthProviderId, state: string) => {
|
||||
clearPollingTimer(provider);
|
||||
@@ -154,8 +180,19 @@ export function OAuthLoginPage() {
|
||||
const result = await invoke<OAuthStatusResult>('get_oauth_status', { state });
|
||||
const status = (result.status || '').toLowerCase();
|
||||
if (status === 'ok') {
|
||||
let priorityError = '';
|
||||
try {
|
||||
await applyDefaultCredentialPriority(provider);
|
||||
} catch (error) {
|
||||
priorityError = String(error);
|
||||
}
|
||||
completeProviderAuth(provider);
|
||||
showNotice(t('oauth.loginSuccess', { provider: providerLabel(provider) }), 'success');
|
||||
showNotice(
|
||||
priorityError
|
||||
? t('oauth.priorityApplyFailed', { error: priorityError })
|
||||
: t('oauth.loginSuccess', { provider: providerLabel(provider) }),
|
||||
priorityError ? 'error' : 'success',
|
||||
);
|
||||
} else if (status === 'error') {
|
||||
updateProviderState(provider, {
|
||||
status: 'error',
|
||||
@@ -188,7 +225,7 @@ export function OAuthLoginPage() {
|
||||
OAUTH_POLL_INTERVAL_MS,
|
||||
);
|
||||
},
|
||||
[clearPollingTimer, completeProviderAuth, showNotice, t, updateProviderState],
|
||||
[applyDefaultCredentialPriority, clearPollingTimer, completeProviderAuth, showNotice, t, updateProviderState],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -217,6 +254,7 @@ export function OAuthLoginPage() {
|
||||
});
|
||||
|
||||
try {
|
||||
await captureCredentialSnapshot(provider);
|
||||
const result = await invoke<OAuthStartResult>('start_oauth_login', { provider });
|
||||
if (!result.state) {
|
||||
updateProviderState(provider, {
|
||||
@@ -247,6 +285,7 @@ export function OAuthLoginPage() {
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
delete credentialSnapshots.current[provider];
|
||||
updateProviderState(provider, {
|
||||
status: 'error',
|
||||
error: String(error),
|
||||
|
||||
@@ -248,46 +248,23 @@ export function ThinkingAliasesPage() {
|
||||
|
||||
return (
|
||||
<section className="page management-page thinking-alias-page">
|
||||
<header className="management-header">
|
||||
<div>
|
||||
<span>Model Alias</span>
|
||||
<h1>{t('aliases.title')}</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="thinking-alias-feedback" aria-live="polite">
|
||||
{error ? <div className="management-alert error">{error}</div> : null}
|
||||
{!error && notice ? <div className="management-alert success">{notice}</div> : null}
|
||||
</div>
|
||||
|
||||
<div className="thinking-alias-guide">
|
||||
<BrainCircuit size={20} />
|
||||
<div className="thinking-alias-guide-copy">
|
||||
<strong>{t('aliases.intro.title')}</strong>
|
||||
<p>{t('aliases.intro.description')}</p>
|
||||
<span>{t('aliases.intro.badge')}</span>
|
||||
</div>
|
||||
<div className="thinking-alias-flow" aria-label={t('aliases.flow.aria')}>
|
||||
<span><small>{t('aliases.flow.original')}</small><code>deepseek-v4-pro</code></span>
|
||||
<ArrowRight size={14} />
|
||||
<div>
|
||||
<span><small>{t('aliases.flow.direct')}</small><code>{t('aliases.flow.directValue')}</code></span>
|
||||
<span><small>{t('aliases.flow.alias')}</small><code>{t('aliases.flow.aliasValue')}</code></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="thinking-alias-workbench">
|
||||
<section className="panel thinking-alias-builder">
|
||||
<div className="thinking-alias-panel-heading">
|
||||
<span><GitFork size={18} /></span>
|
||||
<div>
|
||||
<h2>{t('aliases.create.title')}</h2>
|
||||
<p>{t('aliases.create.description')}</p>
|
||||
<div className="thinking-alias-left-stack">
|
||||
<section className="panel thinking-alias-builder">
|
||||
<div className="thinking-alias-panel-heading">
|
||||
<span><GitFork size={18} /></span>
|
||||
<div>
|
||||
<h2>{t('aliases.create.title')}</h2>
|
||||
<p>{t('aliases.create.description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="thinking-alias-field thinking-model-field">
|
||||
<div className="thinking-alias-field thinking-model-field">
|
||||
<label htmlFor="thinking-model-search">{t('aliases.originalModel')}</label>
|
||||
<div className="thinking-model-picker" ref={modelPickerRef}>
|
||||
<div className="thinking-model-search">
|
||||
@@ -379,9 +356,9 @@ export function ThinkingAliasesPage() {
|
||||
) : (
|
||||
<small className="thinking-model-hint">{t('aliases.sourceHint')}</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="thinking-alias-field">
|
||||
<div className="thinking-alias-field">
|
||||
<div className="thinking-field-heading">
|
||||
<strong>{t('aliases.effort.title')}</strong>
|
||||
<span>{t('aliases.effort.description')}</span>
|
||||
@@ -439,43 +416,54 @@ export function ThinkingAliasesPage() {
|
||||
</button>
|
||||
<small>{t('aliases.customHelp')}</small>
|
||||
</div> : null}
|
||||
</div>
|
||||
|
||||
<div className="thinking-alias-field">
|
||||
<div className="thinking-field-heading">
|
||||
<strong>{t('aliases.aliasName.title')}</strong>
|
||||
<span>{t('aliases.aliasName.description')}</span>
|
||||
</div>
|
||||
<input
|
||||
id="thinking-alias-name"
|
||||
className="thinking-alias-input"
|
||||
value={alias}
|
||||
onChange={(event) => setAlias(event.currentTarget.value)}
|
||||
placeholder={selectedSource
|
||||
? t('aliases.aliasName.example', { model: selectedSource.model, effort: normalizedEffort || 'high' })
|
||||
: t('aliases.aliasName.selectFirst')}
|
||||
disabled={Boolean(busyAlias)}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="thinking-alias-preview">
|
||||
<BrainCircuit size={18} />
|
||||
<div>
|
||||
<span>{selectedSource?.model || t('aliases.notSelected')} <ArrowRight size={13} /> {alias || t('aliases.enterAlias')}</span>
|
||||
<code>{selectedSource?.protocol === 'openai' ? 'reasoning_effort' : 'reasoning.effort'} = {normalizedEffort || t('aliases.notSet')}</code>
|
||||
<section className="panel thinking-alias-name-panel">
|
||||
<div className="thinking-alias-panel-heading">
|
||||
<span><BrainCircuit size={18} /></span>
|
||||
<div>
|
||||
<h2>{t('aliases.modelAlias.title')}</h2>
|
||||
<p>{t('aliases.modelAlias.description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="primary-button thinking-alias-create"
|
||||
onClick={() => void createAlias()}
|
||||
disabled={loading || Boolean(busyAlias) || !selectedSource || !effort.trim() || !alias.trim()}
|
||||
>
|
||||
{busyAction === 'create' ? <LoaderCircle size={16} className="spin" /> : <GitFork size={16} />}
|
||||
{busyAction === 'create' ? t('aliases.creating') : t('aliases.create')}
|
||||
</button>
|
||||
</section>
|
||||
<div className="thinking-alias-field">
|
||||
<div className="thinking-field-heading">
|
||||
<strong>{t('aliases.aliasName.title')}</strong>
|
||||
<span>{t('aliases.aliasName.description')}</span>
|
||||
</div>
|
||||
<input
|
||||
id="thinking-alias-name"
|
||||
className="thinking-alias-input"
|
||||
value={alias}
|
||||
onChange={(event) => setAlias(event.currentTarget.value)}
|
||||
placeholder={selectedSource
|
||||
? t('aliases.aliasName.example', { model: selectedSource.model, effort: normalizedEffort || 'high' })
|
||||
: t('aliases.aliasName.selectFirst')}
|
||||
disabled={Boolean(busyAlias)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="thinking-alias-preview">
|
||||
<BrainCircuit size={18} />
|
||||
<div>
|
||||
<span>{selectedSource?.model || t('aliases.notSelected')} <ArrowRight size={13} /> {alias || t('aliases.enterAlias')}</span>
|
||||
<code>{selectedSource?.protocol === 'openai' ? 'reasoning_effort' : 'reasoning.effort'} = {normalizedEffort || t('aliases.notSet')}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="primary-button thinking-alias-create"
|
||||
onClick={() => void createAlias()}
|
||||
disabled={loading || Boolean(busyAlias) || !selectedSource || !effort.trim() || !alias.trim()}
|
||||
>
|
||||
{busyAction === 'create' ? <LoaderCircle size={16} className="spin" /> : <GitFork size={16} />}
|
||||
{busyAction === 'create' ? t('aliases.creating') : t('aliases.create')}
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section className="panel thinking-alias-list-panel">
|
||||
<div className="thinking-alias-list-heading">
|
||||
|
||||
Reference in new issue
Block a user