mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-16 18:15:24 +00:00
feat(image-analysis): resolve backend status per profile
This commit is contained in:
1 parent
8e4f56d8f9
commit
ae459fc3d7
17 files changed
+932
-48
No files matched your search
@@ -0,0 +1,173 @@
|
||||
import { AlertTriangle, Image as ImageIcon, Route } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { ImageAnalysisStatus } from '@/lib/api-client';
|
||||
|
||||
interface ImageAnalysisStatusSectionProps {
|
||||
status?: ImageAnalysisStatus | null;
|
||||
}
|
||||
|
||||
function getBadge(status: ImageAnalysisStatus | null | undefined): {
|
||||
label: string;
|
||||
variant: 'default' | 'secondary' | 'destructive' | 'outline';
|
||||
} {
|
||||
switch (status?.status) {
|
||||
case 'active':
|
||||
return { label: 'Ready', variant: 'default' };
|
||||
case 'mapped':
|
||||
return { label: 'Saved mapping', variant: 'secondary' };
|
||||
case 'attention':
|
||||
return { label: 'Needs review', variant: 'outline' };
|
||||
case 'disabled':
|
||||
return { label: 'Disabled', variant: 'outline' };
|
||||
case 'hook-missing':
|
||||
return { label: 'Setup needed', variant: 'destructive' };
|
||||
case 'skipped':
|
||||
return { label: 'Not available', variant: 'outline' };
|
||||
default:
|
||||
return { label: 'Checking', variant: 'outline' };
|
||||
}
|
||||
}
|
||||
|
||||
function getSummary(status: ImageAnalysisStatus): string {
|
||||
const backendName = status.backendDisplayName || status.backendId || 'this backend';
|
||||
|
||||
switch (status.status) {
|
||||
case 'disabled':
|
||||
return "Disabled globally. This profile uses Claude's built-in file reading because CCS image analysis is turned off.";
|
||||
case 'mapped':
|
||||
return `Ready via saved ${backendName} mapping. CCS could not infer the backend from this alias, so it uses the mapping saved in CCS config.`;
|
||||
case 'attention':
|
||||
return `Ready via ${backendName}, but runtime is using the current CLIProxy route instead of the route saved in this profile.`;
|
||||
case 'hook-missing':
|
||||
return `Configured for ${backendName}, but the image-analysis hook is not fully installed yet.`;
|
||||
case 'skipped':
|
||||
return status.reason || 'Skipped for this profile.';
|
||||
case 'active':
|
||||
default:
|
||||
if (status.resolutionSource === 'cliproxy-bridge') {
|
||||
return `Ready via ${backendName}. Images and PDFs are routed through CLIProxy before Claude sees text.`;
|
||||
}
|
||||
|
||||
if (status.resolutionSource === 'fallback-backend') {
|
||||
return `Ready via ${backendName} fallback. Images and PDFs are routed through CLIProxy before Claude sees text.`;
|
||||
}
|
||||
|
||||
return `Ready via ${backendName}. Images and PDFs are routed through CLIProxy before Claude sees text.`;
|
||||
}
|
||||
}
|
||||
|
||||
function getRuntimeLine(status: ImageAnalysisStatus): string {
|
||||
if (!status.runtimePath) {
|
||||
return 'Read -> native file access';
|
||||
}
|
||||
|
||||
return `Read -> image-analysis hook -> ${status.runtimePath}`;
|
||||
}
|
||||
|
||||
function getPersistenceLine(status: ImageAnalysisStatus): string {
|
||||
if (!status.shouldPersistHook || !status.persistencePath) {
|
||||
return 'Not persisted for this profile type';
|
||||
}
|
||||
|
||||
if (status.hookInstalled) {
|
||||
return `${status.persistencePath} hook`;
|
||||
}
|
||||
|
||||
return `${status.persistencePath} hook missing`;
|
||||
}
|
||||
|
||||
export function ImageAnalysisStatusSection({ status }: ImageAnalysisStatusSectionProps) {
|
||||
if (!status) {
|
||||
return (
|
||||
<div className="rounded-md border bg-muted/20 p-4" aria-live="polite">
|
||||
<div className="h-4 w-44 animate-pulse rounded bg-muted" />
|
||||
<div className="mt-2 h-3 w-72 animate-pulse rounded bg-muted" />
|
||||
<p className="mt-3 text-sm text-muted-foreground">Checking backend status...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const badge = getBadge(status);
|
||||
const detailLabel = status.supported ? 'Model' : 'Reason';
|
||||
const detailValue = status.supported ? status.model : status.reason || 'Unavailable';
|
||||
|
||||
return (
|
||||
<section className="rounded-md border bg-muted/20 p-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<ImageIcon className="h-4 w-4 text-sky-600" />
|
||||
<h3 className="text-sm font-semibold">Image-analysis backend</h3>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Derived runtime status. This section is not written into the JSON editor above.
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant={badge.variant} className="shrink-0 text-[11px]">
|
||||
{badge.label}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<p aria-live="polite" className="mt-3 text-sm leading-6 text-muted-foreground">
|
||||
{getSummary(status)}
|
||||
</p>
|
||||
|
||||
<dl className="mt-4 grid gap-x-4 gap-y-3 sm:grid-cols-2">
|
||||
<div className="space-y-1">
|
||||
<dt className="text-[11px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
|
||||
Backend
|
||||
</dt>
|
||||
<dd className="text-sm font-medium">{status.backendDisplayName || 'Unresolved'}</dd>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<dt className="text-[11px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
|
||||
Runtime
|
||||
</dt>
|
||||
<dd
|
||||
className="font-mono text-xs leading-5 text-foreground"
|
||||
title={getRuntimeLine(status)}
|
||||
>
|
||||
{getRuntimeLine(status)}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<dt className="text-[11px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
|
||||
Persistence
|
||||
</dt>
|
||||
<dd
|
||||
className="font-mono text-xs leading-5 text-foreground"
|
||||
title={status.persistencePath || 'Not persisted'}
|
||||
>
|
||||
{getPersistenceLine(status)}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<dt className="text-[11px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
|
||||
{detailLabel}
|
||||
</dt>
|
||||
<dd className={cn('text-sm text-foreground', status.supported && 'font-mono text-xs')}>
|
||||
{detailValue}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
{(status.status === 'attention' || status.status === 'hook-missing') && (
|
||||
<div className="mt-4 flex items-start gap-2 rounded-md border border-amber-500/20 bg-amber-500/10 px-3 py-2 text-sm text-amber-900 dark:text-amber-200">
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-amber-500" />
|
||||
<span>{status.reason}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 flex items-center gap-2 border-t border-border/60 pt-3 text-xs text-muted-foreground">
|
||||
<Route className="h-3.5 w-3.5" />
|
||||
<span>
|
||||
WebSearch stays managed separately and is not controlled by this backend status.
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -254,6 +254,7 @@ export function ProfileEditor({
|
||||
isRawJsonValid={computedIsRawJsonValid}
|
||||
rawJsonEdits={rawJsonEdits}
|
||||
settings={settings}
|
||||
imageAnalysisStatus={data?.imageAnalysisStatus}
|
||||
onChange={handleRawJsonChange}
|
||||
missingRequiredFields={missingRequiredFields}
|
||||
/>
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
import { Suspense, lazy } from 'react';
|
||||
import { Loader2, X, AlertTriangle } from 'lucide-react';
|
||||
import { GlobalEnvIndicator } from '@/components/shared/global-env-indicator';
|
||||
import { ImageAnalysisStatusSection } from './image-analysis-status-section';
|
||||
import type { Settings } from './types';
|
||||
import type { ImageAnalysisStatus } from '@/lib/api-client';
|
||||
|
||||
// Lazy load CodeEditor
|
||||
const CodeEditor = lazy(() =>
|
||||
@@ -18,6 +20,7 @@ interface RawEditorSectionProps {
|
||||
isRawJsonValid: boolean;
|
||||
rawJsonEdits: string | null;
|
||||
settings: Settings | undefined;
|
||||
imageAnalysisStatus?: ImageAnalysisStatus | null;
|
||||
onChange: (value: string) => void;
|
||||
missingRequiredFields?: string[];
|
||||
}
|
||||
@@ -27,6 +30,7 @@ export function RawEditorSection({
|
||||
isRawJsonValid,
|
||||
rawJsonEdits,
|
||||
settings,
|
||||
imageAnalysisStatus,
|
||||
onChange,
|
||||
missingRequiredFields = [],
|
||||
}: RawEditorSectionProps) {
|
||||
@@ -75,6 +79,9 @@ export function RawEditorSection({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-6 mb-4">
|
||||
<ImageAnalysisStatusSection status={imageAnalysisStatus} />
|
||||
</div>
|
||||
{/* Global Env Indicator */}
|
||||
<div className="mx-6 mb-4">
|
||||
<div className="border rounded-md overflow-hidden">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Types for Profile Editor
|
||||
*/
|
||||
|
||||
import type { CliTarget, CliproxyBridgeMetadata } from '@/lib/api-client';
|
||||
import type { CliTarget, CliproxyBridgeMetadata, ImageAnalysisStatus } from '@/lib/api-client';
|
||||
|
||||
export interface Settings {
|
||||
env?: Record<string, string>;
|
||||
@@ -14,6 +14,7 @@ export interface SettingsResponse {
|
||||
mtime: number;
|
||||
path: string;
|
||||
cliproxyBridge?: CliproxyBridgeMetadata | null;
|
||||
imageAnalysisStatus?: ImageAnalysisStatus | null;
|
||||
}
|
||||
|
||||
export interface ProfileEditorProps {
|
||||
|
||||
@@ -111,6 +111,35 @@ export interface CliproxyBridgeMetadata {
|
||||
usesCurrentAuthToken: boolean;
|
||||
}
|
||||
|
||||
export interface ImageAnalysisStatus {
|
||||
enabled: boolean;
|
||||
supported: boolean;
|
||||
status: 'active' | 'mapped' | 'attention' | 'disabled' | 'skipped' | 'hook-missing';
|
||||
backendId: string | null;
|
||||
backendDisplayName: string | null;
|
||||
model: string | null;
|
||||
resolutionSource:
|
||||
| 'cliproxy-provider'
|
||||
| 'cliproxy-variant'
|
||||
| 'cliproxy-composite'
|
||||
| 'copilot-alias'
|
||||
| 'cliproxy-bridge'
|
||||
| 'profile-backend'
|
||||
| 'fallback-backend'
|
||||
| 'disabled'
|
||||
| 'unsupported-profile'
|
||||
| 'unresolved'
|
||||
| 'missing-model';
|
||||
reason: string | null;
|
||||
shouldPersistHook: boolean;
|
||||
persistencePath: string | null;
|
||||
runtimePath: string | null;
|
||||
usesCurrentTarget: boolean | null;
|
||||
usesCurrentAuthToken: boolean | null;
|
||||
hookInstalled: boolean | null;
|
||||
sharedHookInstalled: boolean | null;
|
||||
}
|
||||
|
||||
export interface Profile {
|
||||
name: string;
|
||||
settingsPath: string;
|
||||
|
||||
Reference in new issue
Block a user