mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
feat: enhance codexlens frontend integration with reranker config, MCP tools card, and LSP management
Add three integration improvements to the CodexLens management panel: - Enhance SettingsTab with RerankerConfigCard using /reranker/config endpoint for dynamic backend/model/provider dropdowns - Add CcwToolsCard to AdvancedTab showing CCW registered tools with codex-lens tools highlighted - Add LspServerCard to OverviewTab with start/stop/restart controls mirroring the FileWatcherCard pattern - Create LSP lifecycle backend endpoints (start/stop/restart) bridging to Python StandaloneLspManager - Add corresponding TanStack Query hooks, API functions, and i18n keys
This commit is contained in:
@@ -14,6 +14,7 @@ import { Badge } from '@/components/ui/Badge';
|
||||
import { useCodexLensEnv, useUpdateCodexLensEnv } from '@/hooks';
|
||||
import { useNotifications } from '@/hooks';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { CcwToolsCard } from './CcwToolsCard';
|
||||
|
||||
interface AdvancedTabProps {
|
||||
enabled?: boolean;
|
||||
@@ -238,6 +239,9 @@ export function AdvancedTab({ enabled = true }: AdvancedTabProps) {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* CCW Tools Card */}
|
||||
<CcwToolsCard />
|
||||
|
||||
{/* Environment Variables Editor */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
|
||||
153
ccw/frontend/src/components/codexlens/CcwToolsCard.tsx
Normal file
153
ccw/frontend/src/components/codexlens/CcwToolsCard.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
// ========================================
|
||||
// CCW Tools Card Component
|
||||
// ========================================
|
||||
// Displays all registered CCW tools, highlighting codex-lens related tools
|
||||
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Wrench, AlertCircle, Loader2 } from 'lucide-react';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { useCcwToolsList } from '@/hooks';
|
||||
import type { CcwToolInfo } from '@/lib/api';
|
||||
|
||||
const CODEX_LENS_PREFIX = 'codex_lens';
|
||||
|
||||
function isCodexLensTool(tool: CcwToolInfo): boolean {
|
||||
return tool.name.startsWith(CODEX_LENS_PREFIX);
|
||||
}
|
||||
|
||||
export function CcwToolsCard() {
|
||||
const { formatMessage } = useIntl();
|
||||
const { tools, isLoading, error } = useCcwToolsList();
|
||||
|
||||
const codexLensTools = tools.filter(isCodexLensTool);
|
||||
const otherTools = tools.filter((t) => !isCodexLensTool(t));
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Wrench className="w-4 h-4" />
|
||||
<span>{formatMessage({ id: 'codexlens.mcp.title' })}</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
<span>{formatMessage({ id: 'codexlens.mcp.loading' })}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Card className="border-destructive/20">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Wrench className="w-4 h-4" />
|
||||
<span>{formatMessage({ id: 'codexlens.mcp.title' })}</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-start gap-2 text-sm">
|
||||
<AlertCircle className="w-4 h-4 text-destructive flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="font-medium text-destructive">
|
||||
{formatMessage({ id: 'codexlens.mcp.error' })}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{formatMessage({ id: 'codexlens.mcp.errorDesc' })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (tools.length === 0) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Wrench className="w-4 h-4" />
|
||||
<span>{formatMessage({ id: 'codexlens.mcp.title' })}</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formatMessage({ id: 'codexlens.mcp.emptyDesc' })}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Wrench className="w-4 h-4" />
|
||||
<span>{formatMessage({ id: 'codexlens.mcp.title' })}</span>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{formatMessage({ id: 'codexlens.mcp.totalCount' }, { count: tools.length })}
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* CodexLens Tools Section */}
|
||||
{codexLensTools.length > 0 && (
|
||||
<div>
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase mb-2">
|
||||
{formatMessage({ id: 'codexlens.mcp.codexLensSection' })}
|
||||
</p>
|
||||
<div className="space-y-1.5">
|
||||
{codexLensTools.map((tool) => (
|
||||
<ToolRow key={tool.name} tool={tool} variant="default" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Other Tools Section */}
|
||||
{otherTools.length > 0 && (
|
||||
<div>
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase mb-2">
|
||||
{formatMessage({ id: 'codexlens.mcp.otherSection' })}
|
||||
</p>
|
||||
<div className="space-y-1.5">
|
||||
{otherTools.map((tool) => (
|
||||
<ToolRow key={tool.name} tool={tool} variant="secondary" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
interface ToolRowProps {
|
||||
tool: CcwToolInfo;
|
||||
variant: 'default' | 'secondary';
|
||||
}
|
||||
|
||||
function ToolRow({ tool, variant }: ToolRowProps) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 py-1 px-2 rounded-md hover:bg-muted/50 transition-colors">
|
||||
<Badge variant={variant} className="text-xs font-mono shrink-0">
|
||||
{tool.name}
|
||||
</Badge>
|
||||
<span className="text-xs text-muted-foreground truncate" title={tool.description}>
|
||||
{tool.description}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CcwToolsCard;
|
||||
157
ccw/frontend/src/components/codexlens/LspServerCard.tsx
Normal file
157
ccw/frontend/src/components/codexlens/LspServerCard.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
// ========================================
|
||||
// CodexLens LSP Server Card
|
||||
// ========================================
|
||||
// Displays LSP server status, stats, and start/stop/restart controls
|
||||
|
||||
import { useIntl } from 'react-intl';
|
||||
import {
|
||||
Server,
|
||||
Power,
|
||||
PowerOff,
|
||||
RotateCw,
|
||||
FolderOpen,
|
||||
Layers,
|
||||
Cpu,
|
||||
} from 'lucide-react';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useCodexLensLspStatus, useCodexLensLspMutations } from '@/hooks';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
|
||||
interface LspServerCardProps {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function LspServerCard({ disabled = false }: LspServerCardProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
const {
|
||||
available,
|
||||
semanticAvailable,
|
||||
projectCount,
|
||||
modes,
|
||||
isLoading,
|
||||
} = useCodexLensLspStatus();
|
||||
const { startLsp, stopLsp, restartLsp, isStarting, isStopping, isRestarting } = useCodexLensLspMutations();
|
||||
|
||||
const isMutating = isStarting || isStopping || isRestarting;
|
||||
|
||||
const handleToggle = async () => {
|
||||
if (available) {
|
||||
await stopLsp(projectPath);
|
||||
} else {
|
||||
await startLsp(projectPath);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRestart = async () => {
|
||||
await restartLsp(projectPath);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Server className="w-4 h-4" />
|
||||
<span>{formatMessage({ id: 'codexlens.lsp.title' })}</span>
|
||||
</div>
|
||||
<Badge variant={available ? 'success' : 'secondary'}>
|
||||
{available
|
||||
? formatMessage({ id: 'codexlens.lsp.status.running' })
|
||||
: formatMessage({ id: 'codexlens.lsp.status.stopped' })
|
||||
}
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<FolderOpen className={cn('w-4 h-4', available ? 'text-success' : 'text-muted-foreground')} />
|
||||
<div>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{formatMessage({ id: 'codexlens.lsp.projects' })}
|
||||
</p>
|
||||
<p className="font-semibold text-foreground">
|
||||
{available ? projectCount : '--'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Cpu className={cn('w-4 h-4', semanticAvailable ? 'text-info' : 'text-muted-foreground')} />
|
||||
<div>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{formatMessage({ id: 'codexlens.lsp.semanticAvailable' })}
|
||||
</p>
|
||||
<p className="font-semibold text-foreground">
|
||||
{semanticAvailable
|
||||
? formatMessage({ id: 'codexlens.lsp.available' })
|
||||
: formatMessage({ id: 'codexlens.lsp.unavailable' })
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Layers className={cn('w-4 h-4', available && modes.length > 0 ? 'text-accent' : 'text-muted-foreground')} />
|
||||
<div>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{formatMessage({ id: 'codexlens.lsp.modes' })}
|
||||
</p>
|
||||
<p className="font-semibold text-foreground">
|
||||
{available ? modes.length : '--'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant={available ? 'outline' : 'default'}
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={handleToggle}
|
||||
disabled={disabled || isMutating || isLoading}
|
||||
>
|
||||
{available ? (
|
||||
<>
|
||||
<PowerOff className="w-4 h-4 mr-2" />
|
||||
{isStopping
|
||||
? formatMessage({ id: 'codexlens.lsp.stopping' })
|
||||
: formatMessage({ id: 'codexlens.lsp.stop' })
|
||||
}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Power className="w-4 h-4 mr-2" />
|
||||
{isStarting
|
||||
? formatMessage({ id: 'codexlens.lsp.starting' })
|
||||
: formatMessage({ id: 'codexlens.lsp.start' })
|
||||
}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
{available && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleRestart}
|
||||
disabled={disabled || isMutating || isLoading}
|
||||
>
|
||||
<RotateCw className={cn('w-4 h-4 mr-2', isRestarting && 'animate-spin')} />
|
||||
{isRestarting
|
||||
? formatMessage({ id: 'codexlens.lsp.restarting' })
|
||||
: formatMessage({ id: 'codexlens.lsp.restart' })
|
||||
}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default LspServerCard;
|
||||
@@ -16,6 +16,7 @@ import { cn } from '@/lib/utils';
|
||||
import type { CodexLensVenvStatus, CodexLensConfig } from '@/lib/api';
|
||||
import { IndexOperations } from './IndexOperations';
|
||||
import { FileWatcherCard } from './FileWatcherCard';
|
||||
import { LspServerCard } from './LspServerCard';
|
||||
|
||||
interface OverviewTabProps {
|
||||
installed: boolean;
|
||||
@@ -143,8 +144,11 @@ export function OverviewTab({ installed, status, config, isLoading, onRefresh }:
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* File Watcher */}
|
||||
<FileWatcherCard disabled={!isReady} />
|
||||
{/* Service Management */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FileWatcherCard disabled={!isReady} />
|
||||
<LspServerCard disabled={!isReady} />
|
||||
</div>
|
||||
|
||||
{/* Index Operations */}
|
||||
<IndexOperations disabled={!isReady} onRefresh={onRefresh} />
|
||||
|
||||
@@ -18,6 +18,8 @@ vi.mock('@/hooks', async (importOriginal) => {
|
||||
useCodexLensEnv: vi.fn(),
|
||||
useUpdateCodexLensEnv: vi.fn(),
|
||||
useCodexLensModels: vi.fn(),
|
||||
useCodexLensRerankerConfig: vi.fn(),
|
||||
useUpdateRerankerConfig: vi.fn(),
|
||||
useNotifications: vi.fn(() => ({
|
||||
toasts: [],
|
||||
wsStatus: 'disconnected' as const,
|
||||
@@ -48,6 +50,8 @@ import {
|
||||
useCodexLensEnv,
|
||||
useUpdateCodexLensEnv,
|
||||
useCodexLensModels,
|
||||
useCodexLensRerankerConfig,
|
||||
useUpdateRerankerConfig,
|
||||
useNotifications,
|
||||
} from '@/hooks';
|
||||
|
||||
@@ -102,6 +106,25 @@ function setupDefaultMocks() {
|
||||
error: null,
|
||||
refetch: vi.fn(),
|
||||
});
|
||||
vi.mocked(useCodexLensRerankerConfig).mockReturnValue({
|
||||
data: undefined,
|
||||
backend: 'fastembed',
|
||||
modelName: '',
|
||||
apiProvider: '',
|
||||
apiKeySet: false,
|
||||
availableBackends: ['onnx', 'api', 'litellm', 'legacy'],
|
||||
apiProviders: ['siliconflow', 'cohere', 'jina'],
|
||||
litellmModels: undefined,
|
||||
configSource: 'default',
|
||||
isLoading: false,
|
||||
error: null,
|
||||
refetch: vi.fn(),
|
||||
});
|
||||
vi.mocked(useUpdateRerankerConfig).mockReturnValue({
|
||||
updateConfig: vi.fn().mockResolvedValue({ success: true, message: 'Saved' }),
|
||||
isUpdating: false,
|
||||
error: null,
|
||||
});
|
||||
}
|
||||
|
||||
describe('SettingsTab', () => {
|
||||
@@ -324,6 +347,25 @@ describe('SettingsTab', () => {
|
||||
error: null,
|
||||
refetch: vi.fn(),
|
||||
});
|
||||
vi.mocked(useCodexLensRerankerConfig).mockReturnValue({
|
||||
data: undefined,
|
||||
backend: 'fastembed',
|
||||
modelName: '',
|
||||
apiProvider: '',
|
||||
apiKeySet: false,
|
||||
availableBackends: [],
|
||||
apiProviders: [],
|
||||
litellmModels: undefined,
|
||||
configSource: 'default',
|
||||
isLoading: false,
|
||||
error: null,
|
||||
refetch: vi.fn(),
|
||||
});
|
||||
vi.mocked(useUpdateRerankerConfig).mockReturnValue({
|
||||
updateConfig: vi.fn().mockResolvedValue({ success: true }),
|
||||
isUpdating: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
render(<SettingsTab enabled={true} />);
|
||||
|
||||
|
||||
@@ -7,22 +7,265 @@
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Save, RefreshCw } from 'lucide-react';
|
||||
import { Save, RefreshCw, Loader2 } from 'lucide-react';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Label } from '@/components/ui/Label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
SelectGroup,
|
||||
SelectLabel,
|
||||
} from '@/components/ui/Select';
|
||||
import {
|
||||
useCodexLensConfig,
|
||||
useCodexLensEnv,
|
||||
useUpdateCodexLensEnv,
|
||||
useCodexLensModels,
|
||||
useCodexLensRerankerConfig,
|
||||
useUpdateRerankerConfig,
|
||||
} from '@/hooks';
|
||||
import { useNotifications } from '@/hooks';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { SchemaFormRenderer } from './SchemaFormRenderer';
|
||||
import { envVarGroupsSchema, getSchemaDefaults } from './envVarSchema';
|
||||
|
||||
// ========== Reranker Configuration Card ==========
|
||||
|
||||
interface RerankerConfigCardProps {
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
function RerankerConfigCard({ enabled = true }: RerankerConfigCardProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
const { success: showSuccess, error: showError } = useNotifications();
|
||||
|
||||
const {
|
||||
backend: serverBackend,
|
||||
modelName: serverModelName,
|
||||
apiProvider: serverApiProvider,
|
||||
apiKeySet,
|
||||
availableBackends,
|
||||
apiProviders,
|
||||
litellmModels,
|
||||
configSource,
|
||||
isLoading,
|
||||
} = useCodexLensRerankerConfig({ enabled });
|
||||
|
||||
const { updateConfig, isUpdating } = useUpdateRerankerConfig();
|
||||
|
||||
const [backend, setBackend] = useState('');
|
||||
const [modelName, setModelName] = useState('');
|
||||
const [apiProvider, setApiProvider] = useState('');
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
|
||||
// Initialize form from server data
|
||||
useEffect(() => {
|
||||
setBackend(serverBackend);
|
||||
setModelName(serverModelName);
|
||||
setApiProvider(serverApiProvider);
|
||||
setHasChanges(false);
|
||||
}, [serverBackend, serverModelName, serverApiProvider]);
|
||||
|
||||
// Detect changes
|
||||
useEffect(() => {
|
||||
const changed =
|
||||
backend !== serverBackend ||
|
||||
modelName !== serverModelName ||
|
||||
apiProvider !== serverApiProvider;
|
||||
setHasChanges(changed);
|
||||
}, [backend, modelName, apiProvider, serverBackend, serverModelName, serverApiProvider]);
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const request: Record<string, string> = {};
|
||||
if (backend !== serverBackend) request.backend = backend;
|
||||
if (modelName !== serverModelName) {
|
||||
// When backend is litellm, model_name is sent as litellm_endpoint
|
||||
if (backend === 'litellm') {
|
||||
request.litellm_endpoint = modelName;
|
||||
} else {
|
||||
request.model_name = modelName;
|
||||
}
|
||||
}
|
||||
if (apiProvider !== serverApiProvider) request.api_provider = apiProvider;
|
||||
|
||||
const result = await updateConfig(request);
|
||||
if (result.success) {
|
||||
showSuccess(
|
||||
formatMessage({ id: 'codexlens.reranker.saveSuccess' }),
|
||||
result.message || ''
|
||||
);
|
||||
} else {
|
||||
showError(
|
||||
formatMessage({ id: 'codexlens.reranker.saveFailed' }),
|
||||
result.error || ''
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
showError(
|
||||
formatMessage({ id: 'codexlens.reranker.saveFailed' }),
|
||||
err instanceof Error ? err.message : ''
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Determine whether to show litellm model dropdown or text input
|
||||
const showLitellmModelSelect = backend === 'litellm' && litellmModels && litellmModels.length > 0;
|
||||
// Show provider dropdown only for api backend
|
||||
const showProviderSelect = backend === 'api';
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
<span>{formatMessage({ id: 'common.loading' })}</span>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-foreground mb-1">
|
||||
{formatMessage({ id: 'codexlens.reranker.title' })}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
{formatMessage({ id: 'codexlens.reranker.description' })}
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Backend Select */}
|
||||
<div className="space-y-2">
|
||||
<Label>{formatMessage({ id: 'codexlens.reranker.backend' })}</Label>
|
||||
<Select value={backend} onValueChange={setBackend}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={formatMessage({ id: 'codexlens.reranker.selectBackend' })} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableBackends.length > 0 ? (
|
||||
availableBackends.map((b) => (
|
||||
<SelectItem key={b} value={b}>
|
||||
{b}
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
<SelectItem value="_none" disabled>
|
||||
{formatMessage({ id: 'codexlens.reranker.noBackends' })}
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'codexlens.reranker.backendHint' })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Model - Select for litellm, Input for others */}
|
||||
<div className="space-y-2">
|
||||
<Label>{formatMessage({ id: 'codexlens.reranker.model' })}</Label>
|
||||
{showLitellmModelSelect ? (
|
||||
<Select value={modelName} onValueChange={setModelName}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={formatMessage({ id: 'codexlens.reranker.selectModel' })} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>
|
||||
{formatMessage({ id: 'codexlens.reranker.litellmModels' })}
|
||||
</SelectLabel>
|
||||
{litellmModels!.map((m) => (
|
||||
<SelectItem key={m.modelId} value={m.modelId}>
|
||||
{m.modelName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Input
|
||||
value={modelName}
|
||||
onChange={(e) => setModelName(e.target.value)}
|
||||
placeholder={formatMessage({ id: 'codexlens.reranker.selectModel' })}
|
||||
/>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'codexlens.reranker.modelHint' })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Provider Select (only for api backend) */}
|
||||
{showProviderSelect && (
|
||||
<div className="space-y-2">
|
||||
<Label>{formatMessage({ id: 'codexlens.reranker.provider' })}</Label>
|
||||
<Select value={apiProvider} onValueChange={setApiProvider}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={formatMessage({ id: 'codexlens.reranker.selectProvider' })} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{apiProviders.length > 0 ? (
|
||||
apiProviders.map((p) => (
|
||||
<SelectItem key={p} value={p}>
|
||||
{p}
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
<SelectItem value="_none" disabled>
|
||||
{formatMessage({ id: 'codexlens.reranker.noProviders' })}
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'codexlens.reranker.providerHint' })}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Status Row */}
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground pt-2 border-t">
|
||||
<span>
|
||||
{formatMessage({ id: 'codexlens.reranker.apiKeyStatus' })}:{' '}
|
||||
<span className={apiKeySet ? 'text-green-600' : 'text-yellow-600'}>
|
||||
{apiKeySet
|
||||
? formatMessage({ id: 'codexlens.reranker.apiKeySet' })
|
||||
: formatMessage({ id: 'codexlens.reranker.apiKeyNotSet' })}
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
{formatMessage({ id: 'codexlens.reranker.configSource' })}: {configSource}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Save Button */}
|
||||
<div className="flex items-center gap-2 pt-2">
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={isUpdating || !hasChanges}
|
||||
size="sm"
|
||||
>
|
||||
{isUpdating ? (
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
{isUpdating
|
||||
? formatMessage({ id: 'codexlens.reranker.saving' })
|
||||
: formatMessage({ id: 'codexlens.reranker.save' })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// ========== Settings Tab ==========
|
||||
|
||||
interface SettingsTabProps {
|
||||
enabled?: boolean;
|
||||
}
|
||||
@@ -219,6 +462,9 @@ export function SettingsTab({ enabled = true }: SettingsTabProps) {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Reranker Configuration */}
|
||||
<RerankerConfigCard enabled={enabled} />
|
||||
|
||||
{/* General Configuration */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-foreground mb-4">
|
||||
|
||||
Reference in New Issue
Block a user