mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 14:19:56 +00:00
fix(droid): restore tabbed layout and stabilize save state
This commit is contained in:
@@ -97,6 +97,30 @@ interface SaveDroidRawSettingsResponse {
|
|||||||
mtime: number;
|
mtime: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseDroidRawSettingsText(rawText: string): {
|
||||||
|
settings: Record<string, unknown> | null;
|
||||||
|
parseError: string | null;
|
||||||
|
} {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(rawText);
|
||||||
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||||
|
return {
|
||||||
|
settings: null,
|
||||||
|
parseError: 'JSON root must be an object.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
settings: parsed as Record<string, unknown>,
|
||||||
|
parseError: null,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
settings: null,
|
||||||
|
parseError: (error as Error).message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchDroidDiagnostics(): Promise<DroidDashboardDiagnostics> {
|
async function fetchDroidDiagnostics(): Promise<DroidDashboardDiagnostics> {
|
||||||
const res = await fetch(withApiBase('/droid/diagnostics'));
|
const res = await fetch(withApiBase('/droid/diagnostics'));
|
||||||
if (!res.ok) throw new Error('Failed to fetch Droid diagnostics');
|
if (!res.ok) throw new Error('Failed to fetch Droid diagnostics');
|
||||||
@@ -142,9 +166,23 @@ export function useDroid() {
|
|||||||
|
|
||||||
const saveRawSettingsMutation = useMutation({
|
const saveRawSettingsMutation = useMutation({
|
||||||
mutationFn: saveDroidRawSettings,
|
mutationFn: saveDroidRawSettings,
|
||||||
onSuccess: () => {
|
onSuccess: (result, variables) => {
|
||||||
|
queryClient.setQueryData<DroidRawSettings>(['droid-raw-settings'], (current) => {
|
||||||
|
const path = current?.path ?? '~/.factory/settings.json';
|
||||||
|
const resolvedPath = current?.resolvedPath ?? path;
|
||||||
|
const parsed = parseDroidRawSettingsText(variables.rawText);
|
||||||
|
|
||||||
|
return {
|
||||||
|
path,
|
||||||
|
resolvedPath,
|
||||||
|
exists: true,
|
||||||
|
mtime: result.mtime,
|
||||||
|
rawText: variables.rawText,
|
||||||
|
settings: parsed.settings,
|
||||||
|
parseError: parsed.parseError,
|
||||||
|
};
|
||||||
|
});
|
||||||
queryClient.invalidateQueries({ queryKey: ['droid-diagnostics'] });
|
queryClient.invalidateQueries({ queryKey: ['droid-diagnostics'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['droid-raw-settings'] });
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+357
-308
@@ -25,6 +25,7 @@ import { Badge } from '@/components/ui/badge';
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import {
|
import {
|
||||||
applyAnthropicBudgetTokensToDroidByokModel,
|
applyAnthropicBudgetTokensToDroidByokModel,
|
||||||
@@ -257,7 +258,6 @@ export function DroidPage() {
|
|||||||
expectedMtime: rawSettings?.exists ? rawSettings.mtime : undefined,
|
expectedMtime: rawSettings?.exists ? rawSettings.mtime : undefined,
|
||||||
});
|
});
|
||||||
setRawDraftText(null);
|
setRawDraftText(null);
|
||||||
await Promise.all([refetchDiagnostics(), refetchRawSettings()]);
|
|
||||||
toast.success('Droid settings saved');
|
toast.success('Droid settings saved');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isApiConflictError(error)) {
|
if (isApiConflictError(error)) {
|
||||||
@@ -308,321 +308,370 @@ export function DroidPage() {
|
|||||||
const providerValues = docsReference.providerValues ?? [];
|
const providerValues = docsReference.providerValues ?? [];
|
||||||
const settingsHierarchy = docsReference.settingsHierarchy ?? [];
|
const settingsHierarchy = docsReference.settingsHierarchy ?? [];
|
||||||
|
|
||||||
|
const tabContentClassName = 'mt-0 h-full border-0 p-0 data-[state=inactive]:hidden';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollArea className="h-full">
|
<Tabs defaultValue="byok" className="h-full flex flex-col">
|
||||||
<div className="space-y-4 p-4">
|
<div className="px-4 pt-4 shrink-0">
|
||||||
<Card>
|
<TabsList className="grid w-full grid-cols-3">
|
||||||
<CardHeader className="pb-2">
|
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||||
<CardTitle className="text-base flex items-center gap-2">
|
<TabsTrigger value="byok">BYOK</TabsTrigger>
|
||||||
<TerminalSquare className="h-4 w-4" />
|
<TabsTrigger value="docs">Docs</TabsTrigger>
|
||||||
Runtime & Installation
|
</TabsList>
|
||||||
</CardTitle>
|
</div>
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-sm text-muted-foreground">Status</span>
|
|
||||||
<Badge variant={diagnostics.binary.installed ? 'default' : 'secondary'}>
|
|
||||||
{diagnostics.binary.installed ? 'Detected' : 'Not Found'}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<DetailRow label="Detection source" value={diagnostics.binary.source} mono />
|
|
||||||
<DetailRow
|
|
||||||
label="Binary path"
|
|
||||||
value={diagnostics.binary.path || 'Not detected'}
|
|
||||||
mono
|
|
||||||
/>
|
|
||||||
<DetailRow
|
|
||||||
label="Install directory"
|
|
||||||
value={diagnostics.binary.installDir || 'N/A'}
|
|
||||||
mono
|
|
||||||
/>
|
|
||||||
<DetailRow label="Version" value={diagnostics.binary.version || 'Unknown'} mono />
|
|
||||||
<DetailRow
|
|
||||||
label="Override (CCS_DROID_PATH)"
|
|
||||||
value={diagnostics.binary.overridePath || 'Not set'}
|
|
||||||
mono
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
<div className="flex-1 min-h-0 overflow-hidden px-4 pb-4 pt-3">
|
||||||
<CardHeader className="pb-2">
|
<TabsContent value="overview" className={tabContentClassName}>
|
||||||
<CardTitle className="text-base flex items-center gap-2">
|
<ScrollArea className="h-full">
|
||||||
<Folder className="h-4 w-4" />
|
<div className="space-y-4 pr-1">
|
||||||
Config Files
|
<Card>
|
||||||
</CardTitle>
|
<CardHeader className="pb-2">
|
||||||
</CardHeader>
|
<CardTitle className="text-base flex items-center gap-2">
|
||||||
<CardContent className="space-y-3">
|
<TerminalSquare className="h-4 w-4" />
|
||||||
{[diagnostics.files.settings, diagnostics.files.legacyConfig].map((file) => (
|
Runtime & Installation
|
||||||
<div key={file.label} className="rounded-md border p-3 space-y-1.5">
|
</CardTitle>
|
||||||
<div className="flex items-center justify-between gap-2">
|
</CardHeader>
|
||||||
<span className="font-medium text-sm">{file.label}</span>
|
<CardContent className="space-y-2">
|
||||||
{file.exists ? (
|
<div className="flex items-center justify-between">
|
||||||
<CheckCircle2 className="h-4 w-4 text-green-600" />
|
<span className="text-sm text-muted-foreground">Status</span>
|
||||||
) : (
|
<Badge variant={diagnostics.binary.installed ? 'default' : 'secondary'}>
|
||||||
<XCircle className="h-4 w-4 text-muted-foreground" />
|
{diagnostics.binary.installed ? 'Detected' : 'Not Found'}
|
||||||
)}
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<DetailRow label="Path" value={file.path} mono />
|
<DetailRow label="Detection source" value={diagnostics.binary.source} mono />
|
||||||
<DetailRow label="Resolved" value={file.resolvedPath} mono />
|
<DetailRow
|
||||||
<DetailRow label="Size" value={formatBytes(file.sizeBytes)} />
|
label="Binary path"
|
||||||
<DetailRow label="Last modified" value={formatTimestamp(file.mtimeMs)} />
|
value={diagnostics.binary.path || 'Not detected'}
|
||||||
{file.parseError && (
|
mono
|
||||||
<p className="text-xs text-amber-600">Parse warning: {file.parseError}</p>
|
/>
|
||||||
)}
|
<DetailRow
|
||||||
{file.readError && (
|
label="Install directory"
|
||||||
<p className="text-xs text-destructive">Read warning: {file.readError}</p>
|
value={diagnostics.binary.installDir || 'N/A'}
|
||||||
)}
|
mono
|
||||||
</div>
|
/>
|
||||||
))}
|
<DetailRow
|
||||||
</CardContent>
|
label="Version"
|
||||||
</Card>
|
value={diagnostics.binary.version || 'Unknown'}
|
||||||
|
mono
|
||||||
|
/>
|
||||||
|
<DetailRow
|
||||||
|
label="Override (CCS_DROID_PATH)"
|
||||||
|
value={diagnostics.binary.overridePath || 'Not set'}
|
||||||
|
mono
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<DroidSettingsQuickControlsCard
|
<Card>
|
||||||
values={quickSettingsValues}
|
<CardHeader className="pb-2">
|
||||||
disabled={rawSettingsLoading || !rawEditorParsed.valid}
|
<CardTitle className="text-base flex items-center gap-2">
|
||||||
disabledReason={
|
<Folder className="h-4 w-4" />
|
||||||
rawEditorParsed.valid ? null : `Quick settings disabled: ${rawEditorParsed.error}`
|
Config Files
|
||||||
}
|
</CardTitle>
|
||||||
onEnumSettingChange={(key, value) => {
|
</CardHeader>
|
||||||
updateSettingsField(key, value);
|
<CardContent className="space-y-3">
|
||||||
}}
|
{[diagnostics.files.settings, diagnostics.files.legacyConfig].map((file) => (
|
||||||
onBooleanSettingChange={(key, value) => {
|
<div key={file.label} className="rounded-md border p-3 space-y-1.5">
|
||||||
updateSettingsField(key, value);
|
<div className="flex items-center justify-between gap-2">
|
||||||
}}
|
<span className="font-medium text-sm">{file.label}</span>
|
||||||
onNumberSettingChange={(key, value) => {
|
{file.exists ? (
|
||||||
updateSettingsField(key, value);
|
<CheckCircle2 className="h-4 w-4 text-green-600" />
|
||||||
}}
|
) : (
|
||||||
/>
|
<XCircle className="h-4 w-4 text-muted-foreground" />
|
||||||
|
)}
|
||||||
<DroidByokReasoningControlsCard
|
|
||||||
models={byokModels}
|
|
||||||
disabled={rawSettingsLoading || !rawEditorParsed.valid}
|
|
||||||
disabledReason={
|
|
||||||
rawEditorParsed.valid
|
|
||||||
? null
|
|
||||||
: `BYOK reasoning controls disabled: ${rawEditorParsed.error}`
|
|
||||||
}
|
|
||||||
onEffortChange={(modelId, effort) => {
|
|
||||||
if (!rawEditorParsed.valid) {
|
|
||||||
toast.error('Fix JSON syntax before updating BYOK reasoning settings.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextSettings = applyReasoningEffortToDroidByokModel(
|
|
||||||
rawEditorParsed.value,
|
|
||||||
modelId,
|
|
||||||
effort
|
|
||||||
);
|
|
||||||
if (!nextSettings) {
|
|
||||||
toast.error('Unable to update selected BYOK model reasoning setting.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSettingsObject(nextSettings);
|
|
||||||
}}
|
|
||||||
onAnthropicBudgetChange={(modelId, budgetTokens) => {
|
|
||||||
if (!rawEditorParsed.valid) {
|
|
||||||
toast.error('Fix JSON syntax before updating thinking budget.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextSettings = applyAnthropicBudgetTokensToDroidByokModel(
|
|
||||||
rawEditorParsed.value,
|
|
||||||
modelId,
|
|
||||||
budgetTokens
|
|
||||||
);
|
|
||||||
if (!nextSettings) {
|
|
||||||
toast.error('Thinking budget is only available for Anthropic BYOK models.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSettingsObject(nextSettings);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-base flex items-center gap-2">
|
|
||||||
<Server className="h-4 w-4" />
|
|
||||||
BYOK Summary
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-2">
|
|
||||||
<DetailRow
|
|
||||||
label="Active model selector"
|
|
||||||
value={diagnostics.byok.activeModelSelector || 'Not set'}
|
|
||||||
mono
|
|
||||||
/>
|
|
||||||
<DetailRow label="Custom models" value={String(diagnostics.byok.customModelCount)} />
|
|
||||||
<DetailRow label="CCS-managed" value={String(diagnostics.byok.ccsManagedCount)} />
|
|
||||||
<DetailRow label="User-managed" value={String(diagnostics.byok.userManagedCount)} />
|
|
||||||
<DetailRow
|
|
||||||
label="Malformed entries"
|
|
||||||
value={String(diagnostics.byok.invalidModelEntryCount)}
|
|
||||||
/>
|
|
||||||
<Separator />
|
|
||||||
<div className="space-y-1">
|
|
||||||
<p className="text-xs text-muted-foreground">Providers</p>
|
|
||||||
<div className="flex flex-wrap gap-1.5">
|
|
||||||
{providerRows.length === 0 && (
|
|
||||||
<Badge variant="secondary" className="font-mono">
|
|
||||||
none
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{providerRows.map(([provider, count]) => (
|
|
||||||
<Badge key={provider} variant="outline" className="font-mono text-xs">
|
|
||||||
{provider}: {count}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-base flex items-center gap-2">
|
|
||||||
<ShieldCheck className="h-4 w-4" />
|
|
||||||
Docs-Aligned Notes
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-2 text-sm">
|
|
||||||
{docsNotes.map((note, index) => (
|
|
||||||
<p key={`${index}-${note}`} className="text-muted-foreground">
|
|
||||||
- {renderTextWithLinks(note)}
|
|
||||||
</p>
|
|
||||||
))}
|
|
||||||
<Separator />
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-xs text-muted-foreground uppercase tracking-wide">
|
|
||||||
Factory Docs
|
|
||||||
</p>
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
{docsLinks.map((link) => (
|
|
||||||
<a
|
|
||||||
key={link.id}
|
|
||||||
href={link.url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="block rounded-md border px-2.5 py-2 transition-colors hover:bg-muted/50"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between gap-2">
|
|
||||||
<span className="text-xs font-medium">{link.label}</span>
|
|
||||||
<ExternalLink className="h-3.5 w-3.5 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
<p className="mt-0.5 text-[11px] text-muted-foreground">{link.description}</p>
|
|
||||||
<p className="mt-1 break-all font-mono text-[11px] text-muted-foreground/90 underline underline-offset-2">
|
|
||||||
{link.url}
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Separator />
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-xs text-muted-foreground uppercase tracking-wide">
|
|
||||||
Provider Fact-Check Docs
|
|
||||||
</p>
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
{providerDocs.map((providerDoc) => (
|
|
||||||
<a
|
|
||||||
key={`${providerDoc.provider}-${providerDoc.url}`}
|
|
||||||
href={providerDoc.url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="block rounded-md border px-2.5 py-2 transition-colors hover:bg-muted/50"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between gap-2">
|
|
||||||
<span className="text-xs font-medium">{providerDoc.label}</span>
|
|
||||||
<ExternalLink className="h-3.5 w-3.5 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
<p className="mt-0.5 text-[11px] text-muted-foreground">
|
|
||||||
provider: {providerDoc.provider} | format: {providerDoc.apiFormat}
|
|
||||||
</p>
|
|
||||||
<p className="mt-1 break-all font-mono text-[11px] text-muted-foreground/90 underline underline-offset-2">
|
|
||||||
{providerDoc.url}
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Separator />
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Provider values: {providerValues.join(', ')}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Settings hierarchy: {settingsHierarchy.join(' -> ')}
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-base">Custom Models</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="rounded-md border overflow-hidden">
|
|
||||||
<div className="grid grid-cols-[2fr_1fr_2fr] bg-muted/40 px-3 py-2 text-xs font-medium">
|
|
||||||
<span>Name / Model</span>
|
|
||||||
<span>Provider</span>
|
|
||||||
<span>Base URL</span>
|
|
||||||
</div>
|
|
||||||
<ScrollArea className="h-52">
|
|
||||||
<div className="divide-y">
|
|
||||||
{customModels.length === 0 && (
|
|
||||||
<div className="px-3 py-4 text-xs text-muted-foreground">
|
|
||||||
No custom models
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{customModels.map((model) => (
|
|
||||||
<div
|
|
||||||
key={`${model.displayName}-${model.model}-${model.baseUrl}`}
|
|
||||||
className="grid grid-cols-[2fr_1fr_2fr] gap-2 px-3 py-2 text-xs"
|
|
||||||
>
|
|
||||||
<div className="min-w-0">
|
|
||||||
<p className="font-medium truncate">{model.displayName}</p>
|
|
||||||
<p className="text-muted-foreground font-mono truncate">{model.model}</p>
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0">
|
|
||||||
<p className="truncate">{model.provider}</p>
|
|
||||||
<p className="text-muted-foreground">{model.apiKeyPreview || 'no-key'}</p>
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0">
|
|
||||||
<p className="truncate" title={model.baseUrl}>
|
|
||||||
{model.host || model.baseUrl}
|
|
||||||
</p>
|
|
||||||
<p className="text-muted-foreground font-mono truncate">
|
|
||||||
{model.baseUrl}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<DetailRow label="Path" value={file.path} mono />
|
||||||
|
<DetailRow label="Resolved" value={file.resolvedPath} mono />
|
||||||
|
<DetailRow label="Size" value={formatBytes(file.sizeBytes)} />
|
||||||
|
<DetailRow label="Last modified" value={formatTimestamp(file.mtimeMs)} />
|
||||||
|
{file.parseError && (
|
||||||
|
<p className="text-xs text-amber-600">Parse warning: {file.parseError}</p>
|
||||||
|
)}
|
||||||
|
{file.readError && (
|
||||||
|
<p className="text-xs text-destructive">Read warning: {file.readError}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</CardContent>
|
||||||
</ScrollArea>
|
</Card>
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{diagnostics.warnings.length > 0 && (
|
{diagnostics.warnings.length > 0 && (
|
||||||
<Card className="border-amber-200 bg-amber-50/50 dark:bg-amber-950/20">
|
<Card className="border-amber-200 bg-amber-50/50 dark:bg-amber-950/20">
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-base flex items-center gap-2">
|
<CardTitle className="text-base flex items-center gap-2">
|
||||||
<AlertTriangle className="h-4 w-4 text-amber-600" />
|
<AlertTriangle className="h-4 w-4 text-amber-600" />
|
||||||
Warnings
|
Warnings
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-1.5">
|
<CardContent className="space-y-1.5">
|
||||||
{diagnostics.warnings.map((warning) => (
|
{diagnostics.warnings.map((warning) => (
|
||||||
<p key={warning} className="text-sm text-amber-800 dark:text-amber-300">
|
<p key={warning} className="text-sm text-amber-800 dark:text-amber-300">
|
||||||
- {warning}
|
- {warning}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="byok" className={tabContentClassName}>
|
||||||
|
<ScrollArea className="h-full">
|
||||||
|
<div className="space-y-4 pr-1">
|
||||||
|
<DroidSettingsQuickControlsCard
|
||||||
|
values={quickSettingsValues}
|
||||||
|
disabled={rawSettingsLoading || !rawEditorParsed.valid}
|
||||||
|
disabledReason={
|
||||||
|
rawEditorParsed.valid
|
||||||
|
? null
|
||||||
|
: `Quick settings disabled: ${rawEditorParsed.error}`
|
||||||
|
}
|
||||||
|
onEnumSettingChange={(key, value) => {
|
||||||
|
updateSettingsField(key, value);
|
||||||
|
}}
|
||||||
|
onBooleanSettingChange={(key, value) => {
|
||||||
|
updateSettingsField(key, value);
|
||||||
|
}}
|
||||||
|
onNumberSettingChange={(key, value) => {
|
||||||
|
updateSettingsField(key, value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DroidByokReasoningControlsCard
|
||||||
|
models={byokModels}
|
||||||
|
disabled={rawSettingsLoading || !rawEditorParsed.valid}
|
||||||
|
disabledReason={
|
||||||
|
rawEditorParsed.valid
|
||||||
|
? null
|
||||||
|
: `BYOK reasoning controls disabled: ${rawEditorParsed.error}`
|
||||||
|
}
|
||||||
|
onEffortChange={(modelId, effort) => {
|
||||||
|
if (!rawEditorParsed.valid) {
|
||||||
|
toast.error('Fix JSON syntax before updating BYOK reasoning settings.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextSettings = applyReasoningEffortToDroidByokModel(
|
||||||
|
rawEditorParsed.value,
|
||||||
|
modelId,
|
||||||
|
effort
|
||||||
|
);
|
||||||
|
if (!nextSettings) {
|
||||||
|
toast.error('Unable to update selected BYOK model reasoning setting.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSettingsObject(nextSettings);
|
||||||
|
}}
|
||||||
|
onAnthropicBudgetChange={(modelId, budgetTokens) => {
|
||||||
|
if (!rawEditorParsed.valid) {
|
||||||
|
toast.error('Fix JSON syntax before updating thinking budget.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextSettings = applyAnthropicBudgetTokensToDroidByokModel(
|
||||||
|
rawEditorParsed.value,
|
||||||
|
modelId,
|
||||||
|
budgetTokens
|
||||||
|
);
|
||||||
|
if (!nextSettings) {
|
||||||
|
toast.error('Thinking budget is only available for Anthropic BYOK models.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSettingsObject(nextSettings);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-base flex items-center gap-2">
|
||||||
|
<Server className="h-4 w-4" />
|
||||||
|
BYOK Summary
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2">
|
||||||
|
<DetailRow
|
||||||
|
label="Active model selector"
|
||||||
|
value={diagnostics.byok.activeModelSelector || 'Not set'}
|
||||||
|
mono
|
||||||
|
/>
|
||||||
|
<DetailRow
|
||||||
|
label="Custom models"
|
||||||
|
value={String(diagnostics.byok.customModelCount)}
|
||||||
|
/>
|
||||||
|
<DetailRow
|
||||||
|
label="CCS-managed"
|
||||||
|
value={String(diagnostics.byok.ccsManagedCount)}
|
||||||
|
/>
|
||||||
|
<DetailRow
|
||||||
|
label="User-managed"
|
||||||
|
value={String(diagnostics.byok.userManagedCount)}
|
||||||
|
/>
|
||||||
|
<DetailRow
|
||||||
|
label="Malformed entries"
|
||||||
|
value={String(diagnostics.byok.invalidModelEntryCount)}
|
||||||
|
/>
|
||||||
|
<Separator />
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-xs text-muted-foreground">Providers</p>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{providerRows.length === 0 && (
|
||||||
|
<Badge variant="secondary" className="font-mono">
|
||||||
|
none
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{providerRows.map(([provider, count]) => (
|
||||||
|
<Badge key={provider} variant="outline" className="font-mono text-xs">
|
||||||
|
{provider}: {count}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-base">Custom Models</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="rounded-md border overflow-hidden">
|
||||||
|
<div className="grid grid-cols-[2fr_1fr_2fr] bg-muted/40 px-3 py-2 text-xs font-medium">
|
||||||
|
<span>Name / Model</span>
|
||||||
|
<span>Provider</span>
|
||||||
|
<span>Base URL</span>
|
||||||
|
</div>
|
||||||
|
<ScrollArea className="h-52">
|
||||||
|
<div className="divide-y">
|
||||||
|
{customModels.length === 0 && (
|
||||||
|
<div className="px-3 py-4 text-xs text-muted-foreground">
|
||||||
|
No custom models
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{customModels.map((model) => (
|
||||||
|
<div
|
||||||
|
key={`${model.displayName}-${model.model}-${model.baseUrl}`}
|
||||||
|
className="grid grid-cols-[2fr_1fr_2fr] gap-2 px-3 py-2 text-xs"
|
||||||
|
>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="font-medium truncate">{model.displayName}</p>
|
||||||
|
<p className="text-muted-foreground font-mono truncate">
|
||||||
|
{model.model}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="truncate">{model.provider}</p>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
{model.apiKeyPreview || 'no-key'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="truncate" title={model.baseUrl}>
|
||||||
|
{model.host || model.baseUrl}
|
||||||
|
</p>
|
||||||
|
<p className="text-muted-foreground font-mono truncate">
|
||||||
|
{model.baseUrl}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="docs" className={tabContentClassName}>
|
||||||
|
<ScrollArea className="h-full">
|
||||||
|
<div className="space-y-4 pr-1">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-base flex items-center gap-2">
|
||||||
|
<ShieldCheck className="h-4 w-4" />
|
||||||
|
Docs-Aligned Notes
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2 text-sm">
|
||||||
|
{docsNotes.map((note, index) => (
|
||||||
|
<p key={`${index}-${note}`} className="text-muted-foreground">
|
||||||
|
- {renderTextWithLinks(note)}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
<Separator />
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-xs text-muted-foreground uppercase tracking-wide">
|
||||||
|
Factory Docs
|
||||||
|
</p>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
{docsLinks.map((link) => (
|
||||||
|
<a
|
||||||
|
key={link.id}
|
||||||
|
href={link.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="block rounded-md border px-2.5 py-2 transition-colors hover:bg-muted/50"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<span className="text-xs font-medium">{link.label}</span>
|
||||||
|
<ExternalLink className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<p className="mt-0.5 text-[11px] text-muted-foreground">
|
||||||
|
{link.description}
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 break-all font-mono text-[11px] text-muted-foreground/90 underline underline-offset-2">
|
||||||
|
{link.url}
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-xs text-muted-foreground uppercase tracking-wide">
|
||||||
|
Provider Fact-Check Docs
|
||||||
|
</p>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
{providerDocs.map((providerDoc) => (
|
||||||
|
<a
|
||||||
|
key={`${providerDoc.provider}-${providerDoc.url}`}
|
||||||
|
href={providerDoc.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="block rounded-md border px-2.5 py-2 transition-colors hover:bg-muted/50"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<span className="text-xs font-medium">{providerDoc.label}</span>
|
||||||
|
<ExternalLink className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<p className="mt-0.5 text-[11px] text-muted-foreground">
|
||||||
|
provider: {providerDoc.provider} | format: {providerDoc.apiFormat}
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 break-all font-mono text-[11px] text-muted-foreground/90 underline underline-offset-2">
|
||||||
|
{providerDoc.url}
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Provider values: {providerValues.join(', ')}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Settings hierarchy: {settingsHierarchy.join(' -> ')}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</TabsContent>
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</Tabs>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user