mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-14 16:17:02 +00:00
fix(droid): restore tabbed layout and stabilize save state
This commit is contained in:
1 parent
eedb53b49e
commit
a88fd68c34
2 files changed
+161
-74
No files matched your search
@@ -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'] });
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+121
-72
@@ -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,9 +308,22 @@ 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 (
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div className="flex-1 min-h-0 overflow-hidden px-4 pb-4 pt-3">
|
||||||
|
<TabsContent value="overview" className={tabContentClassName}>
|
||||||
<ScrollArea className="h-full">
|
<ScrollArea className="h-full">
|
||||||
<div className="space-y-4 p-4">
|
<div className="space-y-4 pr-1">
|
||||||
<Card>
|
<Card>
|
||||||
<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">
|
||||||
@@ -336,7 +349,11 @@ export function DroidPage() {
|
|||||||
value={diagnostics.binary.installDir || 'N/A'}
|
value={diagnostics.binary.installDir || 'N/A'}
|
||||||
mono
|
mono
|
||||||
/>
|
/>
|
||||||
<DetailRow label="Version" value={diagnostics.binary.version || 'Unknown'} mono />
|
<DetailRow
|
||||||
|
label="Version"
|
||||||
|
value={diagnostics.binary.version || 'Unknown'}
|
||||||
|
mono
|
||||||
|
/>
|
||||||
<DetailRow
|
<DetailRow
|
||||||
label="Override (CCS_DROID_PATH)"
|
label="Override (CCS_DROID_PATH)"
|
||||||
value={diagnostics.binary.overridePath || 'Not set'}
|
value={diagnostics.binary.overridePath || 'Not set'}
|
||||||
@@ -378,11 +395,37 @@ export function DroidPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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
|
<DroidSettingsQuickControlsCard
|
||||||
values={quickSettingsValues}
|
values={quickSettingsValues}
|
||||||
disabled={rawSettingsLoading || !rawEditorParsed.valid}
|
disabled={rawSettingsLoading || !rawEditorParsed.valid}
|
||||||
disabledReason={
|
disabledReason={
|
||||||
rawEditorParsed.valid ? null : `Quick settings disabled: ${rawEditorParsed.error}`
|
rawEditorParsed.valid
|
||||||
|
? null
|
||||||
|
: `Quick settings disabled: ${rawEditorParsed.error}`
|
||||||
}
|
}
|
||||||
onEnumSettingChange={(key, value) => {
|
onEnumSettingChange={(key, value) => {
|
||||||
updateSettingsField(key, value);
|
updateSettingsField(key, value);
|
||||||
@@ -454,9 +497,18 @@ export function DroidPage() {
|
|||||||
value={diagnostics.byok.activeModelSelector || 'Not set'}
|
value={diagnostics.byok.activeModelSelector || 'Not set'}
|
||||||
mono
|
mono
|
||||||
/>
|
/>
|
||||||
<DetailRow label="Custom models" value={String(diagnostics.byok.customModelCount)} />
|
<DetailRow
|
||||||
<DetailRow label="CCS-managed" value={String(diagnostics.byok.ccsManagedCount)} />
|
label="Custom models"
|
||||||
<DetailRow label="User-managed" value={String(diagnostics.byok.userManagedCount)} />
|
value={String(diagnostics.byok.customModelCount)}
|
||||||
|
/>
|
||||||
|
<DetailRow
|
||||||
|
label="CCS-managed"
|
||||||
|
value={String(diagnostics.byok.ccsManagedCount)}
|
||||||
|
/>
|
||||||
|
<DetailRow
|
||||||
|
label="User-managed"
|
||||||
|
value={String(diagnostics.byok.userManagedCount)}
|
||||||
|
/>
|
||||||
<DetailRow
|
<DetailRow
|
||||||
label="Malformed entries"
|
label="Malformed entries"
|
||||||
value={String(diagnostics.byok.invalidModelEntryCount)}
|
value={String(diagnostics.byok.invalidModelEntryCount)}
|
||||||
@@ -480,6 +532,63 @@ export function DroidPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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>
|
<Card>
|
||||||
<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">
|
||||||
@@ -511,7 +620,9 @@ export function DroidPage() {
|
|||||||
<span className="text-xs font-medium">{link.label}</span>
|
<span className="text-xs font-medium">{link.label}</span>
|
||||||
<ExternalLink className="h-3.5 w-3.5 text-muted-foreground" />
|
<ExternalLink className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-0.5 text-[11px] text-muted-foreground">{link.description}</p>
|
<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">
|
<p className="mt-1 break-all font-mono text-[11px] text-muted-foreground/90 underline underline-offset-2">
|
||||||
{link.url}
|
{link.url}
|
||||||
</p>
|
</p>
|
||||||
@@ -556,73 +667,11 @@ export function DroidPage() {
|
|||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
</TabsContent>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</Tabs>
|
||||||
</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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user