mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 06:16:37 +00:00
fix(droid): restore tabbed layout and stabilize save state
This commit is contained in:
@@ -97,6 +97,30 @@ interface SaveDroidRawSettingsResponse {
|
||||
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> {
|
||||
const res = await fetch(withApiBase('/droid/diagnostics'));
|
||||
if (!res.ok) throw new Error('Failed to fetch Droid diagnostics');
|
||||
@@ -142,9 +166,23 @@ export function useDroid() {
|
||||
|
||||
const saveRawSettingsMutation = useMutation({
|
||||
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-raw-settings'] });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+357
-308
@@ -25,6 +25,7 @@ import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
applyAnthropicBudgetTokensToDroidByokModel,
|
||||
@@ -257,7 +258,6 @@ export function DroidPage() {
|
||||
expectedMtime: rawSettings?.exists ? rawSettings.mtime : undefined,
|
||||
});
|
||||
setRawDraftText(null);
|
||||
await Promise.all([refetchDiagnostics(), refetchRawSettings()]);
|
||||
toast.success('Droid settings saved');
|
||||
} catch (error) {
|
||||
if (isApiConflictError(error)) {
|
||||
@@ -308,321 +308,370 @@ export function DroidPage() {
|
||||
const providerValues = docsReference.providerValues ?? [];
|
||||
const settingsHierarchy = docsReference.settingsHierarchy ?? [];
|
||||
|
||||
const tabContentClassName = 'mt-0 h-full border-0 p-0 data-[state=inactive]:hidden';
|
||||
|
||||
return (
|
||||
<ScrollArea className="h-full">
|
||||
<div className="space-y-4 p-4">
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<TerminalSquare className="h-4 w-4" />
|
||||
Runtime & Installation
|
||||
</CardTitle>
|
||||
</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>
|
||||
<Tabs defaultValue="byok" className="h-full flex flex-col">
|
||||
<div className="px-4 pt-4 shrink-0">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="byok">BYOK</TabsTrigger>
|
||||
<TabsTrigger value="docs">Docs</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Folder className="h-4 w-4" />
|
||||
Config Files
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{[diagnostics.files.settings, diagnostics.files.legacyConfig].map((file) => (
|
||||
<div key={file.label} className="rounded-md border p-3 space-y-1.5">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="font-medium text-sm">{file.label}</span>
|
||||
{file.exists ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-green-600" />
|
||||
) : (
|
||||
<XCircle className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</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>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="flex-1 min-h-0 overflow-hidden px-4 pb-4 pt-3">
|
||||
<TabsContent value="overview" 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">
|
||||
<TerminalSquare className="h-4 w-4" />
|
||||
Runtime & Installation
|
||||
</CardTitle>
|
||||
</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>
|
||||
|
||||
<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 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>
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Folder className="h-4 w-4" />
|
||||
Config Files
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{[diagnostics.files.settings, diagnostics.files.legacyConfig].map((file) => (
|
||||
<div key={file.label} className="rounded-md border p-3 space-y-1.5">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="font-medium text-sm">{file.label}</span>
|
||||
{file.exists ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-green-600" />
|
||||
) : (
|
||||
<XCircle className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</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>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{diagnostics.warnings.length > 0 && (
|
||||
<Card className="border-amber-200 bg-amber-50/50 dark:bg-amber-950/20">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<AlertTriangle className="h-4 w-4 text-amber-600" />
|
||||
Warnings
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-1.5">
|
||||
{diagnostics.warnings.map((warning) => (
|
||||
<p key={warning} className="text-sm text-amber-800 dark:text-amber-300">
|
||||
- {warning}
|
||||
</p>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
{diagnostics.warnings.length > 0 && (
|
||||
<Card className="border-amber-200 bg-amber-50/50 dark:bg-amber-950/20">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<AlertTriangle className="h-4 w-4 text-amber-600" />
|
||||
Warnings
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-1.5">
|
||||
{diagnostics.warnings.map((warning) => (
|
||||
<p key={warning} className="text-sm text-amber-800 dark:text-amber-300">
|
||||
- {warning}
|
||||
</p>
|
||||
))}
|
||||
</CardContent>
|
||||
</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>
|
||||
</ScrollArea>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user