mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-10 02:24:35 +08:00
feat: add useApiSettings hook for managing API settings, including providers, endpoints, cache, and model pools
- Implemented hooks for CRUD operations on providers and endpoints. - Added cache management hooks for cache stats and settings. - Introduced model pool management hooks for high availability and load balancing. - Created localization files for English and Chinese translations of API settings.
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Save, RefreshCw, AlertTriangle, FileCode } from 'lucide-react';
|
||||
import { Save, RefreshCw, AlertTriangle, FileCode, AlertCircle } from 'lucide-react';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Textarea } from '@/components/ui/Textarea';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
@@ -32,6 +32,7 @@ export function AdvancedTab({ enabled = true }: AdvancedTabProps) {
|
||||
env,
|
||||
settings,
|
||||
isLoading: isLoadingEnv,
|
||||
error: envError,
|
||||
refetch,
|
||||
} = useCodexLensEnv({ enabled });
|
||||
|
||||
@@ -43,23 +44,25 @@ export function AdvancedTab({ enabled = true }: AdvancedTabProps) {
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [showWarning, setShowWarning] = useState(false);
|
||||
|
||||
// Initialize form from env
|
||||
// Initialize form from env - handles both undefined (loading) and empty string (empty file)
|
||||
// The hook returns raw directly, so we check if it's been set (not undefined means data loaded)
|
||||
useEffect(() => {
|
||||
if (raw !== undefined) {
|
||||
setEnvInput(raw);
|
||||
// Initialize when data is loaded (raw may be empty string but not undefined during loading)
|
||||
// Note: During initial load, raw is undefined. After load completes, raw is set (even if empty string)
|
||||
if (!isLoadingEnv) {
|
||||
setEnvInput(raw ?? ''); // Use empty string if raw is undefined/null
|
||||
setErrors({});
|
||||
setHasChanges(false);
|
||||
setShowWarning(false);
|
||||
}
|
||||
}, [raw]);
|
||||
}, [raw, isLoadingEnv]);
|
||||
|
||||
const handleEnvChange = (value: string) => {
|
||||
setEnvInput(value);
|
||||
// Check if there are changes
|
||||
if (raw !== undefined) {
|
||||
setHasChanges(value !== raw);
|
||||
setShowWarning(value !== raw);
|
||||
}
|
||||
// Check if there are changes - compare with raw value (handle undefined as empty)
|
||||
const currentRaw = raw ?? '';
|
||||
setHasChanges(value !== currentRaw);
|
||||
setShowWarning(value !== currentRaw);
|
||||
if (errors.env) {
|
||||
setErrors((prev) => ({ ...prev, env: undefined }));
|
||||
}
|
||||
@@ -132,12 +135,11 @@ export function AdvancedTab({ enabled = true }: AdvancedTabProps) {
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
if (raw !== undefined) {
|
||||
setEnvInput(raw);
|
||||
setErrors({});
|
||||
setHasChanges(false);
|
||||
setShowWarning(false);
|
||||
}
|
||||
// Reset to current raw value (handle undefined as empty)
|
||||
setEnvInput(raw ?? '');
|
||||
setErrors({});
|
||||
setHasChanges(false);
|
||||
setShowWarning(false);
|
||||
};
|
||||
|
||||
const isLoading = isLoadingEnv;
|
||||
@@ -154,6 +156,32 @@ export function AdvancedTab({ enabled = true }: AdvancedTabProps) {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Error Card */}
|
||||
{envError && (
|
||||
<Card className="p-4 bg-destructive/10 border-destructive/20">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertCircle className="w-5 h-5 text-destructive flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<h4 className="text-sm font-medium text-destructive-foreground">
|
||||
{formatMessage({ id: 'codexlens.advanced.loadError' })}
|
||||
</h4>
|
||||
<p className="text-xs text-destructive-foreground/80 mt-1">
|
||||
{envError.message || formatMessage({ id: 'codexlens.advanced.loadErrorDesc' })}
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => refetch()}
|
||||
className="mt-2"
|
||||
>
|
||||
<RefreshCw className="w-3 h-3 mr-1" />
|
||||
{formatMessage({ id: 'common.actions.retry' })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Sensitivity Warning Card */}
|
||||
{showWarning && (
|
||||
<Card className="p-4 bg-warning/10 border-warning/20">
|
||||
|
||||
286
ccw/frontend/src/components/codexlens/IndexOperations.tsx
Normal file
286
ccw/frontend/src/components/codexlens/IndexOperations.tsx
Normal file
@@ -0,0 +1,286 @@
|
||||
// ========================================
|
||||
// CodexLens Index Operations Component
|
||||
// ========================================
|
||||
// Index management operations with progress tracking
|
||||
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
RotateCw,
|
||||
Zap,
|
||||
AlertCircle,
|
||||
CheckCircle2,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Progress } from '@/components/ui/Progress';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
import {
|
||||
useCodexLensIndexingStatus,
|
||||
useRebuildIndex,
|
||||
useUpdateIndex,
|
||||
useCancelIndexing,
|
||||
} from '@/hooks';
|
||||
import { useNotifications } from '@/hooks/useNotifications';
|
||||
import { useWebSocket } from '@/hooks/useWebSocket';
|
||||
|
||||
interface IndexOperationsProps {
|
||||
disabled?: boolean;
|
||||
onRefresh?: () => void;
|
||||
}
|
||||
|
||||
interface IndexProgress {
|
||||
stage: string;
|
||||
message: string;
|
||||
percent: number;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
type IndexOperation = {
|
||||
id: string;
|
||||
type: 'fts_full' | 'fts_incremental' | 'vector_full' | 'vector_incremental';
|
||||
label: string;
|
||||
description: string;
|
||||
icon: React.ReactNode;
|
||||
};
|
||||
|
||||
export function IndexOperations({ disabled = false, onRefresh }: IndexOperationsProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
const { success, error: showError } = useNotifications();
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
const { inProgress } = useCodexLensIndexingStatus();
|
||||
const { rebuildIndex, isRebuilding } = useRebuildIndex();
|
||||
const { updateIndex, isUpdating } = useUpdateIndex();
|
||||
const { cancelIndexing, isCancelling } = useCancelIndexing();
|
||||
const { lastMessage } = useWebSocket();
|
||||
|
||||
const [indexProgress, setIndexProgress] = useState<IndexProgress | null>(null);
|
||||
const [activeOperation, setActiveOperation] = useState<string | null>(null);
|
||||
|
||||
// Listen for WebSocket progress updates
|
||||
useEffect(() => {
|
||||
if (lastMessage?.type === 'CODEXLENS_INDEX_PROGRESS') {
|
||||
const progress = lastMessage.payload as IndexProgress;
|
||||
setIndexProgress(progress);
|
||||
|
||||
// Clear active operation when complete or error
|
||||
if (progress.stage === 'complete' || progress.stage === 'error' || progress.stage === 'cancelled') {
|
||||
if (progress.stage === 'complete') {
|
||||
success(
|
||||
formatMessage({ id: 'codexlens.index.operationComplete' }),
|
||||
progress.message
|
||||
);
|
||||
onRefresh?.();
|
||||
} else if (progress.stage === 'error') {
|
||||
showError(
|
||||
formatMessage({ id: 'codexlens.index.operationFailed' }),
|
||||
progress.message
|
||||
);
|
||||
}
|
||||
setActiveOperation(null);
|
||||
setIndexProgress(null);
|
||||
}
|
||||
}
|
||||
}, [lastMessage, formatMessage, success, showError, onRefresh]);
|
||||
|
||||
const isOperating = isRebuilding || isUpdating || inProgress || !!activeOperation;
|
||||
|
||||
const handleOperation = async (operation: IndexOperation) => {
|
||||
if (!projectPath) {
|
||||
showError(
|
||||
formatMessage({ id: 'codexlens.index.noProject' }),
|
||||
formatMessage({ id: 'codexlens.index.noProjectDesc' })
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveOperation(operation.id);
|
||||
setIndexProgress({ stage: 'start', message: formatMessage({ id: 'codexlens.index.starting' }), percent: 0 });
|
||||
|
||||
try {
|
||||
// Determine index type and operation
|
||||
const isVector = operation.type.includes('vector');
|
||||
const isIncremental = operation.type.includes('incremental');
|
||||
|
||||
if (isIncremental) {
|
||||
const result = await updateIndex(projectPath, {
|
||||
indexType: isVector ? 'vector' : 'normal',
|
||||
});
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Update failed');
|
||||
}
|
||||
} else {
|
||||
const result = await rebuildIndex(projectPath, {
|
||||
indexType: isVector ? 'vector' : 'normal',
|
||||
});
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Rebuild failed');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
setActiveOperation(null);
|
||||
setIndexProgress(null);
|
||||
showError(
|
||||
formatMessage({ id: 'codexlens.index.operationFailed' }),
|
||||
err instanceof Error ? err.message : formatMessage({ id: 'codexlens.index.unknownError' })
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = async () => {
|
||||
const result = await cancelIndexing();
|
||||
if (result.success) {
|
||||
setActiveOperation(null);
|
||||
setIndexProgress(null);
|
||||
} else {
|
||||
showError(
|
||||
formatMessage({ id: 'codexlens.index.cancelFailed' }),
|
||||
result.error || formatMessage({ id: 'codexlens.index.unknownError' })
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const operations: IndexOperation[] = [
|
||||
{
|
||||
id: 'fts_full',
|
||||
type: 'fts_full',
|
||||
label: formatMessage({ id: 'codexlens.overview.actions.ftsFull' }),
|
||||
description: formatMessage({ id: 'codexlens.overview.actions.ftsFullDesc' }),
|
||||
icon: <RotateCw className="w-4 h-4" />,
|
||||
},
|
||||
{
|
||||
id: 'fts_incremental',
|
||||
type: 'fts_incremental',
|
||||
label: formatMessage({ id: 'codexlens.overview.actions.ftsIncremental' }),
|
||||
description: formatMessage({ id: 'codexlens.overview.actions.ftsIncrementalDesc' }),
|
||||
icon: <Zap className="w-4 h-4" />,
|
||||
},
|
||||
{
|
||||
id: 'vector_full',
|
||||
type: 'vector_full',
|
||||
label: formatMessage({ id: 'codexlens.overview.actions.vectorFull' }),
|
||||
description: formatMessage({ id: 'codexlens.overview.actions.vectorFullDesc' }),
|
||||
icon: <RotateCw className="w-4 h-4" />,
|
||||
},
|
||||
{
|
||||
id: 'vector_incremental',
|
||||
type: 'vector_incremental',
|
||||
label: formatMessage({ id: 'codexlens.overview.actions.vectorIncremental' }),
|
||||
description: formatMessage({ id: 'codexlens.overview.actions.vectorIncrementalDesc' }),
|
||||
icon: <Zap className="w-4 h-4" />,
|
||||
},
|
||||
];
|
||||
|
||||
if (indexProgress && activeOperation) {
|
||||
const operation = operations.find((op) => op.id === activeOperation);
|
||||
const isComplete = indexProgress.stage === 'complete';
|
||||
const isError = indexProgress.stage === 'error';
|
||||
const isCancelled = indexProgress.stage === 'cancelled';
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base flex items-center justify-between">
|
||||
<span>{operation?.label}</span>
|
||||
{!isComplete && !isError && !isCancelled && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleCancel}
|
||||
disabled={isCancelling}
|
||||
>
|
||||
<X className="w-4 h-4 mr-1" />
|
||||
{formatMessage({ id: 'common.actions.cancel' })}
|
||||
</Button>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Status Icon */}
|
||||
<div className="flex items-center gap-3">
|
||||
{isComplete ? (
|
||||
<CheckCircle2 className="w-6 h-6 text-success" />
|
||||
) : isError || isCancelled ? (
|
||||
<AlertCircle className="w-6 h-6 text-destructive" />
|
||||
) : (
|
||||
<RotateCw className="w-6 h-6 text-primary animate-spin" />
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
{isComplete
|
||||
? formatMessage({ id: 'codexlens.index.complete' })
|
||||
: isError
|
||||
? formatMessage({ id: 'codexlens.index.failed' })
|
||||
: isCancelled
|
||||
? formatMessage({ id: 'codexlens.index.cancelled' })
|
||||
: formatMessage({ id: 'codexlens.index.inProgress' })}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">{indexProgress.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
{!isComplete && !isError && !isCancelled && (
|
||||
<div className="space-y-2">
|
||||
<Progress value={indexProgress.percent} className="h-2" />
|
||||
<p className="text-xs text-muted-foreground text-right">
|
||||
{indexProgress.percent}%
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Close Button */}
|
||||
{(isComplete || isError || isCancelled) && (
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setActiveOperation(null);
|
||||
setIndexProgress(null);
|
||||
}}
|
||||
>
|
||||
{formatMessage({ id: 'common.actions.close' })}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">
|
||||
{formatMessage({ id: 'codexlens.overview.actions.title' })}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{operations.map((operation) => (
|
||||
<Button
|
||||
key={operation.id}
|
||||
variant="outline"
|
||||
className="h-auto p-4 flex flex-col items-start gap-2 text-left"
|
||||
onClick={() => handleOperation(operation)}
|
||||
disabled={disabled || isOperating}
|
||||
>
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<span className={cn('text-muted-foreground', (disabled || isOperating) && 'opacity-50')}>
|
||||
{operation.icon}
|
||||
</span>
|
||||
<span className="font-medium">{operation.label}</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{operation.description}</p>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default IndexOperations;
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
RefreshCw,
|
||||
Package,
|
||||
Filter,
|
||||
AlertCircle,
|
||||
} from 'lucide-react';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
@@ -69,6 +70,7 @@ export function ModelsTab({ installed = false }: ModelsTabProps) {
|
||||
const {
|
||||
models,
|
||||
isLoading,
|
||||
error,
|
||||
refetch,
|
||||
} = useCodexLensModels({
|
||||
enabled: installed,
|
||||
@@ -243,7 +245,25 @@ export function ModelsTab({ installed = false }: ModelsTabProps) {
|
||||
/>
|
||||
|
||||
{/* Model List */}
|
||||
{isLoading ? (
|
||||
{error ? (
|
||||
<Card className="p-8 text-center">
|
||||
<AlertCircle className="w-12 h-12 mx-auto text-destructive/50 mb-3" />
|
||||
<h3 className="text-sm font-medium text-destructive-foreground mb-1">
|
||||
{formatMessage({ id: 'codexlens.models.error.title' })}
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">
|
||||
{error.message || formatMessage({ id: 'codexlens.models.error.description' })}
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => refetch()}
|
||||
>
|
||||
<RefreshCw className="w-3 h-3 mr-1" />
|
||||
{formatMessage({ id: 'common.actions.retry' })}
|
||||
</Button>
|
||||
</Card>
|
||||
) : isLoading ? (
|
||||
<Card className="p-8 text-center">
|
||||
<p className="text-muted-foreground">{formatMessage({ id: 'common.actions.loading' })}</p>
|
||||
</Card>
|
||||
@@ -251,10 +271,16 @@ export function ModelsTab({ installed = false }: ModelsTabProps) {
|
||||
<Card className="p-8 text-center">
|
||||
<Package className="w-12 h-12 mx-auto text-muted-foreground/30 mb-3" />
|
||||
<h3 className="text-sm font-medium text-foreground mb-1">
|
||||
{formatMessage({ id: 'codexlens.models.empty.title' })}
|
||||
{models && models.length > 0
|
||||
? formatMessage({ id: 'codexlens.models.empty.filtered' })
|
||||
: formatMessage({ id: 'codexlens.models.empty.title' })
|
||||
}
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'codexlens.models.empty.description' })}
|
||||
{models && models.length > 0
|
||||
? formatMessage({ id: 'codexlens.models.empty.filteredDesc' })
|
||||
: formatMessage({ id: 'codexlens.models.empty.description' })
|
||||
}
|
||||
</p>
|
||||
</Card>
|
||||
) : (
|
||||
|
||||
@@ -9,22 +9,22 @@ import {
|
||||
FileText,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
RotateCw,
|
||||
Zap,
|
||||
} from 'lucide-react';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { CodexLensVenvStatus, CodexLensConfig } from '@/lib/api';
|
||||
import { IndexOperations } from './IndexOperations';
|
||||
|
||||
interface OverviewTabProps {
|
||||
installed: boolean;
|
||||
status?: CodexLensVenvStatus;
|
||||
config?: CodexLensConfig;
|
||||
isLoading: boolean;
|
||||
onRefresh?: () => void;
|
||||
}
|
||||
|
||||
export function OverviewTab({ installed, status, config, isLoading }: OverviewTabProps) {
|
||||
export function OverviewTab({ installed, status, config, isLoading, onRefresh }: OverviewTabProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
if (isLoading) {
|
||||
@@ -142,42 +142,8 @@ export function OverviewTab({ installed, status, config, isLoading }: OverviewTa
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">
|
||||
{formatMessage({ id: 'codexlens.overview.actions.title' })}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
<QuickActionButton
|
||||
icon={<RotateCw className="w-4 h-4" />}
|
||||
label={formatMessage({ id: 'codexlens.overview.actions.ftsFull' })}
|
||||
description={formatMessage({ id: 'codexlens.overview.actions.ftsFullDesc' })}
|
||||
disabled={!isReady}
|
||||
/>
|
||||
<QuickActionButton
|
||||
icon={<Zap className="w-4 h-4" />}
|
||||
label={formatMessage({ id: 'codexlens.overview.actions.ftsIncremental' })}
|
||||
description={formatMessage({ id: 'codexlens.overview.actions.ftsIncrementalDesc' })}
|
||||
disabled={!isReady}
|
||||
/>
|
||||
<QuickActionButton
|
||||
icon={<RotateCw className="w-4 h-4" />}
|
||||
label={formatMessage({ id: 'codexlens.overview.actions.vectorFull' })}
|
||||
description={formatMessage({ id: 'codexlens.overview.actions.vectorFullDesc' })}
|
||||
disabled={!isReady}
|
||||
/>
|
||||
<QuickActionButton
|
||||
icon={<Zap className="w-4 h-4" />}
|
||||
label={formatMessage({ id: 'codexlens.overview.actions.vectorIncremental' })}
|
||||
description={formatMessage({ id: 'codexlens.overview.actions.vectorIncrementalDesc' })}
|
||||
disabled={!isReady}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* Index Operations */}
|
||||
<IndexOperations disabled={!isReady} onRefresh={onRefresh} />
|
||||
|
||||
{/* Venv Details */}
|
||||
{status && (
|
||||
@@ -210,37 +176,3 @@ export function OverviewTab({ installed, status, config, isLoading }: OverviewTa
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface QuickActionButtonProps {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
description: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
function QuickActionButton({ icon, label, description, disabled }: QuickActionButtonProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const handleClick = () => {
|
||||
// TODO: Implement index operations in future tasks
|
||||
// For now, show a message that this feature is coming soon
|
||||
alert(formatMessage({ id: 'codexlens.comingSoon' }));
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-auto p-4 flex flex-col items-start gap-2 text-left"
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<span className={cn('text-muted-foreground', disabled && 'opacity-50')}>
|
||||
{icon}
|
||||
</span>
|
||||
<span className="font-medium">{label}</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{description}</p>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
273
ccw/frontend/src/components/codexlens/SearchTab.tsx
Normal file
273
ccw/frontend/src/components/codexlens/SearchTab.tsx
Normal file
@@ -0,0 +1,273 @@
|
||||
// ========================================
|
||||
// CodexLens Search Tab
|
||||
// ========================================
|
||||
// Semantic code search interface with multiple search types
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Search, FileCode, Code } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Label } from '@/components/ui/Label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/Select';
|
||||
import {
|
||||
useCodexLensSearch,
|
||||
useCodexLensFilesSearch,
|
||||
useCodexLensSymbolSearch,
|
||||
} from '@/hooks/useCodexLens';
|
||||
import type { CodexLensSearchParams } from '@/lib/api';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type SearchType = 'search' | 'search_files' | 'symbol';
|
||||
type SearchMode = 'dense_rerank' | 'fts' | 'fuzzy';
|
||||
|
||||
interface SearchTabProps {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export function SearchTab({ enabled }: SearchTabProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
const [searchType, setSearchType] = useState<SearchType>('search');
|
||||
const [searchMode, setSearchMode] = useState<SearchMode>('dense_rerank');
|
||||
const [query, setQuery] = useState('');
|
||||
const [hasSearched, setHasSearched] = useState(false);
|
||||
|
||||
// Build search params based on search type
|
||||
const searchParams: CodexLensSearchParams = {
|
||||
query,
|
||||
limit: 20,
|
||||
mode: searchType !== 'symbol' ? searchMode : undefined,
|
||||
max_content_length: 200,
|
||||
extra_files_count: 10,
|
||||
};
|
||||
|
||||
// Search hooks - only enable when hasSearched is true and query is not empty
|
||||
const contentSearch = useCodexLensSearch(
|
||||
searchParams,
|
||||
{ enabled: enabled && hasSearched && searchType === 'search' && query.trim().length > 0 }
|
||||
);
|
||||
|
||||
const fileSearch = useCodexLensFilesSearch(
|
||||
searchParams,
|
||||
{ enabled: enabled && hasSearched && searchType === 'search_files' && query.trim().length > 0 }
|
||||
);
|
||||
|
||||
const symbolSearch = useCodexLensSymbolSearch(
|
||||
{ query, limit: 20 },
|
||||
{ enabled: enabled && hasSearched && searchType === 'symbol' && query.trim().length > 0 }
|
||||
);
|
||||
|
||||
// Get loading state based on search type
|
||||
const isLoading = searchType === 'search'
|
||||
? contentSearch.isLoading
|
||||
: searchType === 'search_files'
|
||||
? fileSearch.isLoading
|
||||
: symbolSearch.isLoading;
|
||||
|
||||
const handleSearch = () => {
|
||||
if (query.trim()) {
|
||||
setHasSearched(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSearch();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearchTypeChange = (value: SearchType) => {
|
||||
setSearchType(value);
|
||||
setHasSearched(false); // Reset search state when changing type
|
||||
};
|
||||
|
||||
const handleSearchModeChange = (value: SearchMode) => {
|
||||
setSearchMode(value);
|
||||
setHasSearched(false); // Reset search state when changing mode
|
||||
};
|
||||
|
||||
const handleQueryChange = (value: string) => {
|
||||
setQuery(value);
|
||||
setHasSearched(false); // Reset search state when query changes
|
||||
};
|
||||
|
||||
if (!enabled) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-12">
|
||||
<div className="text-center">
|
||||
<Search className="w-12 h-12 mx-auto text-muted-foreground/50 mb-4" />
|
||||
<h3 className="text-lg font-medium text-foreground mb-2">
|
||||
{formatMessage({ id: 'codexlens.search.notInstalled.title' })}
|
||||
</h3>
|
||||
<p className="text-muted-foreground">
|
||||
{formatMessage({ id: 'codexlens.search.notInstalled.description' })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Search Options */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Search Type */}
|
||||
<div className="space-y-2">
|
||||
<Label>{formatMessage({ id: 'codexlens.search.type' })}</Label>
|
||||
<Select value={searchType} onValueChange={handleSearchTypeChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="search">
|
||||
<div className="flex items-center gap-2">
|
||||
<Search className="w-4 h-4" />
|
||||
{formatMessage({ id: 'codexlens.search.content' })}
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="search_files">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileCode className="w-4 h-4" />
|
||||
{formatMessage({ id: 'codexlens.search.files' })}
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="symbol">
|
||||
<div className="flex items-center gap-2">
|
||||
<Code className="w-4 h-4" />
|
||||
{formatMessage({ id: 'codexlens.search.symbol' })}
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Search Mode - only for content and file search */}
|
||||
{searchType !== 'symbol' && (
|
||||
<div className="space-y-2">
|
||||
<Label>{formatMessage({ id: 'codexlens.search.mode' })}</Label>
|
||||
<Select value={searchMode} onValueChange={handleSearchModeChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="dense_rerank">
|
||||
{formatMessage({ id: 'codexlens.search.mode.semantic' })}
|
||||
</SelectItem>
|
||||
<SelectItem value="fts">
|
||||
{formatMessage({ id: 'codexlens.search.mode.exact' })}
|
||||
</SelectItem>
|
||||
<SelectItem value="fuzzy">
|
||||
{formatMessage({ id: 'codexlens.search.mode.fuzzy' })}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Query Input */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="search-query">{formatMessage({ id: 'codexlens.search.query' })}</Label>
|
||||
<Input
|
||||
id="search-query"
|
||||
placeholder={formatMessage({ id: 'codexlens.search.queryPlaceholder' })}
|
||||
value={query}
|
||||
onChange={(e) => handleQueryChange(e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Search Button */}
|
||||
<Button
|
||||
onClick={handleSearch}
|
||||
disabled={!query.trim() || isLoading}
|
||||
className="w-full"
|
||||
>
|
||||
<Search className={cn('w-4 h-4 mr-2', isLoading && 'animate-spin')} />
|
||||
{isLoading
|
||||
? formatMessage({ id: 'codexlens.search.searching' })
|
||||
: formatMessage({ id: 'codexlens.search.button' })
|
||||
}
|
||||
</Button>
|
||||
|
||||
{/* Results */}
|
||||
{hasSearched && !isLoading && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium">
|
||||
{formatMessage({ id: 'codexlens.search.results' })}
|
||||
</h3>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{searchType === 'symbol'
|
||||
? (symbolSearch.data?.success
|
||||
? `${symbolSearch.data.symbols?.length ?? 0} ${formatMessage({ id: 'codexlens.search.resultsCount' })}`
|
||||
: ''
|
||||
)
|
||||
: searchType === 'search'
|
||||
? (contentSearch.data?.success
|
||||
? `${contentSearch.data.results?.length ?? 0} ${formatMessage({ id: 'codexlens.search.resultsCount' })}`
|
||||
: ''
|
||||
)
|
||||
: (fileSearch.data?.success
|
||||
? `${fileSearch.data.results?.length ?? 0} ${formatMessage({ id: 'codexlens.search.resultsCount' })}`
|
||||
: ''
|
||||
)
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{searchType === 'symbol' && symbolSearch.data && (
|
||||
symbolSearch.data.success ? (
|
||||
<div className="rounded-lg border bg-muted/50 p-4">
|
||||
<pre className="text-xs overflow-auto max-h-96">
|
||||
{JSON.stringify(symbolSearch.data.symbols, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-destructive">
|
||||
{symbolSearch.data.error || formatMessage({ id: 'common.error' })}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
{searchType === 'search' && contentSearch.data && (
|
||||
contentSearch.data.success ? (
|
||||
<div className="rounded-lg border bg-muted/50 p-4">
|
||||
<pre className="text-xs overflow-auto max-h-96">
|
||||
{JSON.stringify(contentSearch.data.results, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-destructive">
|
||||
{contentSearch.data.error || formatMessage({ id: 'common.error' })}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
{searchType === 'search_files' && fileSearch.data && (
|
||||
fileSearch.data.success ? (
|
||||
<div className="rounded-lg border bg-muted/50 p-4">
|
||||
<pre className="text-xs overflow-auto max-h-96">
|
||||
{JSON.stringify(fileSearch.data.results, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-destructive">
|
||||
{fileSearch.data.error || formatMessage({ id: 'common.error' })}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SearchTab;
|
||||
@@ -19,6 +19,9 @@ export function ExecutionTab({ execution, isActive, onClick, onClose }: Executio
|
||||
// Simplify tool name (e.g., gemini-2.5-pro -> gemini)
|
||||
const toolNameShort = execution.tool.split('-')[0];
|
||||
|
||||
// Mode display - use icon for visual clarity
|
||||
const modeDisplay = execution.mode === 'write' ? '✏️' : '🔍';
|
||||
|
||||
// Status color mapping - using softer, semantic colors
|
||||
const statusColor = {
|
||||
running: 'bg-indigo-500 shadow-[0_0_6px_rgba(99,102,241,0.4)] animate-pulse',
|
||||
@@ -31,7 +34,7 @@ export function ExecutionTab({ execution, isActive, onClick, onClose }: Executio
|
||||
value={execution.id}
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
'gap-1.5 text-xs px-2.5 py-1 rounded-md border border-border/50 group',
|
||||
'gap-1.5 text-xs px-2.5 py-1 rounded-md border border-border/50 group shrink-0',
|
||||
isActive
|
||||
? 'bg-slate-100 dark:bg-slate-800 border-slate-300 dark:border-slate-600 shadow-sm'
|
||||
: 'bg-muted/30 hover:bg-muted/50 border-border/30',
|
||||
@@ -41,14 +44,14 @@ export function ExecutionTab({ execution, isActive, onClick, onClose }: Executio
|
||||
{/* Status indicator dot */}
|
||||
<span className={cn('w-1.5 h-1.5 rounded-full shrink-0', statusColor)} />
|
||||
|
||||
{/* Mode indicator */}
|
||||
<span className="text-[10px]" title={execution.mode}>
|
||||
{modeDisplay}
|
||||
</span>
|
||||
|
||||
{/* Simplified tool name */}
|
||||
<span className="font-medium text-[11px]">{toolNameShort}</span>
|
||||
|
||||
{/* Execution mode - show on hover */}
|
||||
<span className="opacity-0 group-hover:opacity-70 text-[10px] transition-opacity">
|
||||
{execution.mode}
|
||||
</span>
|
||||
|
||||
{/* Line count statistics - show on hover */}
|
||||
<span className="opacity-0 group-hover:opacity-50 text-[9px] tabular-nums transition-opacity">
|
||||
{execution.output.length}
|
||||
|
||||
@@ -12,6 +12,114 @@ export interface JsonDetectionResult {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to recover truncated JSON by completing brackets
|
||||
* This handles cases where JSON is split during streaming
|
||||
*/
|
||||
function tryRecoverTruncatedJson(content: string): Record<string, unknown> | null {
|
||||
const trimmed = content.trim();
|
||||
|
||||
// Must start with { to be recoverable JSON
|
||||
if (!trimmed.startsWith('{')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Count opening vs closing braces
|
||||
let openBraces = 0;
|
||||
let closeBraces = 0;
|
||||
let inString = false;
|
||||
let escapeNext = false;
|
||||
|
||||
for (let i = 0; i < trimmed.length; i++) {
|
||||
const char = trimmed[i];
|
||||
|
||||
if (escapeNext) {
|
||||
escapeNext = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '\\') {
|
||||
escapeNext = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '"') {
|
||||
inString = !inString;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!inString) {
|
||||
if (char === '{') openBraces++;
|
||||
if (char === '}') closeBraces++;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're missing closing braces, try to complete them
|
||||
if (openBraces > closeBraces) {
|
||||
const missingBraces = openBraces - closeBraces;
|
||||
const recovered = trimmed + '}'.repeat(missingBraces);
|
||||
|
||||
// Also close any open quote
|
||||
let finalRecovered = recovered;
|
||||
if (inString) {
|
||||
finalRecovered = recovered + '"';
|
||||
// Add closing braces after the quote
|
||||
finalRecovered = finalRecovered + '}'.repeat(missingBraces);
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(finalRecovered) as Record<string, unknown>;
|
||||
} catch {
|
||||
// Recovery failed, try one more approach
|
||||
}
|
||||
}
|
||||
|
||||
// Try parsing as-is first
|
||||
try {
|
||||
return JSON.parse(trimmed) as Record<string, unknown>;
|
||||
} catch {
|
||||
// If still failing, try to close any hanging structures
|
||||
// Remove trailing incomplete key/value and try again
|
||||
const lastCommaIndex = trimmed.lastIndexOf(',');
|
||||
if (lastCommaIndex > 0) {
|
||||
const truncated = trimmed.substring(0, lastCommaIndex) + '}';
|
||||
try {
|
||||
return JSON.parse(truncated) as Record<string, unknown>;
|
||||
} catch {
|
||||
// Still failed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect token usage stats pattern (common in CLI output)
|
||||
* Pattern: {"type":"result","status":"success","stats":{"total_tokens":...,"input_tokens":...,...}
|
||||
*/
|
||||
function detectTokenStats(content: string): Record<string, unknown> | null {
|
||||
// Check for common token stat patterns
|
||||
const patterns = [
|
||||
/"type"\s*:\s*"result"/,
|
||||
/"status"\s*:\s*"success"/,
|
||||
/"stats"\s*:\s*\{/,
|
||||
/"total_tokens"\s*:\s*\d+/,
|
||||
];
|
||||
|
||||
const matchCount = patterns.filter(p => p.test(content)).length;
|
||||
|
||||
// If at least 3 patterns match, this is likely token stats
|
||||
if (matchCount >= 3) {
|
||||
const recovered = tryRecoverTruncatedJson(content);
|
||||
if (recovered) {
|
||||
return recovered;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if a line contains JSON data
|
||||
* Supports multiple formats:
|
||||
@@ -20,17 +128,29 @@ export interface JsonDetectionResult {
|
||||
* - Tool Result: [Tool Result] status: {...}
|
||||
* - Embedded JSON: trailing JSON object
|
||||
* - Code block JSON: ```json ... ```
|
||||
* - Truncated JSON: handles streaming incomplete JSON
|
||||
*/
|
||||
export function detectJsonInLine(content: string): JsonDetectionResult {
|
||||
const trimmed = content.trim();
|
||||
|
||||
// 1. Direct JSON object or array
|
||||
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||
// First try normal parse
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
return { isJson: true, parsed: parsed as Record<string, unknown> };
|
||||
} catch {
|
||||
// Continue to other patterns
|
||||
// Normal parse failed, try recovery for truncated JSON
|
||||
const recovered = tryRecoverTruncatedJson(trimmed);
|
||||
if (recovered) {
|
||||
return { isJson: true, parsed: recovered };
|
||||
}
|
||||
|
||||
// Check for token stats pattern specifically
|
||||
const tokenStats = detectTokenStats(trimmed);
|
||||
if (tokenStats) {
|
||||
return { isJson: true, parsed: tokenStats };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// ========================================
|
||||
// Global CLI streaming monitor with multi-execution support
|
||||
|
||||
import { useEffect, useRef, useCallback, useState } from 'react';
|
||||
import { useEffect, useRef, useCallback, useState, useMemo, memo } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import {
|
||||
X,
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
RefreshCw,
|
||||
Search,
|
||||
ArrowDownToLine,
|
||||
Trash2,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
@@ -26,8 +27,6 @@ import { useActiveCliExecutions, useInvalidateActiveCliExecutions } from '@/hook
|
||||
|
||||
// New components for Tab + JSON Cards
|
||||
import { ExecutionTab } from './CliStreamMonitor/components/ExecutionTab';
|
||||
import { OutputLine } from './CliStreamMonitor/components/OutputLine';
|
||||
import { JsonCard } from './CliStreamMonitor/components/JsonCard';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
|
||||
@@ -95,55 +94,99 @@ function getBorderColorForType(type: CliOutputLine['type']): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single output line as a card
|
||||
* Extract content from a line (handle JSON with 'content' field)
|
||||
*/
|
||||
interface OutputLineCardProps {
|
||||
line: CliOutputLine;
|
||||
onCopy?: (content: string) => void;
|
||||
}
|
||||
|
||||
function OutputLineCard({ line, onCopy }: OutputLineCardProps) {
|
||||
const borderColor = getBorderColorForType(line.type);
|
||||
function extractContentFromLine(line: CliOutputLine): { content: string; isMarkdown: boolean } {
|
||||
const trimmed = line.content.trim();
|
||||
|
||||
// Check if line is JSON with 'content' field
|
||||
let contentToRender = trimmed;
|
||||
let isMarkdown = false;
|
||||
|
||||
try {
|
||||
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
if ('content' in parsed && typeof parsed.content === 'string') {
|
||||
contentToRender = parsed.content;
|
||||
// Check if content looks like markdown
|
||||
isMarkdown = !!contentToRender.match(/^#{1,6}\s|^\*{3,}$|^\s*[-*+]\s+|^\s*\d+\.\s+|\*\*.*?\*\*|`{3,}/m);
|
||||
const content = parsed.content;
|
||||
const isMarkdown = !!content.match(/^#{1,6}\s|^\*{3,}$|^\s*[-*+]\s+|^\s*\d+\.\s+|\*\*.*?\*\*|`{3,}/m);
|
||||
return { content, isMarkdown };
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Not valid JSON, use original content
|
||||
// Check if original content looks like markdown
|
||||
isMarkdown = !!trimmed.match(/^#{1,6}\s|^\*{3,}$|^\s*[-*+]\s+|^\s*\d+\.\s+|\*\*.*?\*\*|`{3,}/m);
|
||||
}
|
||||
|
||||
// Check if original content looks like markdown
|
||||
const isMarkdown = !!trimmed.match(/^#{1,6}\s|^\*{3,}$|^\s*[-*+]\s+|^\s*\d+\.\s+|\*\*.*?\*\*|`{3,}/m);
|
||||
return { content: trimmed, isMarkdown };
|
||||
}
|
||||
|
||||
/**
|
||||
* Group consecutive output lines by type
|
||||
*/
|
||||
interface OutputLineGroup {
|
||||
type: CliOutputLine['type'];
|
||||
lines: CliOutputLine[];
|
||||
}
|
||||
|
||||
function groupConsecutiveLinesByType(lines: CliOutputLine[]): OutputLineGroup[] {
|
||||
const groups: OutputLineGroup[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
// Start new group if type changes
|
||||
if (groups.length === 0 || groups[groups.length - 1].type !== line.type) {
|
||||
groups.push({
|
||||
type: line.type,
|
||||
lines: [line],
|
||||
});
|
||||
} else {
|
||||
// Append to existing group
|
||||
groups[groups.length - 1].lines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a group of output lines as a merged card
|
||||
*/
|
||||
interface OutputLineCardProps {
|
||||
group: OutputLineGroup;
|
||||
onCopy?: (content: string) => void;
|
||||
}
|
||||
|
||||
function OutputLineCard({ group, onCopy }: OutputLineCardProps) {
|
||||
const borderColor = getBorderColorForType(group.type);
|
||||
|
||||
// Extract content from all lines in the group
|
||||
const lineContents = group.lines.map(line => extractContentFromLine(line));
|
||||
|
||||
// Check if any line has markdown
|
||||
const hasMarkdown = lineContents.some(c => c.isMarkdown);
|
||||
|
||||
return (
|
||||
<div className={`border-l-2 rounded-r my-1 py-1 px-2 group relative bg-background ${borderColor}`}>
|
||||
<div className="pr-6">
|
||||
{isMarkdown ? (
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none text-xs leading-relaxed">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{contentToRender}
|
||||
</ReactMarkdown>
|
||||
<div className={`border-l-2 rounded-r my-1 py-1 px-2 group relative bg-background contain-content ${borderColor}`}>
|
||||
<div className="pr-6 space-y-1">
|
||||
{lineContents.map((item, index) => (
|
||||
<div key={index} className="contain-layout">
|
||||
{item.isMarkdown || hasMarkdown ? (
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none text-xs leading-relaxed contain-layout">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{item.content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-xs whitespace-pre-wrap break-words leading-relaxed contain-layout">
|
||||
{item.content}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-xs whitespace-pre-wrap break-words leading-relaxed">
|
||||
{contentToRender}
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Memoize the OutputLineCard component to prevent unnecessary re-renders
|
||||
const MemoizedOutputLineCard = memo(OutputLineCard);
|
||||
|
||||
// ========== Component ==========
|
||||
|
||||
export interface CliStreamMonitorProps {
|
||||
@@ -160,11 +203,15 @@ export function CliStreamMonitor({ isOpen, onClose }: CliStreamMonitorProps) {
|
||||
const [isUserScrolling, setIsUserScrolling] = useState(false);
|
||||
const [viewMode, setViewMode] = useState<'list' | 'blocks'>('list');
|
||||
|
||||
// Track last output length to detect new output
|
||||
const lastOutputLengthRef = useRef<Record<string, number>>({});
|
||||
|
||||
// Store state
|
||||
const executions = useCliStreamStore((state) => state.executions);
|
||||
const currentExecutionId = useCliStreamStore((state) => state.currentExecutionId);
|
||||
const setCurrentExecution = useCliStreamStore((state) => state.setCurrentExecution);
|
||||
const removeExecution = useCliStreamStore((state) => state.removeExecution);
|
||||
const markExecutionClosedByUser = useCliStreamStore((state) => state.markExecutionClosedByUser);
|
||||
|
||||
// Active execution sync
|
||||
const { isLoading: isSyncing, refetch } = useActiveCliExecutions(isOpen);
|
||||
@@ -264,21 +311,42 @@ export function CliStreamMonitor({ isOpen, onClose }: CliStreamMonitorProps) {
|
||||
});
|
||||
invalidateActive();
|
||||
}
|
||||
}, [lastMessage, currentExecutionId, setCurrentExecution, invalidateActive]);
|
||||
}, [lastMessage, invalidateActive]);
|
||||
|
||||
// Auto-scroll to bottom when new output arrives
|
||||
// Auto-scroll to bottom when new output arrives (optimized - only scroll when output length changes)
|
||||
useEffect(() => {
|
||||
if (autoScroll && !isUserScrolling && logsEndRef.current) {
|
||||
logsEndRef.current.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}, [executions, autoScroll, isUserScrolling, currentExecutionId]);
|
||||
if (!currentExecutionId || !autoScroll || isUserScrolling) return;
|
||||
|
||||
// Handle scroll to detect user scrolling
|
||||
const currentExecution = executions[currentExecutionId];
|
||||
if (!currentExecution) return;
|
||||
|
||||
const currentLength = currentExecution.output.length;
|
||||
const lastLength = lastOutputLengthRef.current[currentExecutionId] || 0;
|
||||
|
||||
// Only scroll if new output was added
|
||||
if (currentLength > lastLength) {
|
||||
lastOutputLengthRef.current[currentExecutionId] = currentLength;
|
||||
requestAnimationFrame(() => {
|
||||
if (logsEndRef.current) {
|
||||
logsEndRef.current.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [executions, currentExecutionId, autoScroll, isUserScrolling]);
|
||||
|
||||
// Handle scroll to detect user scrolling (with debounce for performance)
|
||||
const handleScrollRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const handleScroll = useCallback(() => {
|
||||
if (!logsContainerRef.current) return;
|
||||
const { scrollTop, scrollHeight, clientHeight } = logsContainerRef.current;
|
||||
const isAtBottom = scrollHeight - scrollTop - clientHeight < 50;
|
||||
setIsUserScrolling(!isAtBottom);
|
||||
if (handleScrollRef.current) {
|
||||
clearTimeout(handleScrollRef.current);
|
||||
}
|
||||
|
||||
handleScrollRef.current = setTimeout(() => {
|
||||
if (!logsContainerRef.current) return;
|
||||
const { scrollTop, scrollHeight, clientHeight } = logsContainerRef.current;
|
||||
const isAtBottom = scrollHeight - scrollTop - clientHeight < 50;
|
||||
setIsUserScrolling(!isAtBottom);
|
||||
}, 50); // 50ms debounce
|
||||
}, []);
|
||||
|
||||
// Scroll to bottom handler
|
||||
@@ -287,6 +355,28 @@ export function CliStreamMonitor({ isOpen, onClose }: CliStreamMonitorProps) {
|
||||
setIsUserScrolling(false);
|
||||
}, []);
|
||||
|
||||
// Handle closing an execution tab
|
||||
const handleCloseExecution = useCallback((executionId: string) => {
|
||||
// Mark as closed by user so it won't be re-added by server sync
|
||||
markExecutionClosedByUser(executionId);
|
||||
// Remove from local state
|
||||
removeExecution(executionId);
|
||||
// If this was the current execution, clear current selection
|
||||
if (currentExecutionId === executionId) {
|
||||
const remainingIds = Object.keys(executions).filter(id => id !== executionId);
|
||||
setCurrentExecution(remainingIds.length > 0 ? remainingIds[0] : null);
|
||||
}
|
||||
}, [markExecutionClosedByUser, removeExecution, currentExecutionId, executions, setCurrentExecution]);
|
||||
|
||||
// Close all executions
|
||||
const handleCloseAll = useCallback(() => {
|
||||
for (const id of Object.keys(executions)) {
|
||||
markExecutionClosedByUser(id);
|
||||
removeExecution(id);
|
||||
}
|
||||
setCurrentExecution(null);
|
||||
}, [markExecutionClosedByUser, removeExecution, executions, setCurrentExecution]);
|
||||
|
||||
// ESC key to close
|
||||
useEffect(() => {
|
||||
const handleEsc = (e: KeyboardEvent) => {
|
||||
@@ -302,27 +392,67 @@ export function CliStreamMonitor({ isOpen, onClose }: CliStreamMonitorProps) {
|
||||
return () => window.removeEventListener('keydown', handleEsc);
|
||||
}, [isOpen, onClose, searchQuery]);
|
||||
|
||||
// Get sorted execution IDs (running first, then by start time)
|
||||
const sortedExecutionIds = Object.keys(executions).sort((a, b) => {
|
||||
const execA = executions[a];
|
||||
const execB = executions[b];
|
||||
if (execA.status === 'running' && execB.status !== 'running') return -1;
|
||||
if (execA.status !== 'running' && execB.status === 'running') return 1;
|
||||
return execB.startTime - execA.startTime;
|
||||
});
|
||||
// Cleanup scroll handler timeout on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (handleScrollRef.current) {
|
||||
clearTimeout(handleScrollRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Active execution count for badge
|
||||
const activeCount = Object.values(executions).filter(e => e.status === 'running').length;
|
||||
// Get sorted execution IDs (memoized to avoid unnecessary recalculations)
|
||||
const sortedExecutionIds = useMemo(() => {
|
||||
return Object.keys(executions).sort((a, b) => {
|
||||
const execA = executions[a];
|
||||
const execB = executions[b];
|
||||
if (execA.status === 'running' && execB.status !== 'running') return -1;
|
||||
if (execA.status !== 'running' && execB.status === 'running') return 1;
|
||||
return execB.startTime - execA.startTime;
|
||||
});
|
||||
}, [executions]);
|
||||
|
||||
// Current execution
|
||||
const currentExecution = currentExecutionId ? executions[currentExecutionId] : null;
|
||||
// Active execution count for badge (memoized)
|
||||
const activeCount = useMemo(() => {
|
||||
return Object.values(executions).filter(e => e.status === 'running').length;
|
||||
}, [executions]);
|
||||
|
||||
// Filter output lines based on search
|
||||
const filteredOutput = currentExecution && searchQuery
|
||||
? currentExecution.output.filter(line =>
|
||||
// Current execution (memoized)
|
||||
const currentExecution = useMemo(() => {
|
||||
return currentExecutionId ? executions[currentExecutionId] : null;
|
||||
}, [currentExecutionId, executions]);
|
||||
|
||||
// Maximum lines to display (for performance)
|
||||
const MAX_DISPLAY_LINES = 1000;
|
||||
|
||||
// Filter output lines based on search (memoized with limit)
|
||||
const filteredOutput = useMemo(() => {
|
||||
if (!currentExecution) return [];
|
||||
|
||||
let output = currentExecution.output;
|
||||
|
||||
// Apply search filter
|
||||
if (searchQuery) {
|
||||
output = output.filter(line =>
|
||||
line.content.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
: currentExecution?.output || [];
|
||||
);
|
||||
}
|
||||
|
||||
// Limit display for performance
|
||||
if (output.length > MAX_DISPLAY_LINES) {
|
||||
return output.slice(-MAX_DISPLAY_LINES);
|
||||
}
|
||||
|
||||
return output;
|
||||
}, [currentExecution, searchQuery]);
|
||||
|
||||
// Check if output was truncated
|
||||
const isOutputTruncated = currentExecution && currentExecution.output.length > MAX_DISPLAY_LINES;
|
||||
|
||||
// Group output lines by type (memoized for performance)
|
||||
const groupedOutput = useMemo(() => {
|
||||
return groupConsecutiveLinesByType(filteredOutput);
|
||||
}, [filteredOutput]);
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
@@ -367,6 +497,16 @@ export function CliStreamMonitor({ isOpen, onClose }: CliStreamMonitorProps) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{sortedExecutionIds.length > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleCloseAll}
|
||||
title="Close all executions"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -390,7 +530,7 @@ export function CliStreamMonitor({ isOpen, onClose }: CliStreamMonitorProps) {
|
||||
onValueChange={(v) => setCurrentExecution(v || null)}
|
||||
className="w-full"
|
||||
>
|
||||
<TabsList className="w-full h-auto flex-wrap gap-1 bg-secondary/50 p-1">
|
||||
<TabsList className="w-full h-auto gap-1 bg-secondary/50 p-1 overflow-x-auto overflow-y-hidden no-scrollbar">
|
||||
{sortedExecutionIds.map((id) => (
|
||||
<ExecutionTab
|
||||
key={id}
|
||||
@@ -399,7 +539,7 @@ export function CliStreamMonitor({ isOpen, onClose }: CliStreamMonitorProps) {
|
||||
onClick={() => setCurrentExecution(id)}
|
||||
onClose={(e) => {
|
||||
e.stopPropagation();
|
||||
removeExecution(id);
|
||||
handleCloseExecution(id);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
@@ -472,26 +612,27 @@ export function CliStreamMonitor({ isOpen, onClose }: CliStreamMonitorProps) {
|
||||
) : (
|
||||
<div
|
||||
ref={logsContainerRef}
|
||||
className="h-full overflow-y-auto p-3 font-mono text-xs bg-background"
|
||||
className="h-full overflow-y-auto p-3 font-mono text-xs bg-background contain-strict"
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
{isOutputTruncated && (
|
||||
<div className="mb-2 p-2 bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-800 rounded text-amber-800 dark:text-amber-200 text-xs">
|
||||
Showing last {MAX_DISPLAY_LINES} lines of {currentExecution?.output.length} total lines. Use search to find specific content.
|
||||
</div>
|
||||
)}
|
||||
{filteredOutput.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-full text-muted-foreground">
|
||||
{searchQuery ? 'No matching output found' : 'Waiting for output...'}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{(() => {
|
||||
// Group output lines by type
|
||||
const groupedOutput = groupOutputLines(filteredOutput);
|
||||
return groupedOutput.map((group, groupIndex) => (
|
||||
<OutputGroupRenderer
|
||||
key={`group-${group.type}-${groupIndex}`}
|
||||
group={group}
|
||||
onCopy={(content) => navigator.clipboard.writeText(content)}
|
||||
/>
|
||||
));
|
||||
})()}
|
||||
<div>
|
||||
{groupedOutput.map((group, groupIndex) => (
|
||||
<MemoizedOutputLineCard
|
||||
key={`group-${group.type}-${groupIndex}`}
|
||||
group={group}
|
||||
onCopy={(content) => navigator.clipboard.writeText(content)}
|
||||
/>
|
||||
))}
|
||||
<div ref={logsEndRef} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -105,8 +105,12 @@ export function useActiveCliExecutions(
|
||||
refetchInterval: number = 5000
|
||||
) {
|
||||
const upsertExecution = useCliStreamStore(state => state.upsertExecution);
|
||||
const removeExecution = useCliStreamStore(state => state.removeExecution);
|
||||
const executions = useCliStreamStore(state => state.executions);
|
||||
const setCurrentExecution = useCliStreamStore(state => state.setCurrentExecution);
|
||||
const markExecutionClosedByUser = useCliStreamStore(state => state.markExecutionClosedByUser);
|
||||
const isExecutionClosedByUser = useCliStreamStore(state => state.isExecutionClosedByUser);
|
||||
const cleanupUserClosedExecutions = useCliStreamStore(state => state.cleanupUserClosedExecutions);
|
||||
|
||||
return useQuery({
|
||||
queryKey: ACTIVE_CLI_EXECUTIONS_QUERY_KEY,
|
||||
@@ -117,11 +121,33 @@ export function useActiveCliExecutions(
|
||||
}
|
||||
const data: ActiveCliExecutionsResponse = await response.json();
|
||||
|
||||
// Get server execution IDs
|
||||
const serverIds = new Set(data.executions.map(e => e.id));
|
||||
|
||||
// Clean up userClosedExecutions - remove those no longer on server
|
||||
cleanupUserClosedExecutions(serverIds);
|
||||
|
||||
// Remove executions that are no longer on server and were closed by user
|
||||
for (const [id, exec] of Object.entries(executions)) {
|
||||
if (isExecutionClosedByUser(id)) {
|
||||
// User closed this execution, remove from local state
|
||||
removeExecution(id);
|
||||
} else if (exec.status !== 'running' && !serverIds.has(id) && exec.recovered) {
|
||||
// Not running, not on server, and was recovered (not user-created)
|
||||
removeExecution(id);
|
||||
}
|
||||
}
|
||||
|
||||
// Process executions and sync to store
|
||||
let hasNewExecution = false;
|
||||
const now = Date.now();
|
||||
|
||||
for (const exec of data.executions) {
|
||||
// Skip if user closed this execution
|
||||
if (isExecutionClosedByUser(exec.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const existing = executions[exec.id];
|
||||
const historicalOutput = parseHistoricalOutput(exec.output || '', exec.startTime);
|
||||
|
||||
@@ -175,7 +201,7 @@ export function useActiveCliExecutions(
|
||||
|
||||
// Set current execution to first running execution if none selected
|
||||
if (hasNewExecution) {
|
||||
const runningExec = data.executions.find(e => e.status === 'running');
|
||||
const runningExec = data.executions.find(e => e.status === 'running' && !isExecutionClosedByUser(e.id));
|
||||
if (runningExec && !executions[runningExec.id]) {
|
||||
setCurrentExecution(runningExec.id);
|
||||
}
|
||||
|
||||
623
ccw/frontend/src/hooks/useApiSettings.ts
Normal file
623
ccw/frontend/src/hooks/useApiSettings.ts
Normal file
@@ -0,0 +1,623 @@
|
||||
// ========================================
|
||||
// useApiSettings Hook
|
||||
// ========================================
|
||||
// TanStack Query hooks for API Settings management
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
fetchProviders,
|
||||
createProvider,
|
||||
updateProvider,
|
||||
deleteProvider,
|
||||
testProvider,
|
||||
testProviderKey,
|
||||
getProviderHealthStatus,
|
||||
triggerProviderHealthCheck,
|
||||
fetchEndpoints,
|
||||
createEndpoint,
|
||||
updateEndpoint,
|
||||
deleteEndpoint,
|
||||
fetchCacheStats,
|
||||
clearCache,
|
||||
updateCacheSettings,
|
||||
fetchModelPools,
|
||||
fetchModelPool,
|
||||
createModelPool,
|
||||
updateModelPool,
|
||||
deleteModelPool,
|
||||
getAvailableModelsForPool,
|
||||
discoverModelsForPool,
|
||||
fetchApiConfig,
|
||||
syncApiConfig,
|
||||
previewYamlConfig,
|
||||
checkCcwLitellmStatus,
|
||||
installCcwLitellm,
|
||||
uninstallCcwLitellm,
|
||||
type ProviderCredential,
|
||||
type CustomEndpoint,
|
||||
type CacheStats,
|
||||
type GlobalCacheSettings,
|
||||
type ModelPoolConfig,
|
||||
type ModelPoolType,
|
||||
type DiscoveredProvider,
|
||||
} from '../lib/api';
|
||||
|
||||
// Query key factory
|
||||
export const apiSettingsKeys = {
|
||||
all: ['apiSettings'] as const,
|
||||
providers: () => [...apiSettingsKeys.all, 'providers'] as const,
|
||||
provider: (id: string) => [...apiSettingsKeys.providers(), id] as const,
|
||||
endpoints: () => [...apiSettingsKeys.all, 'endpoints'] as const,
|
||||
endpoint: (id: string) => [...apiSettingsKeys.endpoints(), id] as const,
|
||||
cache: () => [...apiSettingsKeys.all, 'cache'] as const,
|
||||
modelPools: () => [...apiSettingsKeys.all, 'modelPools'] as const,
|
||||
modelPool: (id: string) => [...apiSettingsKeys.modelPools(), id] as const,
|
||||
ccwLitellm: () => [...apiSettingsKeys.all, 'ccwLitellm'] as const,
|
||||
};
|
||||
|
||||
const STALE_TIME = 2 * 60 * 1000;
|
||||
|
||||
// ========================================
|
||||
// Provider Hooks
|
||||
// ========================================
|
||||
|
||||
export interface UseProvidersOptions {
|
||||
staleTime?: number;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface UseProvidersReturn {
|
||||
providers: ProviderCredential[];
|
||||
totalCount: number;
|
||||
isLoading: boolean;
|
||||
isFetching: boolean;
|
||||
error: Error | null;
|
||||
refetch: () => Promise<void>;
|
||||
invalidate: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function useProviders(options: UseProvidersOptions = {}): UseProvidersReturn {
|
||||
const { staleTime = STALE_TIME, enabled = true } = options;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: apiSettingsKeys.providers(),
|
||||
queryFn: fetchProviders,
|
||||
staleTime,
|
||||
enabled,
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
const providers = query.data?.providers ?? [];
|
||||
|
||||
const refetch = async () => {
|
||||
await query.refetch();
|
||||
};
|
||||
|
||||
const invalidate = async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: apiSettingsKeys.providers() });
|
||||
};
|
||||
|
||||
return {
|
||||
providers,
|
||||
totalCount: providers.length,
|
||||
isLoading: query.isLoading,
|
||||
isFetching: query.isFetching,
|
||||
error: query.error,
|
||||
refetch,
|
||||
invalidate,
|
||||
};
|
||||
}
|
||||
|
||||
export function useCreateProvider() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (provider: Omit<ProviderCredential, 'id' | 'createdAt' | 'updatedAt'>) =>
|
||||
createProvider(provider),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.providers() });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
createProvider: mutation.mutateAsync,
|
||||
isCreating: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
export function useUpdateProvider() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: ({ providerId, updates }: { providerId: string; updates: Partial<Omit<ProviderCredential, 'id' | 'createdAt' | 'updatedAt'>> }) =>
|
||||
updateProvider(providerId, updates),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.providers() });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
updateProvider: (providerId: string, updates: Partial<Omit<ProviderCredential, 'id' | 'createdAt' | 'updatedAt'>>) =>
|
||||
mutation.mutateAsync({ providerId, updates }),
|
||||
isUpdating: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
export function useDeleteProvider() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (providerId: string) => deleteProvider(providerId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.providers() });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
deleteProvider: mutation.mutateAsync,
|
||||
isDeleting: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
export function useTestProvider() {
|
||||
const mutation = useMutation({
|
||||
mutationFn: (providerId: string) => testProvider(providerId),
|
||||
});
|
||||
|
||||
return {
|
||||
testProvider: mutation.mutateAsync,
|
||||
isTesting: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
export function useTestProviderKey() {
|
||||
const mutation = useMutation({
|
||||
mutationFn: ({ providerId, keyId }: { providerId: string; keyId: string }) =>
|
||||
testProviderKey(providerId, keyId),
|
||||
});
|
||||
|
||||
return {
|
||||
testProviderKey: mutation.mutateAsync,
|
||||
isTesting: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
export function useProviderHealthStatus(providerId: string) {
|
||||
return useQuery({
|
||||
queryKey: [...apiSettingsKeys.provider(providerId), 'health'],
|
||||
queryFn: () => getProviderHealthStatus(providerId),
|
||||
enabled: !!providerId,
|
||||
staleTime: 30000, // 30 seconds
|
||||
refetchInterval: 60000, // Refetch every minute
|
||||
});
|
||||
}
|
||||
|
||||
export function useTriggerProviderHealthCheck() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (providerId: string) => triggerProviderHealthCheck(providerId),
|
||||
onSuccess: (_, providerId) => {
|
||||
queryClient.invalidateQueries({ queryKey: [...apiSettingsKeys.provider(providerId), 'health'] });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
triggerHealthCheck: mutation.mutateAsync,
|
||||
isChecking: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Endpoint Hooks
|
||||
// ========================================
|
||||
|
||||
export interface UseEndpointsOptions {
|
||||
staleTime?: number;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface UseEndpointsReturn {
|
||||
endpoints: CustomEndpoint[];
|
||||
totalCount: number;
|
||||
cachedCount: number;
|
||||
isLoading: boolean;
|
||||
isFetching: boolean;
|
||||
error: Error | null;
|
||||
refetch: () => Promise<void>;
|
||||
invalidate: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function useEndpoints(options: UseEndpointsOptions = {}): UseEndpointsReturn {
|
||||
const { staleTime = STALE_TIME, enabled = true } = options;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: apiSettingsKeys.endpoints(),
|
||||
queryFn: fetchEndpoints,
|
||||
staleTime,
|
||||
enabled,
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
const endpoints = query.data?.endpoints ?? [];
|
||||
const cachedEndpoints = endpoints.filter((e) => e.cacheStrategy.enabled);
|
||||
|
||||
const refetch = async () => {
|
||||
await query.refetch();
|
||||
};
|
||||
|
||||
const invalidate = async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: apiSettingsKeys.endpoints() });
|
||||
};
|
||||
|
||||
return {
|
||||
endpoints,
|
||||
totalCount: endpoints.length,
|
||||
cachedCount: cachedEndpoints.length,
|
||||
isLoading: query.isLoading,
|
||||
isFetching: query.isFetching,
|
||||
error: query.error,
|
||||
refetch,
|
||||
invalidate,
|
||||
};
|
||||
}
|
||||
|
||||
export function useCreateEndpoint() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (endpoint: Omit<CustomEndpoint, 'createdAt' | 'updatedAt'>) =>
|
||||
createEndpoint(endpoint),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.endpoints() });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
createEndpoint: mutation.mutateAsync,
|
||||
isCreating: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
export function useUpdateEndpoint() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: ({ endpointId, updates }: { endpointId: string; updates: Partial<Omit<CustomEndpoint, 'id' | 'createdAt' | 'updatedAt'>> }) =>
|
||||
updateEndpoint(endpointId, updates),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.endpoints() });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
updateEndpoint: (endpointId: string, updates: Partial<Omit<CustomEndpoint, 'id' | 'createdAt' | 'updatedAt'>>) =>
|
||||
mutation.mutateAsync({ endpointId, updates }),
|
||||
isUpdating: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
export function useDeleteEndpoint() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (endpointId: string) => deleteEndpoint(endpointId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.endpoints() });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
deleteEndpoint: mutation.mutateAsync,
|
||||
isDeleting: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Cache Hooks
|
||||
// ========================================
|
||||
|
||||
export interface UseCacheStatsOptions {
|
||||
staleTime?: number;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface UseCacheStatsReturn {
|
||||
stats: CacheStats | null;
|
||||
isLoading: boolean;
|
||||
isFetching: boolean;
|
||||
error: Error | null;
|
||||
refetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function useCacheStats(options: UseCacheStatsOptions = {}): UseCacheStatsReturn {
|
||||
const { staleTime = 30000, enabled = true } = options; // 30 seconds stale time for cache stats
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: apiSettingsKeys.cache(),
|
||||
queryFn: fetchCacheStats,
|
||||
staleTime,
|
||||
enabled,
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
const refetch = async () => {
|
||||
await query.refetch();
|
||||
};
|
||||
|
||||
return {
|
||||
stats: query.data ?? null,
|
||||
isLoading: query.isLoading,
|
||||
isFetching: query.isFetching,
|
||||
error: query.error,
|
||||
refetch,
|
||||
};
|
||||
}
|
||||
|
||||
export function useClearCache() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () => clearCache(),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.cache() });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
clearCache: mutation.mutateAsync,
|
||||
isClearing: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
export function useUpdateCacheSettings() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (settings: Partial<{ enabled: boolean; cacheDir: string; maxTotalSizeMB: number }>) =>
|
||||
updateCacheSettings(settings),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.cache() });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
updateCacheSettings: mutation.mutateAsync,
|
||||
isUpdating: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Model Pool Hooks
|
||||
// ========================================
|
||||
|
||||
export interface UseModelPoolsOptions {
|
||||
staleTime?: number;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface UseModelPoolsReturn {
|
||||
pools: ModelPoolConfig[];
|
||||
totalCount: number;
|
||||
enabledCount: number;
|
||||
isLoading: boolean;
|
||||
isFetching: boolean;
|
||||
error: Error | null;
|
||||
refetch: () => Promise<void>;
|
||||
invalidate: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function useModelPools(options: UseModelPoolsOptions = {}): UseModelPoolsReturn {
|
||||
const { staleTime = STALE_TIME, enabled = true } = options;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: apiSettingsKeys.modelPools(),
|
||||
queryFn: fetchModelPools,
|
||||
staleTime,
|
||||
enabled,
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
const pools = query.data?.pools ?? [];
|
||||
const enabledPools = pools.filter((p) => p.enabled);
|
||||
|
||||
const refetch = async () => {
|
||||
await query.refetch();
|
||||
};
|
||||
|
||||
const invalidate = async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: apiSettingsKeys.modelPools() });
|
||||
};
|
||||
|
||||
return {
|
||||
pools,
|
||||
totalCount: pools.length,
|
||||
enabledCount: enabledPools.length,
|
||||
isLoading: query.isLoading,
|
||||
isFetching: query.isFetching,
|
||||
error: query.error,
|
||||
refetch,
|
||||
invalidate,
|
||||
};
|
||||
}
|
||||
|
||||
export function useModelPool(poolId: string) {
|
||||
return useQuery({
|
||||
queryKey: apiSettingsKeys.modelPool(poolId),
|
||||
queryFn: () => fetchModelPool(poolId),
|
||||
enabled: !!poolId,
|
||||
staleTime: STALE_TIME,
|
||||
retry: 2,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateModelPool() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (pool: Omit<ModelPoolConfig, 'id'>) => createModelPool(pool),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.modelPools() });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
createModelPool: mutation.mutateAsync,
|
||||
isCreating: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
export function useUpdateModelPool() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: ({ poolId, updates }: { poolId: string; updates: Partial<ModelPoolConfig> }) =>
|
||||
updateModelPool(poolId, updates),
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.modelPools() });
|
||||
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.modelPool(variables.poolId) });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
updateModelPool: (poolId: string, updates: Partial<ModelPoolConfig>) =>
|
||||
mutation.mutateAsync({ poolId, updates }),
|
||||
isUpdating: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
export function useDeleteModelPool() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (poolId: string) => deleteModelPool(poolId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.modelPools() });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
deleteModelPool: mutation.mutateAsync,
|
||||
isDeleting: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
export function useAvailableModelsForPool(modelType: ModelPoolType) {
|
||||
return useQuery({
|
||||
queryKey: [...apiSettingsKeys.modelPools(), 'available', modelType],
|
||||
queryFn: () => getAvailableModelsForPool(modelType),
|
||||
enabled: !!modelType,
|
||||
staleTime: STALE_TIME,
|
||||
});
|
||||
}
|
||||
|
||||
export function useDiscoverModelsForPool() {
|
||||
const mutation = useMutation({
|
||||
mutationFn: ({ modelType, targetModel }: { modelType: ModelPoolType; targetModel: string }) =>
|
||||
discoverModelsForPool(modelType, targetModel),
|
||||
});
|
||||
|
||||
return {
|
||||
discoverModels: (modelType: ModelPoolType, targetModel: string) =>
|
||||
mutation.mutateAsync({ modelType, targetModel }),
|
||||
isDiscovering: mutation.isPending,
|
||||
error: mutation.error,
|
||||
data: mutation.data,
|
||||
};
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Config Hooks
|
||||
// ========================================
|
||||
|
||||
export function useApiConfig() {
|
||||
return useQuery({
|
||||
queryKey: [...apiSettingsKeys.all, 'config'],
|
||||
queryFn: fetchApiConfig,
|
||||
staleTime: STALE_TIME,
|
||||
});
|
||||
}
|
||||
|
||||
export function useSyncApiConfig() {
|
||||
return useMutation({
|
||||
mutationFn: () => syncApiConfig(),
|
||||
});
|
||||
}
|
||||
|
||||
export function usePreviewYamlConfig() {
|
||||
return useMutation({
|
||||
mutationFn: () => previewYamlConfig(),
|
||||
});
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// CCW-LiteLLM Package Hooks
|
||||
// ========================================
|
||||
|
||||
export interface UseCcwLitellmStatusOptions {
|
||||
staleTime?: number;
|
||||
enabled?: boolean;
|
||||
refresh?: boolean;
|
||||
}
|
||||
|
||||
export function useCcwLitellmStatus(options: UseCcwLitellmStatusOptions = {}) {
|
||||
const { staleTime = 5 * 60 * 1000, enabled = true, refresh = false } = options;
|
||||
|
||||
return useQuery({
|
||||
queryKey: [...apiSettingsKeys.ccwLitellm(), 'status', refresh],
|
||||
queryFn: () => checkCcwLitellmStatus(refresh),
|
||||
staleTime,
|
||||
enabled,
|
||||
});
|
||||
}
|
||||
|
||||
export function useInstallCcwLitellm() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () => installCcwLitellm(),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.ccwLitellm() });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
install: mutation.mutateAsync,
|
||||
isInstalling: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
export function useUninstallCcwLitellm() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () => uninstallCcwLitellm(),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.ccwLitellm() });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
uninstall: mutation.mutateAsync,
|
||||
isUninstalling: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
@@ -26,6 +26,14 @@ import {
|
||||
resetCodexLensGpu,
|
||||
fetchCodexLensIgnorePatterns,
|
||||
updateCodexLensIgnorePatterns,
|
||||
searchCodexLens,
|
||||
searchFilesCodexLens,
|
||||
searchSymbolCodexLens,
|
||||
fetchCodexLensIndexes,
|
||||
rebuildCodexLensIndex,
|
||||
updateCodexLensIndex,
|
||||
cancelCodexLensIndexing,
|
||||
checkCodexLensIndexingStatus,
|
||||
type CodexLensDashboardInitResponse,
|
||||
type CodexLensVenvStatus,
|
||||
type CodexLensConfig,
|
||||
@@ -39,6 +47,11 @@ import {
|
||||
type CodexLensUpdateEnvRequest,
|
||||
type CodexLensUpdateIgnorePatternsRequest,
|
||||
type CodexLensWorkspaceStatus,
|
||||
type CodexLensSearchParams,
|
||||
type CodexLensSearchResponse,
|
||||
type CodexLensSymbolSearchResponse,
|
||||
type CodexLensIndexesResponse,
|
||||
type CodexLensIndexingStatusResponse,
|
||||
} from '../lib/api';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
|
||||
@@ -56,6 +69,11 @@ export const codexLensKeys = {
|
||||
gpuList: () => [...codexLensKeys.gpu(), 'list'] as const,
|
||||
gpuDetect: () => [...codexLensKeys.gpu(), 'detect'] as const,
|
||||
ignorePatterns: () => [...codexLensKeys.all, 'ignorePatterns'] as const,
|
||||
indexes: () => [...codexLensKeys.all, 'indexes'] as const,
|
||||
indexingStatus: () => [...codexLensKeys.all, 'indexingStatus'] as const,
|
||||
search: (params: CodexLensSearchParams) => [...codexLensKeys.all, 'search', params] as const,
|
||||
filesSearch: (params: CodexLensSearchParams) => [...codexLensKeys.all, 'filesSearch', params] as const,
|
||||
symbolSearch: (params: Pick<CodexLensSearchParams, 'query' | 'limit'>) => [...codexLensKeys.all, 'symbolSearch', params] as const,
|
||||
};
|
||||
|
||||
// Default stale times
|
||||
@@ -715,6 +733,189 @@ export function useUpdateIgnorePatterns(): UseUpdateIgnorePatternsReturn {
|
||||
};
|
||||
}
|
||||
|
||||
// ========== Index Management Hooks ==========
|
||||
|
||||
export interface UseCodexLensIndexesOptions {
|
||||
enabled?: boolean;
|
||||
staleTime?: number;
|
||||
}
|
||||
|
||||
export interface UseCodexLensIndexesReturn {
|
||||
data: CodexLensIndexesResponse | undefined;
|
||||
indexes: CodexLensIndexesResponse['indexes'] | undefined;
|
||||
isLoading: boolean;
|
||||
error: Error | null;
|
||||
refetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for fetching CodexLens indexes
|
||||
*/
|
||||
export function useCodexLensIndexes(options: UseCodexLensIndexesOptions = {}): UseCodexLensIndexesReturn {
|
||||
const { enabled = true, staleTime = STALE_TIME_MEDIUM } = options;
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: codexLensKeys.indexes(),
|
||||
queryFn: fetchCodexLensIndexes,
|
||||
staleTime,
|
||||
enabled,
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
const refetch = async () => {
|
||||
await query.refetch();
|
||||
};
|
||||
|
||||
return {
|
||||
data: query.data,
|
||||
indexes: query.data?.indexes,
|
||||
isLoading: query.isLoading,
|
||||
error: query.error,
|
||||
refetch,
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseCodexLensIndexingStatusReturn {
|
||||
data: CodexLensIndexingStatusResponse | undefined;
|
||||
inProgress: boolean;
|
||||
isLoading: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for checking CodexLens indexing status
|
||||
*/
|
||||
export function useCodexLensIndexingStatus(): UseCodexLensIndexingStatusReturn {
|
||||
const query = useQuery({
|
||||
queryKey: codexLensKeys.indexingStatus(),
|
||||
queryFn: checkCodexLensIndexingStatus,
|
||||
staleTime: STALE_TIME_SHORT,
|
||||
refetchInterval: (data) => (data?.inProgress ? 2000 : false), // Poll every 2s when indexing
|
||||
retry: false,
|
||||
});
|
||||
|
||||
return {
|
||||
data: query.data,
|
||||
inProgress: query.data?.inProgress ?? false,
|
||||
isLoading: query.isLoading,
|
||||
error: query.error,
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseRebuildIndexReturn {
|
||||
rebuildIndex: (projectPath: string, options?: {
|
||||
indexType?: 'normal' | 'vector';
|
||||
embeddingModel?: string;
|
||||
embeddingBackend?: 'fastembed' | 'litellm';
|
||||
maxWorkers?: number;
|
||||
}) => Promise<{ success: boolean; message?: string; error?: string }>;
|
||||
isRebuilding: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for rebuilding CodexLens index (full rebuild)
|
||||
*/
|
||||
export function useRebuildIndex(): UseRebuildIndexReturn {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async ({
|
||||
projectPath,
|
||||
options = {},
|
||||
}: {
|
||||
projectPath: string;
|
||||
options?: {
|
||||
indexType?: 'normal' | 'vector';
|
||||
embeddingModel?: string;
|
||||
embeddingBackend?: 'fastembed' | 'litellm';
|
||||
maxWorkers?: number;
|
||||
};
|
||||
}) => rebuildCodexLensIndex(projectPath, options),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: codexLensKeys.indexes() });
|
||||
queryClient.invalidateQueries({ queryKey: codexLensKeys.dashboard() });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
rebuildIndex: (projectPath, options) =>
|
||||
mutation.mutateAsync({ projectPath, options }),
|
||||
isRebuilding: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseUpdateIndexReturn {
|
||||
updateIndex: (projectPath: string, options?: {
|
||||
indexType?: 'normal' | 'vector';
|
||||
embeddingModel?: string;
|
||||
embeddingBackend?: 'fastembed' | 'litellm';
|
||||
maxWorkers?: number;
|
||||
}) => Promise<{ success: boolean; message?: string; error?: string }>;
|
||||
isUpdating: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for updating CodexLens index (incremental update)
|
||||
*/
|
||||
export function useUpdateIndex(): UseUpdateIndexReturn {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async ({
|
||||
projectPath,
|
||||
options = {},
|
||||
}: {
|
||||
projectPath: string;
|
||||
options?: {
|
||||
indexType?: 'normal' | 'vector';
|
||||
embeddingModel?: string;
|
||||
embeddingBackend?: 'fastembed' | 'litellm';
|
||||
maxWorkers?: number;
|
||||
};
|
||||
}) => updateCodexLensIndex(projectPath, options),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: codexLensKeys.indexes() });
|
||||
queryClient.invalidateQueries({ queryKey: codexLensKeys.dashboard() });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
updateIndex: (projectPath, options) =>
|
||||
mutation.mutateAsync({ projectPath, options }),
|
||||
isUpdating: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseCancelIndexingReturn {
|
||||
cancelIndexing: () => Promise<{ success: boolean; error?: string }>;
|
||||
isCancelling: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for canceling CodexLens indexing
|
||||
*/
|
||||
export function useCancelIndexing(): UseCancelIndexingReturn {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: cancelCodexLensIndexing,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: codexLensKeys.indexingStatus() });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
cancelIndexing: mutation.mutateAsync,
|
||||
isCancelling: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Combined hook for all CodexLens mutations
|
||||
*/
|
||||
@@ -727,6 +928,9 @@ export function useCodexLensMutations() {
|
||||
const updateEnv = useUpdateCodexLensEnv();
|
||||
const gpu = useSelectGpu();
|
||||
const updatePatterns = useUpdateIgnorePatterns();
|
||||
const rebuildIndex = useRebuildIndex();
|
||||
const updateIndex = useUpdateIndex();
|
||||
const cancelIndexing = useCancelIndexing();
|
||||
|
||||
return {
|
||||
updateConfig: updateConfig.updateConfig,
|
||||
@@ -748,6 +952,12 @@ export function useCodexLensMutations() {
|
||||
isSelectingGpu: gpu.isSelecting || gpu.isResetting,
|
||||
updatePatterns: updatePatterns.updatePatterns,
|
||||
isUpdatingPatterns: updatePatterns.isUpdating,
|
||||
rebuildIndex: rebuildIndex.rebuildIndex,
|
||||
isRebuildingIndex: rebuildIndex.isRebuilding,
|
||||
updateIndex: updateIndex.updateIndex,
|
||||
isUpdatingIndex: updateIndex.isUpdating,
|
||||
cancelIndexing: cancelIndexing.cancelIndexing,
|
||||
isCancellingIndexing: cancelIndexing.isCancelling,
|
||||
isMutating:
|
||||
updateConfig.isUpdating ||
|
||||
bootstrap.isBootstrapping ||
|
||||
@@ -757,6 +967,119 @@ export function useCodexLensMutations() {
|
||||
updateEnv.isUpdating ||
|
||||
gpu.isSelecting ||
|
||||
gpu.isResetting ||
|
||||
updatePatterns.isUpdating,
|
||||
updatePatterns.isUpdating ||
|
||||
rebuildIndex.isRebuilding ||
|
||||
updateIndex.isUpdating ||
|
||||
cancelIndexing.isCancelling,
|
||||
};
|
||||
}
|
||||
|
||||
// ========== Search Hooks ==========
|
||||
|
||||
export interface UseCodexLensSearchOptions {
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface UseCodexLensSearchReturn {
|
||||
data: CodexLensSearchResponse | undefined;
|
||||
results: CodexLensSearchResponse['results'] | undefined;
|
||||
isLoading: boolean;
|
||||
error: Error | null;
|
||||
refetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for content search using CodexLens
|
||||
*/
|
||||
export function useCodexLensSearch(params: CodexLensSearchParams, options: UseCodexLensSearchOptions = {}): UseCodexLensSearchReturn {
|
||||
const { enabled = false } = options;
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: codexLensKeys.search(params),
|
||||
queryFn: () => searchCodexLens(params),
|
||||
enabled,
|
||||
staleTime: STALE_TIME_SHORT,
|
||||
retry: 1,
|
||||
});
|
||||
|
||||
const refetch = async () => {
|
||||
await query.refetch();
|
||||
};
|
||||
|
||||
return {
|
||||
data: query.data,
|
||||
results: query.data?.results,
|
||||
isLoading: query.isLoading,
|
||||
error: query.error,
|
||||
refetch,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for file search using CodexLens
|
||||
*/
|
||||
export function useCodexLensFilesSearch(params: CodexLensSearchParams, options: UseCodexLensSearchOptions = {}): UseCodexLensSearchReturn {
|
||||
const { enabled = false } = options;
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: codexLensKeys.filesSearch(params),
|
||||
queryFn: () => searchFilesCodexLens(params),
|
||||
enabled,
|
||||
staleTime: STALE_TIME_SHORT,
|
||||
retry: 1,
|
||||
});
|
||||
|
||||
const refetch = async () => {
|
||||
await query.refetch();
|
||||
};
|
||||
|
||||
return {
|
||||
data: query.data,
|
||||
results: query.data?.results,
|
||||
isLoading: query.isLoading,
|
||||
error: query.error,
|
||||
refetch,
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseCodexLensSymbolSearchOptions {
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface UseCodexLensSymbolSearchReturn {
|
||||
data: CodexLensSymbolSearchResponse | undefined;
|
||||
symbols: CodexLensSymbolSearchResponse['symbols'] | undefined;
|
||||
isLoading: boolean;
|
||||
error: Error | null;
|
||||
refetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for symbol search using CodexLens
|
||||
*/
|
||||
export function useCodexLensSymbolSearch(
|
||||
params: Pick<CodexLensSearchParams, 'query' | 'limit'>,
|
||||
options: UseCodexLensSymbolSearchOptions = {}
|
||||
): UseCodexLensSymbolSearchReturn {
|
||||
const { enabled = false } = options;
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: codexLensKeys.symbolSearch(params),
|
||||
queryFn: () => searchSymbolCodexLens(params),
|
||||
enabled,
|
||||
staleTime: STALE_TIME_SHORT,
|
||||
retry: 1,
|
||||
});
|
||||
|
||||
const refetch = async () => {
|
||||
await query.refetch();
|
||||
};
|
||||
|
||||
return {
|
||||
data: query.data,
|
||||
symbols: query.data?.symbols,
|
||||
isLoading: query.isLoading,
|
||||
error: query.error,
|
||||
refetch,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
// ========================================
|
||||
// TanStack Query hook for project overview data
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchProjectOverview } from '../lib/api';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { fetchProjectOverview, updateProjectGuidelines, type ProjectGuidelines } from '../lib/api';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
|
||||
// Query key factory
|
||||
@@ -53,3 +53,39 @@ export function useProjectOverview(options: UseProjectOverviewOptions = {}) {
|
||||
refetch: query.refetch,
|
||||
};
|
||||
}
|
||||
|
||||
// ========== Mutations ==========
|
||||
|
||||
export interface UseUpdateGuidelinesReturn {
|
||||
updateGuidelines: (guidelines: ProjectGuidelines) => Promise<{ success: boolean; guidelines?: ProjectGuidelines; error?: string }>;
|
||||
isUpdating: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for updating project guidelines
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { updateGuidelines, isUpdating } = useUpdateGuidelines();
|
||||
* await updateGuidelines({ conventions: { ... }, constraints: { ... } });
|
||||
* ```
|
||||
*/
|
||||
export function useUpdateGuidelines(): UseUpdateGuidelinesReturn {
|
||||
const queryClient = useQueryClient();
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (guidelines: ProjectGuidelines) => updateProjectGuidelines(guidelines, projectPath),
|
||||
onSuccess: () => {
|
||||
// Invalidate project overview cache to trigger refetch
|
||||
queryClient.invalidateQueries({ queryKey: projectOverviewKeys.detail() });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
updateGuidelines: mutation.mutateAsync,
|
||||
isUpdating: mutation.isPending,
|
||||
error: mutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1249,6 +1249,11 @@ export interface ProjectGuidelines {
|
||||
constraints?: Record<string, string[]>;
|
||||
quality_rules?: GuidelineEntry[];
|
||||
learnings?: LearningEntry[];
|
||||
_metadata?: {
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
version?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ProjectOverviewMetadata {
|
||||
@@ -1285,6 +1290,23 @@ export async function fetchProjectOverview(projectPath?: string): Promise<Projec
|
||||
return data.projectOverview ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update project guidelines for a specific workspace
|
||||
*/
|
||||
export async function updateProjectGuidelines(
|
||||
guidelines: ProjectGuidelines,
|
||||
projectPath?: string
|
||||
): Promise<{ success: boolean; guidelines?: ProjectGuidelines; error?: string }> {
|
||||
const url = projectPath
|
||||
? `/api/ccw/guidelines?path=${encodeURIComponent(projectPath)}`
|
||||
: '/api/ccw/guidelines';
|
||||
return fetchApi(url, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(guidelines),
|
||||
});
|
||||
}
|
||||
|
||||
// ========== Session Detail API ==========
|
||||
|
||||
export interface SessionDetailContext {
|
||||
@@ -2939,3 +2961,727 @@ export async function updateCodexLensIgnorePatterns(request: CodexLensUpdateIgno
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
}
|
||||
|
||||
// ========== CodexLens Search API ==========
|
||||
|
||||
/**
|
||||
* CodexLens search request parameters
|
||||
*/
|
||||
export interface CodexLensSearchParams {
|
||||
query: string;
|
||||
limit?: number;
|
||||
mode?: 'dense_rerank' | 'fts' | 'fuzzy';
|
||||
max_content_length?: number;
|
||||
extra_files_count?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* CodexLens search result
|
||||
*/
|
||||
export interface CodexLensSearchResult {
|
||||
path: string;
|
||||
score: number;
|
||||
content?: string;
|
||||
line_start?: number;
|
||||
line_end?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* CodexLens search response
|
||||
*/
|
||||
export interface CodexLensSearchResponse {
|
||||
success: boolean;
|
||||
results: CodexLensSearchResult[];
|
||||
total?: number;
|
||||
query: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* CodexLens symbol search response
|
||||
*/
|
||||
export interface CodexLensSymbolSearchResponse {
|
||||
success: boolean;
|
||||
symbols: Array<{
|
||||
name: string;
|
||||
kind: string;
|
||||
path: string;
|
||||
line: number;
|
||||
[key: string]: unknown;
|
||||
}>;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform content search using CodexLens
|
||||
*/
|
||||
export async function searchCodexLens(params: CodexLensSearchParams): Promise<CodexLensSearchResponse> {
|
||||
const queryParams = new URLSearchParams();
|
||||
queryParams.append('query', params.query);
|
||||
if (params.limit) queryParams.append('limit', String(params.limit));
|
||||
if (params.mode) queryParams.append('mode', params.mode);
|
||||
if (params.max_content_length) queryParams.append('max_content_length', String(params.max_content_length));
|
||||
if (params.extra_files_count) queryParams.append('extra_files_count', String(params.extra_files_count));
|
||||
|
||||
return fetchApi<CodexLensSearchResponse>(`/api/codexlens/search?${queryParams.toString()}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform file search using CodexLens
|
||||
*/
|
||||
export async function searchFilesCodexLens(params: CodexLensSearchParams): Promise<CodexLensSearchResponse> {
|
||||
const queryParams = new URLSearchParams();
|
||||
queryParams.append('query', params.query);
|
||||
if (params.limit) queryParams.append('limit', String(params.limit));
|
||||
if (params.mode) queryParams.append('mode', params.mode);
|
||||
if (params.max_content_length) queryParams.append('max_content_length', String(params.max_content_length));
|
||||
if (params.extra_files_count) queryParams.append('extra_files_count', String(params.extra_files_count));
|
||||
|
||||
return fetchApi<CodexLensSearchResponse>(`/api/codexlens/search_files?${queryParams.toString()}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform symbol search using CodexLens
|
||||
*/
|
||||
export async function searchSymbolCodexLens(params: Pick<CodexLensSearchParams, 'query' | 'limit'>): Promise<CodexLensSymbolSearchResponse> {
|
||||
const queryParams = new URLSearchParams();
|
||||
queryParams.append('query', params.query);
|
||||
if (params.limit) queryParams.append('limit', String(params.limit));
|
||||
|
||||
return fetchApi<CodexLensSymbolSearchResponse>(`/api/codexlens/symbol?${queryParams.toString()}`);
|
||||
}
|
||||
|
||||
// ========== CodexLens Index Management API ==========
|
||||
|
||||
/**
|
||||
* Index operation type
|
||||
*/
|
||||
export type CodexLensIndexOperation = 'fts_full' | 'fts_incremental' | 'vector_full' | 'vector_incremental';
|
||||
|
||||
/**
|
||||
* CodexLens index entry
|
||||
*/
|
||||
export interface CodexLensIndex {
|
||||
id: string;
|
||||
path: string;
|
||||
indexPath: string;
|
||||
size: number;
|
||||
sizeFormatted: string;
|
||||
fileCount: number;
|
||||
dirCount: number;
|
||||
hasVectorIndex: boolean;
|
||||
hasNormalIndex: boolean;
|
||||
status: string;
|
||||
lastModified: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* CodexLens index list response
|
||||
*/
|
||||
export interface CodexLensIndexesResponse {
|
||||
success: boolean;
|
||||
indexDir: string;
|
||||
indexes: CodexLensIndex[];
|
||||
summary: {
|
||||
totalSize: number;
|
||||
totalSizeFormatted: string;
|
||||
vectorIndexCount: number;
|
||||
normalIndexCount: number;
|
||||
totalProjects?: number;
|
||||
totalFiles?: number;
|
||||
totalDirs?: number;
|
||||
indexSizeBytes?: number;
|
||||
indexSizeMb?: number;
|
||||
embeddings?: any;
|
||||
fullIndexDirSize?: number;
|
||||
fullIndexDirSizeFormatted?: string;
|
||||
};
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* CodexLens index operation request
|
||||
*/
|
||||
export interface CodexLensIndexOperationRequest {
|
||||
path: string;
|
||||
operation: CodexLensIndexOperation;
|
||||
indexType?: 'normal' | 'vector';
|
||||
embeddingModel?: string;
|
||||
embeddingBackend?: 'fastembed' | 'litellm';
|
||||
maxWorkers?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* CodexLens index operation response
|
||||
*/
|
||||
export interface CodexLensIndexOperationResponse {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
error?: string;
|
||||
result?: any;
|
||||
output?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* CodexLens indexing status response
|
||||
*/
|
||||
export interface CodexLensIndexingStatusResponse {
|
||||
success: boolean;
|
||||
inProgress: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all CodexLens indexes
|
||||
*/
|
||||
export async function fetchCodexLensIndexes(): Promise<CodexLensIndexesResponse> {
|
||||
return fetchApi<CodexLensIndexesResponse>('/api/codexlens/indexes');
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild CodexLens index (full rebuild)
|
||||
* @param projectPath - Project path to index
|
||||
* @param options - Index options
|
||||
*/
|
||||
export async function rebuildCodexLensIndex(
|
||||
projectPath: string,
|
||||
options: {
|
||||
indexType?: 'normal' | 'vector';
|
||||
embeddingModel?: string;
|
||||
embeddingBackend?: 'fastembed' | 'litellm';
|
||||
maxWorkers?: number;
|
||||
} = {}
|
||||
): Promise<CodexLensIndexOperationResponse> {
|
||||
return fetchApi<CodexLensIndexOperationResponse>('/api/codexlens/init', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
path: projectPath,
|
||||
indexType: options.indexType || 'vector',
|
||||
embeddingModel: options.embeddingModel || 'code',
|
||||
embeddingBackend: options.embeddingBackend || 'fastembed',
|
||||
maxWorkers: options.maxWorkers || 1
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Incremental update CodexLens index
|
||||
* @param projectPath - Project path to update
|
||||
* @param options - Index options
|
||||
*/
|
||||
export async function updateCodexLensIndex(
|
||||
projectPath: string,
|
||||
options: {
|
||||
indexType?: 'normal' | 'vector';
|
||||
embeddingModel?: string;
|
||||
embeddingBackend?: 'fastembed' | 'litellm';
|
||||
maxWorkers?: number;
|
||||
} = {}
|
||||
): Promise<CodexLensIndexOperationResponse> {
|
||||
return fetchApi<CodexLensIndexOperationResponse>('/api/codexlens/update', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
path: projectPath,
|
||||
indexType: options.indexType || 'vector',
|
||||
embeddingModel: options.embeddingModel || 'code',
|
||||
embeddingBackend: options.embeddingBackend || 'fastembed',
|
||||
maxWorkers: options.maxWorkers || 1
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel ongoing CodexLens indexing
|
||||
*/
|
||||
export async function cancelCodexLensIndexing(): Promise<{ success: boolean; error?: string }> {
|
||||
return fetchApi('/api/codexlens/cancel', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if CodexLens indexing is in progress
|
||||
*/
|
||||
export async function checkCodexLensIndexingStatus(): Promise<CodexLensIndexingStatusResponse> {
|
||||
return fetchApi<CodexLensIndexingStatusResponse>('/api/codexlens/indexing-status');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean CodexLens indexes
|
||||
* @param options - Clean options
|
||||
*/
|
||||
export async function cleanCodexLensIndexes(options: {
|
||||
all?: boolean;
|
||||
path?: string;
|
||||
} = {}): Promise<{ success: boolean; message?: string; error?: string }> {
|
||||
return fetchApi('/api/codexlens/clean', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(options),
|
||||
});
|
||||
}
|
||||
|
||||
// ========== LiteLLM API Settings API ==========
|
||||
|
||||
/**
|
||||
* Provider credential types
|
||||
*/
|
||||
export type ProviderType = 'openai' | 'anthropic' | 'custom';
|
||||
|
||||
/**
|
||||
* Advanced provider settings
|
||||
*/
|
||||
export interface ProviderAdvancedSettings {
|
||||
timeout?: number;
|
||||
maxRetries?: number;
|
||||
organization?: string;
|
||||
apiVersion?: string;
|
||||
customHeaders?: Record<string, string>;
|
||||
rpm?: number;
|
||||
tpm?: number;
|
||||
proxy?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Routing strategy types
|
||||
*/
|
||||
export type RoutingStrategy = 'simple-shuffle' | 'weighted' | 'latency-based' | 'cost-based' | 'least-busy';
|
||||
|
||||
/**
|
||||
* Individual API key entry
|
||||
*/
|
||||
export interface ApiKeyEntry {
|
||||
id: string;
|
||||
key: string;
|
||||
label?: string;
|
||||
weight?: number;
|
||||
enabled: boolean;
|
||||
healthStatus?: 'healthy' | 'unhealthy' | 'unknown';
|
||||
lastHealthCheck?: string;
|
||||
lastError?: string;
|
||||
lastLatencyMs?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Health check configuration
|
||||
*/
|
||||
export interface HealthCheckConfig {
|
||||
enabled: boolean;
|
||||
intervalSeconds: number;
|
||||
cooldownSeconds: number;
|
||||
failureThreshold: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Model capabilities
|
||||
*/
|
||||
export interface ModelCapabilities {
|
||||
streaming?: boolean;
|
||||
functionCalling?: boolean;
|
||||
vision?: boolean;
|
||||
contextWindow?: number;
|
||||
embeddingDimension?: number;
|
||||
maxOutputTokens?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Model endpoint settings
|
||||
*/
|
||||
export interface ModelEndpointSettings {
|
||||
baseUrl?: string;
|
||||
timeout?: number;
|
||||
maxRetries?: number;
|
||||
customHeaders?: Record<string, string>;
|
||||
cacheStrategy?: CacheStrategy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Model definition
|
||||
*/
|
||||
export interface ModelDefinition {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'llm' | 'embedding' | 'reranker';
|
||||
series: string;
|
||||
enabled: boolean;
|
||||
capabilities?: ModelCapabilities;
|
||||
endpointSettings?: ModelEndpointSettings;
|
||||
description?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider credential
|
||||
*/
|
||||
export interface ProviderCredential {
|
||||
id: string;
|
||||
name: string;
|
||||
type: ProviderType;
|
||||
apiKey: string;
|
||||
apiBase?: string;
|
||||
enabled: boolean;
|
||||
advancedSettings?: ProviderAdvancedSettings;
|
||||
apiKeys?: ApiKeyEntry[];
|
||||
routingStrategy?: RoutingStrategy;
|
||||
healthCheck?: HealthCheckConfig;
|
||||
llmModels?: ModelDefinition[];
|
||||
embeddingModels?: ModelDefinition[];
|
||||
rerankerModels?: ModelDefinition[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache strategy
|
||||
*/
|
||||
export interface CacheStrategy {
|
||||
enabled: boolean;
|
||||
ttlMinutes: number;
|
||||
maxSizeKB: number;
|
||||
filePatterns: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom endpoint
|
||||
*/
|
||||
export interface CustomEndpoint {
|
||||
id: string;
|
||||
name: string;
|
||||
providerId: string;
|
||||
model: string;
|
||||
description?: string;
|
||||
cacheStrategy: CacheStrategy;
|
||||
enabled: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Global cache settings
|
||||
*/
|
||||
export interface GlobalCacheSettings {
|
||||
enabled: boolean;
|
||||
cacheDir: string;
|
||||
maxTotalSizeMB: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache statistics
|
||||
*/
|
||||
export interface CacheStats {
|
||||
totalSize: number;
|
||||
maxSize: number;
|
||||
entries: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Model pool type
|
||||
*/
|
||||
export type ModelPoolType = 'embedding' | 'llm' | 'reranker';
|
||||
|
||||
/**
|
||||
* Model pool config
|
||||
*/
|
||||
export interface ModelPoolConfig {
|
||||
id: string;
|
||||
modelType: ModelPoolType;
|
||||
enabled: boolean;
|
||||
targetModel: string;
|
||||
strategy: 'round_robin' | 'latency_aware' | 'weighted_random';
|
||||
autoDiscover: boolean;
|
||||
excludedProviderIds?: string[];
|
||||
defaultCooldown: number;
|
||||
defaultMaxConcurrentPerKey: number;
|
||||
name?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider for model pool discovery
|
||||
*/
|
||||
export interface DiscoveredProvider {
|
||||
providerId: string;
|
||||
providerName: string;
|
||||
models: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* CLI settings mode
|
||||
*/
|
||||
export type CliSettingsMode = 'provider-based' | 'direct';
|
||||
|
||||
/**
|
||||
* CLI settings
|
||||
*/
|
||||
export interface CliSettings {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
enabled: boolean;
|
||||
mode: CliSettingsMode;
|
||||
providerId?: string;
|
||||
settings?: Record<string, unknown>;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
// ========== Provider Management ==========
|
||||
|
||||
/**
|
||||
* Fetch all providers
|
||||
*/
|
||||
export async function fetchProviders(): Promise<{ providers: ProviderCredential[]; count: number }> {
|
||||
return fetchApi('/api/litellm-api/providers');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create provider
|
||||
*/
|
||||
export async function createProvider(provider: Omit<ProviderCredential, 'id' | 'createdAt' | 'updatedAt'>): Promise<{ success: boolean; provider: ProviderCredential }> {
|
||||
return fetchApi('/api/litellm-api/providers', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(provider),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update provider
|
||||
*/
|
||||
export async function updateProvider(providerId: string, updates: Partial<Omit<ProviderCredential, 'id' | 'createdAt' | 'updatedAt'>>): Promise<{ success: boolean; provider: ProviderCredential }> {
|
||||
return fetchApi(`/api/litellm-api/providers/${encodeURIComponent(providerId)}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(updates),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete provider
|
||||
*/
|
||||
export async function deleteProvider(providerId: string): Promise<{ success: boolean; message: string }> {
|
||||
return fetchApi(`/api/litellm-api/providers/${encodeURIComponent(providerId)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Test provider connection
|
||||
*/
|
||||
export async function testProvider(providerId: string): Promise<{ success: boolean; provider: string; latencyMs?: number; error?: string }> {
|
||||
return fetchApi(`/api/litellm-api/providers/${encodeURIComponent(providerId)}/test`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Test specific API key
|
||||
*/
|
||||
export async function testProviderKey(providerId: string, keyId: string): Promise<{ valid: boolean; error?: string; latencyMs?: number; keyLabel?: string }> {
|
||||
return fetchApi(`/api/litellm-api/providers/${encodeURIComponent(providerId)}/test-key`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ keyId }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get provider health status
|
||||
*/
|
||||
export async function getProviderHealthStatus(providerId: string): Promise<{ providerId: string; providerName: string; keys: Array<{ keyId: string; label: string; status: string; lastCheck?: string; lastLatencyMs?: number; consecutiveFailures?: number; inCooldown?: boolean; lastError?: string }> }> {
|
||||
return fetchApi(`/api/litellm-api/providers/${encodeURIComponent(providerId)}/health-status`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger health check now
|
||||
*/
|
||||
export async function triggerProviderHealthCheck(providerId: string): Promise<{ success: boolean; providerId: string; providerName?: string; keys: Array<any>; checkedAt: string }> {
|
||||
return fetchApi(`/api/litellm-api/providers/${encodeURIComponent(providerId)}/health-check-now`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
// ========== Endpoint Management ==========
|
||||
|
||||
/**
|
||||
* Fetch all endpoints
|
||||
*/
|
||||
export async function fetchEndpoints(): Promise<{ endpoints: CustomEndpoint[]; count: number }> {
|
||||
return fetchApi('/api/litellm-api/endpoints');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create endpoint
|
||||
*/
|
||||
export async function createEndpoint(endpoint: Omit<CustomEndpoint, 'createdAt' | 'updatedAt'>): Promise<{ success: boolean; endpoint: CustomEndpoint }> {
|
||||
return fetchApi('/api/litellm-api/endpoints', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(endpoint),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update endpoint
|
||||
*/
|
||||
export async function updateEndpoint(endpointId: string, updates: Partial<Omit<CustomEndpoint, 'id' | 'createdAt' | 'updatedAt'>>): Promise<{ success: boolean; endpoint: CustomEndpoint }> {
|
||||
return fetchApi(`/api/litellm-api/endpoints/${encodeURIComponent(endpointId)}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(updates),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete endpoint
|
||||
*/
|
||||
export async function deleteEndpoint(endpointId: string): Promise<{ success: boolean; message: string }> {
|
||||
return fetchApi(`/api/litellm-api/endpoints/${encodeURIComponent(endpointId)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
// ========== Model Discovery ==========
|
||||
|
||||
/**
|
||||
* Get available models for provider type
|
||||
*/
|
||||
export async function getProviderModels(providerType: string): Promise<{ providerType: string; models: Array<{ id: string; name: string; provider: string; description?: string }>; count: number }> {
|
||||
return fetchApi(`/api/litellm-api/models/${encodeURIComponent(providerType)}`);
|
||||
}
|
||||
|
||||
// ========== Cache Management ==========
|
||||
|
||||
/**
|
||||
* Fetch cache statistics
|
||||
*/
|
||||
export async function fetchCacheStats(): Promise<CacheStats> {
|
||||
return fetchApi('/api/litellm-api/cache/stats');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cache
|
||||
*/
|
||||
export async function clearCache(): Promise<{ success: boolean; removed: number }> {
|
||||
return fetchApi('/api/litellm-api/cache/clear', {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update cache settings
|
||||
*/
|
||||
export async function updateCacheSettings(settings: Partial<{ enabled: boolean; cacheDir: string; maxTotalSizeMB: number }>): Promise<{ success: boolean; settings: GlobalCacheSettings }> {
|
||||
return fetchApi('/api/litellm-api/config/cache', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(settings),
|
||||
});
|
||||
}
|
||||
|
||||
// ========== Model Pool Management ==========
|
||||
|
||||
/**
|
||||
* Fetch all model pools
|
||||
*/
|
||||
export async function fetchModelPools(): Promise<{ pools: ModelPoolConfig[] }> {
|
||||
return fetchApi('/api/litellm-api/model-pools');
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch single model pool
|
||||
*/
|
||||
export async function fetchModelPool(poolId: string): Promise<{ pool: ModelPoolConfig }> {
|
||||
return fetchApi(`/api/litellm-api/model-pools/${encodeURIComponent(poolId)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create model pool
|
||||
*/
|
||||
export async function createModelPool(pool: Omit<ModelPoolConfig, 'id'>): Promise<{ success: boolean; poolId: string; syncResult?: any }> {
|
||||
return fetchApi('/api/litellm-api/model-pools', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(pool),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update model pool
|
||||
*/
|
||||
export async function updateModelPool(poolId: string, updates: Partial<ModelPoolConfig>): Promise<{ success: boolean; poolId?: string; syncResult?: any }> {
|
||||
return fetchApi(`/api/litellm-api/model-pools/${encodeURIComponent(poolId)}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(updates),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete model pool
|
||||
*/
|
||||
export async function deleteModelPool(poolId: string): Promise<{ success: boolean; syncResult?: any }> {
|
||||
return fetchApi(`/api/litellm-api/model-pools/${encodeURIComponent(poolId)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available models for pool type
|
||||
*/
|
||||
export async function getAvailableModelsForPool(modelType: ModelPoolType): Promise<{ availableModels: Array<{ modelId: string; modelName: string; providers: string[] }> }> {
|
||||
return fetchApi(`/api/litellm-api/model-pools/available-models/${encodeURIComponent(modelType)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover providers for model
|
||||
*/
|
||||
export async function discoverModelsForPool(modelType: ModelPoolType, targetModel: string): Promise<{ modelType: string; targetModel: string; discovered: DiscoveredProvider[]; count: number }> {
|
||||
return fetchApi(`/api/litellm-api/model-pools/discover/${encodeURIComponent(modelType)}/${encodeURIComponent(targetModel)}`);
|
||||
}
|
||||
|
||||
// ========== Config Management ==========
|
||||
|
||||
/**
|
||||
* Get full config
|
||||
*/
|
||||
export async function fetchApiConfig(): Promise<any> {
|
||||
return fetchApi('/api/litellm-api/config');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync config to YAML
|
||||
*/
|
||||
export async function syncApiConfig(): Promise<{ success: boolean; message: string; yamlPath?: string }> {
|
||||
return fetchApi('/api/litellm-api/config/sync', {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview YAML config
|
||||
*/
|
||||
export async function previewYamlConfig(): Promise<{ success: boolean; config: string }> {
|
||||
return fetchApi('/api/litellm-api/config/yaml-preview');
|
||||
}
|
||||
|
||||
// ========== CCW-LiteLLM Package Management ==========
|
||||
|
||||
/**
|
||||
* Check ccw-litellm status
|
||||
*/
|
||||
export async function checkCcwLitellmStatus(refresh = false): Promise<{ installed: boolean; version?: string; error?: string }> {
|
||||
return fetchApi(`/api/litellm-api/ccw-litellm/status${refresh ? '?refresh=true' : ''}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Install ccw-litellm
|
||||
*/
|
||||
export async function installCcwLitellm(): Promise<{ success: boolean; message?: string; error?: string; path?: string }> {
|
||||
return fetchApi('/api/litellm-api/ccw-litellm/install', {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall ccw-litellm
|
||||
*/
|
||||
export async function uninstallCcwLitellm(): Promise<{ success: boolean; message?: string; error?: string }> {
|
||||
return fetchApi('/api/litellm-api/ccw-litellm/uninstall', {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -112,3 +112,19 @@ export const workspaceQueryKeys = {
|
||||
cliExecutionDetail: (projectPath: string, executionId: string) =>
|
||||
[...workspaceQueryKeys.cliHistory(projectPath), 'detail', executionId] as const,
|
||||
};
|
||||
|
||||
// ========== API Settings Keys ==========
|
||||
/**
|
||||
* API Settings query keys (global, not workspace-specific)
|
||||
*/
|
||||
export const apiSettingsKeys = {
|
||||
all: ['apiSettings'] as const,
|
||||
providers: () => [...apiSettingsKeys.all, 'providers'] as const,
|
||||
provider: (id: string) => [...apiSettingsKeys.providers(), id] as const,
|
||||
endpoints: () => [...apiSettingsKeys.all, 'endpoints'] as const,
|
||||
endpoint: (id: string) => [...apiSettingsKeys.endpoints(), id] as const,
|
||||
cache: () => [...apiSettingsKeys.all, 'cache'] as const,
|
||||
modelPools: () => [...apiSettingsKeys.all, 'modelPools'] as const,
|
||||
modelPool: (id: string) => [...apiSettingsKeys.modelPools(), id] as const,
|
||||
ccwLitellm: () => [...apiSettingsKeys.all, 'ccwLitellm'] as const,
|
||||
};
|
||||
|
||||
335
ccw/frontend/src/locales/en/api-settings.json
Normal file
335
ccw/frontend/src/locales/en/api-settings.json
Normal file
@@ -0,0 +1,335 @@
|
||||
{
|
||||
"title": "API Settings",
|
||||
"description": "Manage LiteLLM providers, endpoints, cache settings, and model pools",
|
||||
"tabs": {
|
||||
"providers": "Providers",
|
||||
"endpoints": "Endpoints",
|
||||
"cache": "Cache",
|
||||
"modelPools": "Model Pools",
|
||||
"cliSettings": "CLI Settings"
|
||||
},
|
||||
"providers": {
|
||||
"title": "Providers",
|
||||
"description": "Configure API provider credentials and multi-key settings",
|
||||
"stats": {
|
||||
"total": "Total Providers",
|
||||
"enabled": "Enabled"
|
||||
},
|
||||
"actions": {
|
||||
"add": "Add Provider",
|
||||
"edit": "Edit Provider",
|
||||
"delete": "Delete Provider",
|
||||
"test": "Test Connection",
|
||||
"multiKeySettings": "Multi-Key Settings",
|
||||
"syncToCodexLens": "Sync to CodexLens",
|
||||
"manageModels": "Manage Models",
|
||||
"addModel": "Add Model"
|
||||
},
|
||||
"deleteConfirm": "Are you sure you want to delete the provider \"{name}\"?",
|
||||
"emptyState": {
|
||||
"title": "No Providers Found",
|
||||
"message": "Add a provider to configure API credentials and models."
|
||||
},
|
||||
"apiFormat": "API Format",
|
||||
"openaiCompatible": "OpenAI Compatible",
|
||||
"customFormat": "Custom Format",
|
||||
"apiFormatHint": "Most providers use OpenAI-compatible format",
|
||||
"displayName": "Display Name",
|
||||
"apiKey": "API Key",
|
||||
"useEnvVar": "Use environment variable (e.g., ${OPENAI_API_KEY})",
|
||||
"apiBaseUrl": "API Base URL",
|
||||
"preview": "Preview",
|
||||
"enableProvider": "Enable this provider",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"timeout": "Timeout",
|
||||
"timeoutHint": "Request timeout in seconds (default: 300)",
|
||||
"maxRetries": "Max Retries",
|
||||
"organization": "Organization",
|
||||
"organizationHint": "OpenAI-specific organization ID",
|
||||
"apiVersion": "API Version",
|
||||
"apiVersionHint": "Azure-specific API version (e.g., 2024-02-01)",
|
||||
"rpm": "RPM",
|
||||
"tpm": "TPM",
|
||||
"unlimited": "Unlimited",
|
||||
"proxy": "Proxy URL",
|
||||
"customHeaders": "Custom Headers",
|
||||
"customHeadersHint": "JSON format, e.g., {\"X-Custom\": \"value\"}",
|
||||
"testConnection": "Test Connection",
|
||||
"connectionSuccess": "Connection successful",
|
||||
"connectionFailed": "Connection failed",
|
||||
"addProviderFirst": "Please add a provider first",
|
||||
"saveProviderFirst": "Please save the provider first",
|
||||
"llmModels": "LLM Models",
|
||||
"embeddingModels": "Embedding Models",
|
||||
"rerankerModels": "Reranker Models",
|
||||
"noModels": "No models configured for this provider",
|
||||
"selectProvider": "Select Provider",
|
||||
"selectProviderHint": "Choose a provider to view and manage their models",
|
||||
"modelSettings": "Model Settings",
|
||||
"deleteModel": "Delete Model",
|
||||
"multiKeySettings": "Multi-Key Settings",
|
||||
"keyLabel": "Key Label",
|
||||
"keyValue": "Key Value",
|
||||
"keyWeight": "Weight",
|
||||
"routingStrategy": "Routing Strategy",
|
||||
"healthCheck": "Health Check",
|
||||
"healthStatus": "Health Status",
|
||||
"lastCheck": "Last check",
|
||||
"lastError": "Last error",
|
||||
"lastLatency": "Latency",
|
||||
"cooldown": "Cooldown",
|
||||
"consecutiveFailures": "Failures",
|
||||
"inCooldown": "In cooldown",
|
||||
"healthy": "Healthy",
|
||||
"unhealthy": "Unhealthy",
|
||||
"unknown": "Unknown",
|
||||
"justNow": "just now",
|
||||
"minutesAgo": "m ago",
|
||||
"hoursAgo": "h ago",
|
||||
"daysAgo": "d ago",
|
||||
"seconds": "seconds",
|
||||
"testKey": "Test Key",
|
||||
"addKey": "Add Key",
|
||||
"enableKey": "Enable",
|
||||
"disableKey": "Disable",
|
||||
"deleteKey": "Delete Key",
|
||||
"simpleShuffle": "Simple Shuffle",
|
||||
"weighted": "Weighted",
|
||||
"latencyBased": "Latency Based",
|
||||
"costBased": "Cost Based",
|
||||
"leastBusy": "Least Busy",
|
||||
"enableHealthCheck": "Enable health checks",
|
||||
"checkInterval": "Check interval (seconds)",
|
||||
"cooldownPeriod": "Cooldown period (seconds)",
|
||||
"failureThreshold": "Failure threshold",
|
||||
"addLlmModel": "Add LLM Model",
|
||||
"addEmbeddingModel": "Add Embedding Model",
|
||||
"addRerankerModel": "Add Reranker Model",
|
||||
"selectFromPresets": "Select from presets",
|
||||
"customModel": "Custom model...",
|
||||
"modelId": "Model ID",
|
||||
"modelName": "Model Name",
|
||||
"modelSeries": "Model Series",
|
||||
"contextWindow": "Context Window",
|
||||
"capabilities": "Capabilities",
|
||||
"streaming": "Streaming",
|
||||
"functionCalling": "Function Calling",
|
||||
"vision": "Vision",
|
||||
"embeddingMaxTokens": "Max Tokens",
|
||||
"rerankerTopK": "Top K",
|
||||
"rerankerTopKHint": "Number of results to return",
|
||||
"embeddingDimensions": "Dimensions",
|
||||
"description": "Description",
|
||||
"optional": "Optional",
|
||||
"modelIdExists": "Model ID already exists",
|
||||
"useModelTreeToManage": "Use provider card to manage models",
|
||||
"endpointPreview": "Endpoint Preview",
|
||||
"modelBaseUrlOverride": "Base URL Override",
|
||||
"modelBaseUrlHint": "Override base URL for this model",
|
||||
"basicInfo": "Basic Information",
|
||||
"endpointSettings": "Endpoint Settings",
|
||||
"apiBaseUpdated": "Base URL updated"
|
||||
},
|
||||
"endpoints": {
|
||||
"title": "Endpoints",
|
||||
"description": "Configure custom API endpoints with caching strategies",
|
||||
"stats": {
|
||||
"totalEndpoints": "Total Endpoints",
|
||||
"cachedEndpoints": "Cached Endpoints"
|
||||
},
|
||||
"actions": {
|
||||
"add": "Add Endpoint",
|
||||
"edit": "Edit Endpoint",
|
||||
"delete": "Delete Endpoint",
|
||||
"enable": "Enable",
|
||||
"disable": "Disable"
|
||||
},
|
||||
"deleteConfirm": "Are you sure you want to delete the endpoint \"{id}\"?",
|
||||
"emptyState": {
|
||||
"title": "No Endpoints Found",
|
||||
"message": "Add an endpoint to configure custom API mappings."
|
||||
},
|
||||
"endpointId": "Endpoint ID",
|
||||
"endpointIdHint": "Unique CLI identifier (e.g., my-gpt4o)",
|
||||
"name": "Name",
|
||||
"provider": "Provider",
|
||||
"model": "Model",
|
||||
"noModelsConfigured": "No models configured for this provider",
|
||||
"selectModel": "Select a model...",
|
||||
"cacheStrategy": "Cache Strategy",
|
||||
"enableContextCaching": "Enable context caching",
|
||||
"cacheTTL": "Cache TTL (minutes)",
|
||||
"cacheMaxSize": "Max Size (KB)",
|
||||
"autoCachePatterns": "Auto-cache patterns",
|
||||
"filePatternsHint": "Comma-separated glob patterns (e.g., *.md,*.ts)",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"noEndpoints": "No Endpoints Configured",
|
||||
"noEndpointsHint": "Add an endpoint to create custom API mappings with caching.",
|
||||
"providerBased": "Provider-based",
|
||||
"direct": "Direct",
|
||||
"off": "Off"
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache",
|
||||
"description": "Manage global cache settings and view cache statistics",
|
||||
"settings": {
|
||||
"title": "Cache Settings",
|
||||
"globalCache": "Global Cache",
|
||||
"enableGlobalCaching": "Enable global caching",
|
||||
"cacheDirectory": "Cache directory",
|
||||
"maxSize": "Max total size (MB)",
|
||||
"cacheUsage": "Cache usage",
|
||||
"cacheEntries": "Cache entries",
|
||||
"cacheSize": "Cache size",
|
||||
"used": "used",
|
||||
"total": "total",
|
||||
"actions": "Cache Actions",
|
||||
"clearCache": "Clear Cache",
|
||||
"confirmClearCache": "Are you sure you want to clear the cache? This will remove all cached entries."
|
||||
},
|
||||
"statistics": {
|
||||
"title": "Cache Statistics",
|
||||
"cachedEntries": "Cached Entries",
|
||||
"storageUsed": "Storage Used",
|
||||
"totalSize": "Total Size"
|
||||
},
|
||||
"actions": {
|
||||
"clear": "Clear Cache",
|
||||
"confirmClear": "Are you sure you want to clear the cache?"
|
||||
},
|
||||
"messages": {
|
||||
"cacheCleared": "Cache cleared",
|
||||
"cacheSettingsUpdated": "Cache settings updated"
|
||||
}
|
||||
},
|
||||
"modelPools": {
|
||||
"title": "Model Pools",
|
||||
"description": "Configure model pools for high availability and load balancing",
|
||||
"stats": {
|
||||
"total": "Total Pools",
|
||||
"enabled": "Enabled"
|
||||
},
|
||||
"actions": {
|
||||
"add": "Add Model Pool",
|
||||
"edit": "Edit Model Pool",
|
||||
"delete": "Delete Model Pool",
|
||||
"autoDiscover": "Auto-Discover"
|
||||
},
|
||||
"deleteConfirm": "Are you sure you want to delete the model pool \"{name}\"?",
|
||||
"emptyState": {
|
||||
"title": "No Model Pools Found",
|
||||
"message": "Add a model pool to enable high availability and load balancing."
|
||||
},
|
||||
"modelType": "Model Type",
|
||||
"embedding": "Embedding",
|
||||
"llm": "LLM",
|
||||
"reranker": "Reranker",
|
||||
"targetModel": "Target Model",
|
||||
"selectTargetModel": "Select target model...",
|
||||
"strategy": "Strategy",
|
||||
"roundRobin": "Round Robin",
|
||||
"latencyAware": "Latency Aware",
|
||||
"weightedRandom": "Weighted Random",
|
||||
"poolEnabled": "Pool Enabled",
|
||||
"autoDiscover": "Auto-discover providers",
|
||||
"excludedProviders": "Excluded Providers",
|
||||
"defaultCooldown": "Default Cooldown (s)",
|
||||
"defaultConcurrent": "Default Concurrent",
|
||||
"discoveredProviders": "Discovered Providers",
|
||||
"excludeProvider": "Exclude",
|
||||
"includeProvider": "Include",
|
||||
"noProvidersFound": "No providers found offering this model",
|
||||
"poolSaved": "Model pool saved",
|
||||
"embeddingPoolDesc": "Configure model pools for high availability and load balancing across multiple providers",
|
||||
"embeddingPool": "Embedding Pool",
|
||||
"discovered": "Discovered",
|
||||
"providers": "providers"
|
||||
},
|
||||
"cliSettings": {
|
||||
"title": "CLI Settings",
|
||||
"description": "Configure CLI tool settings and modes",
|
||||
"stats": {
|
||||
"total": "Total Settings",
|
||||
"enabled": "Enabled"
|
||||
},
|
||||
"actions": {
|
||||
"add": "Add CLI Settings",
|
||||
"edit": "Edit CLI Settings",
|
||||
"delete": "Delete CLI Settings"
|
||||
},
|
||||
"deleteConfirm": "Are you sure you want to delete the CLI settings \"{name}\"?",
|
||||
"emptyState": {
|
||||
"title": "No CLI Settings Found",
|
||||
"message": "Add CLI settings to configure tool-specific options."
|
||||
},
|
||||
"mode": "Mode",
|
||||
"providerBased": "Provider-based",
|
||||
"direct": "Direct",
|
||||
"authToken": "Auth Token",
|
||||
"baseUrl": "Base URL",
|
||||
"model": "Model"
|
||||
},
|
||||
"ccwLitellm": {
|
||||
"title": "CCW-LiteLLM Package",
|
||||
"description": "Manage ccw-litellm Python package installation",
|
||||
"status": {
|
||||
"installed": "Installed",
|
||||
"notInstalled": "Not Installed",
|
||||
"version": "Version"
|
||||
},
|
||||
"actions": {
|
||||
"install": "Install",
|
||||
"uninstall": "Uninstall",
|
||||
"refreshStatus": "Refresh Status"
|
||||
},
|
||||
"messages": {
|
||||
"installSuccess": "ccw-litellm installed successfully",
|
||||
"installFailed": "Failed to install ccw-litellm",
|
||||
"uninstallSuccess": "ccw-litellm uninstalled successfully",
|
||||
"uninstallFailed": "Failed to uninstall ccw-litellm"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"add": "Add",
|
||||
"close": "Close",
|
||||
"loading": "Loading...",
|
||||
"error": "Error",
|
||||
"success": "Success",
|
||||
"warning": "Warning",
|
||||
"optional": "Optional",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"searchPlaceholder": "Search...",
|
||||
"noResults": "No results found",
|
||||
"actions": "Actions",
|
||||
"name": "Name",
|
||||
"description": "Description",
|
||||
"type": "Type",
|
||||
"status": "Status"
|
||||
},
|
||||
"messages": {
|
||||
"settingsSaved": "Settings saved successfully",
|
||||
"settingsDeleted": "Settings deleted successfully",
|
||||
"providerSaved": "Provider saved successfully",
|
||||
"providerDeleted": "Provider deleted successfully",
|
||||
"providerUpdated": "Provider updated successfully",
|
||||
"endpointSaved": "Endpoint saved successfully",
|
||||
"endpointDeleted": "Endpoint deleted successfully",
|
||||
"confirmDeleteProvider": "Are you sure you want to delete this provider?",
|
||||
"confirmDeleteEndpoint": "Are you sure you want to delete this endpoint?",
|
||||
"confirmClearCache": "Are you sure you want to clear the cache?",
|
||||
"invalidJsonHeaders": "Invalid JSON format for custom headers",
|
||||
"failedToLoad": "Failed to load data",
|
||||
"noProviders": "No Providers",
|
||||
"noProvidersHint": "Add a provider to get started with API configuration.",
|
||||
"noEndpoints": "No Endpoints",
|
||||
"noEndpointsHint": "Add an endpoint to create custom API mappings.",
|
||||
"configSynced": "Configuration synced to YAML file"
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@
|
||||
"overview": "Overview",
|
||||
"settings": "Settings",
|
||||
"models": "Models",
|
||||
"search": "Search",
|
||||
"advanced": "Advanced"
|
||||
},
|
||||
"overview": {
|
||||
@@ -46,6 +47,19 @@
|
||||
"lastCheck": "Last Check Time"
|
||||
}
|
||||
},
|
||||
"index": {
|
||||
"operationComplete": "Index Operation Complete",
|
||||
"operationFailed": "Index Operation Failed",
|
||||
"noProject": "No Project Selected",
|
||||
"noProjectDesc": "Please open a project to perform index operations.",
|
||||
"starting": "Starting index operation...",
|
||||
"cancelFailed": "Failed to cancel operation",
|
||||
"unknownError": "An unknown error occurred",
|
||||
"complete": "Complete",
|
||||
"failed": "Failed",
|
||||
"cancelled": "Cancelled",
|
||||
"inProgress": "In Progress"
|
||||
},
|
||||
"settings": {
|
||||
"currentCount": "Current Index Count",
|
||||
"currentWorkers": "Current Workers",
|
||||
@@ -114,6 +128,8 @@
|
||||
"advanced": {
|
||||
"warningTitle": "Sensitive Operations Warning",
|
||||
"warningMessage": "Modifying environment variables may affect CodexLens operation. Ensure you understand each variable's purpose.",
|
||||
"loadError": "Failed to load environment variables",
|
||||
"loadErrorDesc": "Unable to fetch environment configuration. Please check if CodexLens is properly installed.",
|
||||
"currentVars": "Current Environment Variables",
|
||||
"settingsVars": "Settings Variables",
|
||||
"customVars": "Custom Variables",
|
||||
@@ -170,9 +186,35 @@
|
||||
"title": "CodexLens Not Installed",
|
||||
"description": "Please install CodexLens to use model management features."
|
||||
},
|
||||
"error": {
|
||||
"title": "Failed to load models",
|
||||
"description": "Unable to fetch model list. Please check if CodexLens is properly installed."
|
||||
},
|
||||
"empty": {
|
||||
"title": "No models found",
|
||||
"description": "Try adjusting your search or filter criteria"
|
||||
"description": "No models are available. Try downloading models from the list.",
|
||||
"filtered": "No models match your filter",
|
||||
"filteredDesc": "Try adjusting your search or filter criteria"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"type": "Search Type",
|
||||
"content": "Content Search",
|
||||
"files": "File Search",
|
||||
"symbol": "Symbol Search",
|
||||
"mode": "Mode",
|
||||
"mode.semantic": "Semantic (default)",
|
||||
"mode.exact": "Exact (FTS)",
|
||||
"mode.fuzzy": "Fuzzy",
|
||||
"query": "Query",
|
||||
"queryPlaceholder": "Enter search query...",
|
||||
"button": "Search",
|
||||
"searching": "Searching...",
|
||||
"results": "Results",
|
||||
"resultsCount": "results",
|
||||
"notInstalled": {
|
||||
"title": "CodexLens Not Installed",
|
||||
"description": "Please install CodexLens to use semantic code search features."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
335
ccw/frontend/src/locales/zh/api-settings.json
Normal file
335
ccw/frontend/src/locales/zh/api-settings.json
Normal file
@@ -0,0 +1,335 @@
|
||||
{
|
||||
"title": "API 设置",
|
||||
"description": "管理 LiteLLM 提供商、端点、缓存设置和模型池",
|
||||
"tabs": {
|
||||
"providers": "提供商",
|
||||
"endpoints": "端点",
|
||||
"cache": "缓存",
|
||||
"modelPools": "模型池",
|
||||
"cliSettings": "CLI 设置"
|
||||
},
|
||||
"providers": {
|
||||
"title": "提供商",
|
||||
"description": "配置 API 提供商凭据和多密钥设置",
|
||||
"stats": {
|
||||
"total": "总提供商数",
|
||||
"enabled": "已启用"
|
||||
},
|
||||
"actions": {
|
||||
"add": "添加提供商",
|
||||
"edit": "编辑提供商",
|
||||
"delete": "删除提供商",
|
||||
"test": "测试连接",
|
||||
"multiKeySettings": "多密钥设置",
|
||||
"syncToCodexLens": "同步到 CodexLens",
|
||||
"manageModels": "管理模型",
|
||||
"addModel": "添加模型"
|
||||
},
|
||||
"deleteConfirm": "确定要删除提供商 \"{name}\" 吗?",
|
||||
"emptyState": {
|
||||
"title": "未找到提供商",
|
||||
"message": "添加提供商以配置 API 凭据和模型。"
|
||||
},
|
||||
"apiFormat": "API 格式",
|
||||
"openaiCompatible": "OpenAI 兼容",
|
||||
"customFormat": "自定义格式",
|
||||
"apiFormatHint": "大多数提供商使用 OpenAI 兼容格式",
|
||||
"displayName": "显示名称",
|
||||
"apiKey": "API 密钥",
|
||||
"useEnvVar": "使用环境变量(例如:${OPENAI_API_KEY})",
|
||||
"apiBaseUrl": "API 基础 URL",
|
||||
"preview": "预览",
|
||||
"enableProvider": "启用此提供商",
|
||||
"advancedSettings": "高级设置",
|
||||
"timeout": "超时",
|
||||
"timeoutHint": "请求超时时间(秒)(默认:300)",
|
||||
"maxRetries": "最大重试次数",
|
||||
"organization": "组织",
|
||||
"organizationHint": "OpenAI 特定的组织 ID",
|
||||
"apiVersion": "API 版本",
|
||||
"apiVersionHint": "Azure 特定的 API 版本(例如:2024-02-01)",
|
||||
"rpm": "RPM",
|
||||
"tpm": "TPM",
|
||||
"unlimited": "无限制",
|
||||
"proxy": "代理 URL",
|
||||
"customHeaders": "自定义请求头",
|
||||
"customHeadersHint": "JSON 格式,例如:{\"X-Custom\": \"value\"}",
|
||||
"testConnection": "测试连接",
|
||||
"connectionSuccess": "连接成功",
|
||||
"connectionFailed": "连接失败",
|
||||
"addProviderFirst": "请先添加提供商",
|
||||
"saveProviderFirst": "请先保存提供商",
|
||||
"llmModels": "LLM 模型",
|
||||
"embeddingModels": "嵌入模型",
|
||||
"rerankerModels": "重排序模型",
|
||||
"noModels": "此提供商未配置模型",
|
||||
"selectProvider": "选择提供商",
|
||||
"selectProviderHint": "选择提供商以查看和管理其模型",
|
||||
"modelSettings": "模型设置",
|
||||
"deleteModel": "删除模型",
|
||||
"multiKeySettings": "多密钥设置",
|
||||
"keyLabel": "密钥标签",
|
||||
"keyValue": "密钥值",
|
||||
"keyWeight": "权重",
|
||||
"routingStrategy": "路由策略",
|
||||
"healthCheck": "健康检查",
|
||||
"healthStatus": "健康状态",
|
||||
"lastCheck": "最后检查",
|
||||
"lastError": "最后错误",
|
||||
"lastLatency": "延迟",
|
||||
"cooldown": "冷却",
|
||||
"consecutiveFailures": "失败次数",
|
||||
"inCooldown": "冷却中",
|
||||
"healthy": "健康",
|
||||
"unhealthy": "不健康",
|
||||
"unknown": "未知",
|
||||
"justNow": "刚刚",
|
||||
"minutesAgo": "分钟前",
|
||||
"hoursAgo": "小时前",
|
||||
"daysAgo": "天前",
|
||||
"seconds": "秒",
|
||||
"testKey": "测试密钥",
|
||||
"addKey": "添加密钥",
|
||||
"enableKey": "启用",
|
||||
"disableKey": "禁用",
|
||||
"deleteKey": "删除密钥",
|
||||
"simpleShuffle": "简单随机",
|
||||
"weighted": "加权",
|
||||
"latencyBased": "基于延迟",
|
||||
"costBased": "基于成本",
|
||||
"leastBusy": "最少繁忙",
|
||||
"enableHealthCheck": "启用健康检查",
|
||||
"checkInterval": "检查间隔(秒)",
|
||||
"cooldownPeriod": "冷却周期(秒)",
|
||||
"failureThreshold": "失败阈值",
|
||||
"addLlmModel": "添加 LLM 模型",
|
||||
"addEmbeddingModel": "添加嵌入模型",
|
||||
"addRerankerModel": "添加重排序模型",
|
||||
"selectFromPresets": "从预设选择",
|
||||
"customModel": "自定义模型...",
|
||||
"modelId": "模型 ID",
|
||||
"modelName": "模型名称",
|
||||
"modelSeries": "模型系列",
|
||||
"contextWindow": "上下文窗口",
|
||||
"capabilities": "功能",
|
||||
"streaming": "流式传输",
|
||||
"functionCalling": "函数调用",
|
||||
"vision": "视觉",
|
||||
"embeddingMaxTokens": "最大令牌数",
|
||||
"rerankerTopK": "Top K",
|
||||
"rerankerTopKHint": "返回的结果数量",
|
||||
"embeddingDimensions": "维度",
|
||||
"description": "描述",
|
||||
"optional": "可选",
|
||||
"modelIdExists": "模型 ID 已存在",
|
||||
"useModelTreeToManage": "使用提供商卡片管理模型",
|
||||
"endpointPreview": "端点预览",
|
||||
"modelBaseUrlOverride": "基础 URL 覆盖",
|
||||
"modelBaseUrlHint": "为此模型覆盖基础 URL",
|
||||
"basicInfo": "基本信息",
|
||||
"endpointSettings": "端点设置",
|
||||
"apiBaseUpdated": "基础 URL 已更新"
|
||||
},
|
||||
"endpoints": {
|
||||
"title": "端点",
|
||||
"description": "配置带有缓存策略的自定义 API 端点",
|
||||
"stats": {
|
||||
"totalEndpoints": "总端点数",
|
||||
"cachedEndpoints": "已缓存端点"
|
||||
},
|
||||
"actions": {
|
||||
"add": "添加端点",
|
||||
"edit": "编辑端点",
|
||||
"delete": "删除端点",
|
||||
"enable": "启用",
|
||||
"disable": "禁用"
|
||||
},
|
||||
"deleteConfirm": "确定要删除端点 \"{id}\" 吗?",
|
||||
"emptyState": {
|
||||
"title": "未找到端点",
|
||||
"message": "添加端点以配置自定义 API 映射。"
|
||||
},
|
||||
"endpointId": "端点 ID",
|
||||
"endpointIdHint": "唯一的 CLI 标识符(例如:my-gpt4o)",
|
||||
"name": "名称",
|
||||
"provider": "提供商",
|
||||
"model": "模型",
|
||||
"noModelsConfigured": "此提供商未配置模型",
|
||||
"selectModel": "选择模型...",
|
||||
"cacheStrategy": "缓存策略",
|
||||
"enableContextCaching": "启用上下文缓存",
|
||||
"cacheTTL": "缓存 TTL(分钟)",
|
||||
"cacheMaxSize": "最大大小(KB)",
|
||||
"autoCachePatterns": "自动缓存模式",
|
||||
"filePatternsHint": "逗号分隔的 glob 模式(例如:*.md,*.ts)",
|
||||
"enabled": "已启用",
|
||||
"disabled": "已禁用",
|
||||
"noEndpoints": "未配置端点",
|
||||
"noEndpointsHint": "添加端点以创建带有缓存的自定义 API 映射。",
|
||||
"providerBased": "基于提供商",
|
||||
"direct": "直接",
|
||||
"off": "关闭"
|
||||
},
|
||||
"cache": {
|
||||
"title": "缓存",
|
||||
"description": "管理全局缓存设置和查看缓存统计信息",
|
||||
"settings": {
|
||||
"title": "缓存设置",
|
||||
"globalCache": "全局缓存",
|
||||
"enableGlobalCaching": "启用全局缓存",
|
||||
"cacheDirectory": "缓存目录",
|
||||
"maxSize": "最大总大小(MB)",
|
||||
"cacheUsage": "缓存使用",
|
||||
"cacheEntries": "缓存条目",
|
||||
"cacheSize": "缓存大小",
|
||||
"used": "已使用",
|
||||
"total": "总计",
|
||||
"actions": "缓存操作",
|
||||
"clearCache": "清除缓存",
|
||||
"confirmClearCache": "确定要清除缓存吗?这将删除所有缓存条目。"
|
||||
},
|
||||
"statistics": {
|
||||
"title": "缓存统计",
|
||||
"cachedEntries": "已缓存条目",
|
||||
"storageUsed": "已用存储",
|
||||
"totalSize": "总大小"
|
||||
},
|
||||
"actions": {
|
||||
"clear": "清除缓存",
|
||||
"confirmClear": "确定要清除缓存吗?"
|
||||
},
|
||||
"messages": {
|
||||
"cacheCleared": "缓存已清除",
|
||||
"cacheSettingsUpdated": "缓存设置已更新"
|
||||
}
|
||||
},
|
||||
"modelPools": {
|
||||
"title": "模型池",
|
||||
"description": "配置模型池以实现高可用性和负载均衡",
|
||||
"stats": {
|
||||
"total": "总池数",
|
||||
"enabled": "已启用"
|
||||
},
|
||||
"actions": {
|
||||
"add": "添加模型池",
|
||||
"edit": "编辑模型池",
|
||||
"delete": "删除模型池",
|
||||
"autoDiscover": "自动发现"
|
||||
},
|
||||
"deleteConfirm": "确定要删除模型池 \"{name}\" 吗?",
|
||||
"emptyState": {
|
||||
"title": "未找到模型池",
|
||||
"message": "添加模型池以启用高可用性和负载均衡。"
|
||||
},
|
||||
"modelType": "模型类型",
|
||||
"embedding": "嵌入",
|
||||
"llm": "LLM",
|
||||
"reranker": "重排序",
|
||||
"targetModel": "目标模型",
|
||||
"selectTargetModel": "选择目标模型...",
|
||||
"strategy": "策略",
|
||||
"roundRobin": "轮询",
|
||||
"latencyAware": "延迟感知",
|
||||
"weightedRandom": "加权随机",
|
||||
"poolEnabled": "池已启用",
|
||||
"autoDiscover": "自动发现提供商",
|
||||
"excludedProviders": "排除的提供商",
|
||||
"defaultCooldown": "默认冷却时间(秒)",
|
||||
"defaultConcurrent": "默认并发数",
|
||||
"discoveredProviders": "已发现的提供商",
|
||||
"excludeProvider": "排除",
|
||||
"includeProvider": "包含",
|
||||
"noProvidersFound": "未找到提供此模型的提供商",
|
||||
"poolSaved": "模型池已保存",
|
||||
"embeddingPoolDesc": "配置模型池以实现跨多个提供商的高可用性和负载均衡",
|
||||
"embeddingPool": "嵌入池",
|
||||
"discovered": "已发现",
|
||||
"providers": "提供商"
|
||||
},
|
||||
"cliSettings": {
|
||||
"title": "CLI 设置",
|
||||
"description": "配置 CLI 工具设置和模式",
|
||||
"stats": {
|
||||
"total": "总设置数",
|
||||
"enabled": "已启用"
|
||||
},
|
||||
"actions": {
|
||||
"add": "添加 CLI 设置",
|
||||
"edit": "编辑 CLI 设置",
|
||||
"delete": "删除 CLI 设置"
|
||||
},
|
||||
"deleteConfirm": "确定要删除 CLI 设置 \"{name}\" 吗?",
|
||||
"emptyState": {
|
||||
"title": "未找到 CLI 设置",
|
||||
"message": "添加 CLI 设置以配置工具特定选项。"
|
||||
},
|
||||
"mode": "模式",
|
||||
"providerBased": "基于提供商",
|
||||
"direct": "直接",
|
||||
"authToken": "认证令牌",
|
||||
"baseUrl": "基础 URL",
|
||||
"model": "模型"
|
||||
},
|
||||
"ccwLitellm": {
|
||||
"title": "CCW-LiteLLM 包",
|
||||
"description": "管理 ccw-litellm Python 包安装",
|
||||
"status": {
|
||||
"installed": "已安装",
|
||||
"notInstalled": "未安装",
|
||||
"version": "版本"
|
||||
},
|
||||
"actions": {
|
||||
"install": "安装",
|
||||
"uninstall": "卸载",
|
||||
"refreshStatus": "刷新状态"
|
||||
},
|
||||
"messages": {
|
||||
"installSuccess": "ccw-litellm 安装成功",
|
||||
"installFailed": "ccw-litellm 安装失败",
|
||||
"uninstallSuccess": "ccw-litellm 卸载成功",
|
||||
"uninstallFailed": "ccw-litellm 卸载失败"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
"confirm": "确认",
|
||||
"delete": "删除",
|
||||
"edit": "编辑",
|
||||
"add": "添加",
|
||||
"close": "关闭",
|
||||
"loading": "加载中...",
|
||||
"error": "错误",
|
||||
"success": "成功",
|
||||
"warning": "警告",
|
||||
"optional": "可选",
|
||||
"enabled": "已启用",
|
||||
"disabled": "已禁用",
|
||||
"searchPlaceholder": "搜索...",
|
||||
"noResults": "未找到结果",
|
||||
"actions": "操作",
|
||||
"name": "名称",
|
||||
"description": "描述",
|
||||
"type": "类型",
|
||||
"status": "状态"
|
||||
},
|
||||
"messages": {
|
||||
"settingsSaved": "设置保存成功",
|
||||
"settingsDeleted": "设置删除成功",
|
||||
"providerSaved": "提供商保存成功",
|
||||
"providerDeleted": "提供商删除成功",
|
||||
"providerUpdated": "提供商更新成功",
|
||||
"endpointSaved": "端点保存成功",
|
||||
"endpointDeleted": "端点删除成功",
|
||||
"confirmDeleteProvider": "确定要删除此提供商吗?",
|
||||
"confirmDeleteEndpoint": "确定要删除此端点吗?",
|
||||
"confirmClearCache": "确定要清除缓存吗?",
|
||||
"invalidJsonHeaders": "自定义请求头的 JSON 格式无效",
|
||||
"failedToLoad": "加载数据失败",
|
||||
"noProviders": "无提供商",
|
||||
"noProvidersHint": "添加提供商以开始配置 API。",
|
||||
"noEndpoints": "无端点",
|
||||
"noEndpointsHint": "添加端点以创建自定义 API 映射。",
|
||||
"configSynced": "配置已同步到 YAML 文件"
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@
|
||||
"overview": "概览",
|
||||
"settings": "设置",
|
||||
"models": "模型",
|
||||
"search": "搜索",
|
||||
"advanced": "高级"
|
||||
},
|
||||
"overview": {
|
||||
@@ -114,6 +115,8 @@
|
||||
"advanced": {
|
||||
"warningTitle": "敏感操作警告",
|
||||
"warningMessage": "修改环境变量可能影响 CodexLens 的正常运行。请确保您了解每个变量的作用。",
|
||||
"loadError": "加载环境变量失败",
|
||||
"loadErrorDesc": "无法获取环境配置。请检查 CodexLens 是否正确安装。",
|
||||
"currentVars": "当前环境变量",
|
||||
"settingsVars": "设置变量",
|
||||
"customVars": "自定义变量",
|
||||
@@ -170,9 +173,35 @@
|
||||
"title": "CodexLens 未安装",
|
||||
"description": "请先安装 CodexLens 以使用模型管理功能。"
|
||||
},
|
||||
"error": {
|
||||
"title": "加载模型失败",
|
||||
"description": "无法获取模型列表。请检查 CodexLens 是否正确安装。"
|
||||
},
|
||||
"empty": {
|
||||
"title": "没有找到模型",
|
||||
"description": "尝试调整搜索或筛选条件"
|
||||
"description": "当前没有可用模型。请从列表中下载模型。",
|
||||
"filtered": "没有匹配的模型",
|
||||
"filteredDesc": "尝试调整搜索或筛选条件"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"type": "搜索类型",
|
||||
"content": "内容搜索",
|
||||
"files": "文件搜索",
|
||||
"symbol": "符号搜索",
|
||||
"mode": "模式",
|
||||
"mode.semantic": "语义(默认)",
|
||||
"mode.exact": "精确(FTS)",
|
||||
"mode.fuzzy": "模糊",
|
||||
"query": "查询",
|
||||
"queryPlaceholder": "输入搜索查询...",
|
||||
"button": "搜索",
|
||||
"searching": "搜索中...",
|
||||
"results": "结果",
|
||||
"resultsCount": "个结果",
|
||||
"notInstalled": {
|
||||
"title": "CodexLens 未安装",
|
||||
"description": "请先安装 CodexLens 以使用语义代码搜索功能。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import { SettingsTab } from '@/components/codexlens/SettingsTab';
|
||||
import { AdvancedTab } from '@/components/codexlens/AdvancedTab';
|
||||
import { GpuSelector } from '@/components/codexlens/GpuSelector';
|
||||
import { ModelsTab } from '@/components/codexlens/ModelsTab';
|
||||
import { SearchTab } from '@/components/codexlens/SearchTab';
|
||||
import { useCodexLensDashboard, useCodexLensMutations } from '@/hooks';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
@@ -172,6 +173,9 @@ export function CodexLensManagerPage() {
|
||||
<TabsTrigger value="models">
|
||||
{formatMessage({ id: 'codexlens.tabs.models' })}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="search">
|
||||
{formatMessage({ id: 'codexlens.tabs.search' })}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="advanced">
|
||||
{formatMessage({ id: 'codexlens.tabs.advanced' })}
|
||||
</TabsTrigger>
|
||||
@@ -183,6 +187,7 @@ export function CodexLensManagerPage() {
|
||||
status={status}
|
||||
config={config}
|
||||
isLoading={isLoading}
|
||||
onRefresh={handleRefresh}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
@@ -194,6 +199,10 @@ export function CodexLensManagerPage() {
|
||||
<ModelsTab installed={installed} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="search">
|
||||
<SearchTab enabled={installed} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="advanced">
|
||||
<AdvancedTab enabled={installed} />
|
||||
</TabsContent>
|
||||
|
||||
@@ -77,6 +77,7 @@ interface CliStreamState extends BlockCacheState {
|
||||
outputs: Record<string, CliOutputLine[]>;
|
||||
executions: Record<string, CliExecutionState>;
|
||||
currentExecutionId: string | null;
|
||||
userClosedExecutions: Set<string>; // Track executions closed by user
|
||||
|
||||
// Legacy methods
|
||||
addOutput: (executionId: string, line: CliOutputLine) => void;
|
||||
@@ -87,6 +88,9 @@ interface CliStreamState extends BlockCacheState {
|
||||
getAllExecutions: () => CliExecutionState[];
|
||||
upsertExecution: (executionId: string, exec: Partial<CliExecutionState> & { tool?: string; mode?: string }) => void;
|
||||
removeExecution: (executionId: string) => void;
|
||||
markExecutionClosedByUser: (executionId: string) => void;
|
||||
isExecutionClosedByUser: (executionId: string) => boolean;
|
||||
cleanupUserClosedExecutions: (serverIds: Set<string>) => void;
|
||||
setCurrentExecution: (executionId: string | null) => void;
|
||||
|
||||
// Block cache methods
|
||||
@@ -320,6 +324,7 @@ export const useCliStreamStore = create<CliStreamState>()(
|
||||
outputs: {},
|
||||
executions: {},
|
||||
currentExecutionId: null,
|
||||
userClosedExecutions: new Set<string>(),
|
||||
|
||||
// Block cache state
|
||||
blocks: {},
|
||||
@@ -426,6 +431,35 @@ export const useCliStreamStore = create<CliStreamState>()(
|
||||
}, false, 'cliStream/removeExecution');
|
||||
},
|
||||
|
||||
markExecutionClosedByUser: (executionId: string) => {
|
||||
set((state) => {
|
||||
const newUserClosedExecutions = new Set(state.userClosedExecutions);
|
||||
newUserClosedExecutions.add(executionId);
|
||||
return {
|
||||
userClosedExecutions: newUserClosedExecutions,
|
||||
};
|
||||
}, false, 'cliStream/markExecutionClosedByUser');
|
||||
},
|
||||
|
||||
isExecutionClosedByUser: (executionId: string) => {
|
||||
return get().userClosedExecutions.has(executionId);
|
||||
},
|
||||
|
||||
cleanupUserClosedExecutions: (serverIds: Set<string>) => {
|
||||
set((state) => {
|
||||
const newUserClosedExecutions = new Set<string>();
|
||||
for (const executionId of state.userClosedExecutions) {
|
||||
// Only keep if still on server (user might want to keep it closed)
|
||||
if (serverIds.has(executionId)) {
|
||||
newUserClosedExecutions.add(executionId);
|
||||
}
|
||||
}
|
||||
return {
|
||||
userClosedExecutions: newUserClosedExecutions,
|
||||
};
|
||||
}, false, 'cliStream/cleanupUserClosedExecutions');
|
||||
},
|
||||
|
||||
setCurrentExecution: (executionId: string | null) => {
|
||||
set({ currentExecutionId: executionId }, false, 'cliStream/setCurrentExecution');
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user