mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +08:00
feat: Refactor CLI tool configuration management and introduce skill context loader
- Updated `claude-cli-tools.ts` to support new model configurations and migration from older versions. - Added `getPredefinedModels` and `getAllPredefinedModels` functions for better model management. - Deprecated `cli-config-manager.ts` in favor of `claude-cli-tools.ts`, maintaining backward compatibility. - Introduced `skill-context-loader.ts` to handle skill context loading based on user prompts and keywords. - Enhanced tool configuration functions to include secondary models and improved migration logic. - Updated index file to register the new skill context loader tool.
This commit is contained in:
@@ -308,3 +308,311 @@
|
||||
background: hsl(var(--destructive) / 0.1);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
* CLI Manager Split Layout (Claude Config)
|
||||
* ======================================== */
|
||||
|
||||
.cli-manager-split {
|
||||
display: flex;
|
||||
gap: var(--spacing-md, 1rem);
|
||||
height: 100%;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.cli-manager-sidebar {
|
||||
width: 280px;
|
||||
flex-shrink: 0;
|
||||
overflow-y: auto;
|
||||
background: hsl(var(--card));
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid hsl(var(--border));
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.cli-manager-sidebar h3 {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 0.75rem 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.cli-manager-main {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1.25rem;
|
||||
background: hsl(var(--card));
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
/* Tool List Items */
|
||||
.cli-tool-list-item {
|
||||
padding: 0.625rem 0.75rem;
|
||||
cursor: pointer;
|
||||
border-radius: 0.375rem;
|
||||
transition: all 0.15s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.cli-tool-list-item:hover {
|
||||
background: hsl(var(--accent));
|
||||
}
|
||||
|
||||
.cli-tool-list-item.selected {
|
||||
background: hsl(var(--primary) / 0.1);
|
||||
border-left: 3px solid hsl(var(--primary));
|
||||
padding-left: calc(0.75rem - 3px);
|
||||
}
|
||||
|
||||
.cli-tool-list-item .tool-name {
|
||||
font-weight: 500;
|
||||
font-size: 0.8125rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.cli-tool-list-item .tool-status {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cli-tool-list-item .tool-status.installed {
|
||||
background: hsl(var(--success));
|
||||
}
|
||||
|
||||
.cli-tool-list-item .tool-status.not-installed {
|
||||
background: hsl(var(--muted-foreground) / 0.4);
|
||||
}
|
||||
|
||||
/* Config Mode Toggle */
|
||||
.config-mode-toggle {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.25rem;
|
||||
background: hsl(var(--muted) / 0.5);
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.config-mode-btn {
|
||||
flex: 1;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
background: transparent;
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.config-mode-btn:hover {
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.config-mode-btn.active {
|
||||
background: hsl(var(--background));
|
||||
color: hsl(var(--primary));
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Tool Detail Header */
|
||||
.tool-detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1.25rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.tool-detail-header h3 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Claude Settings Form */
|
||||
.claude-config-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.claude-config-form .form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.claude-config-form .form-group label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--muted-foreground));
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
.claude-config-form .form-control {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.8125rem;
|
||||
font-family: inherit;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.375rem;
|
||||
background: hsl(var(--background));
|
||||
color: hsl(var(--foreground));
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.claude-config-form .form-control:focus {
|
||||
outline: none;
|
||||
border-color: hsl(var(--primary));
|
||||
box-shadow: 0 0 0 2px hsl(var(--primary) / 0.2);
|
||||
}
|
||||
|
||||
.claude-config-form .form-control::placeholder {
|
||||
color: hsl(var(--muted-foreground) / 0.6);
|
||||
}
|
||||
|
||||
/* Model Config Section */
|
||||
.model-config-section {
|
||||
background: hsl(var(--muted) / 0.3);
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.model-config-section h4 {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 0.75rem 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.model-config-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.model-config-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.cli-manager-main .empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 0.875rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.cli-manager-main .empty-state i {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Endpoints List in Config Panel */
|
||||
.claude-endpoints-list {
|
||||
margin-top: 1.5rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.claude-endpoints-list h4 {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 0.75rem 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.claude-endpoint-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.625rem 0.75rem;
|
||||
background: hsl(var(--muted) / 0.3);
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.claude-endpoint-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.claude-endpoint-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.claude-endpoint-name {
|
||||
font-weight: 500;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.claude-endpoint-meta {
|
||||
font-size: 0.6875rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.claude-endpoint-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
/* Config Source Badge */
|
||||
.config-source-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.125rem 0.375rem;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 500;
|
||||
border-radius: 9999px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
.config-source-badge.provider {
|
||||
background: hsl(var(--primary) / 0.15);
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.config-source-badge.direct {
|
||||
background: hsl(var(--success) / 0.15);
|
||||
color: hsl(var(--success));
|
||||
}
|
||||
|
||||
|
||||
@@ -2329,4 +2329,132 @@ select.cli-input {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
Model Pool Detail Info Grid
|
||||
=========================== */
|
||||
|
||||
.provider-detail {
|
||||
padding: 1.5rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.provider-detail .provider-detail-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.provider-detail .provider-detail-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.provider-detail .provider-detail-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.provider-detail .form-section {
|
||||
padding: 1.25rem;
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.provider-detail .form-section h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--muted-foreground));
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.provider-detail .form-section p {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: hsl(var(--foreground));
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Info Grid - Used in Model Pool Detail */
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.info-item label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--muted-foreground));
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.info-item span {
|
||||
font-size: 0.875rem;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
/* Status badge inside info-item should not stretch */
|
||||
.info-item .status-badge {
|
||||
display: inline-flex;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* Excluded Providers List */
|
||||
.excluded-providers-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.excluded-providers-list .tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.625rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
background: hsl(var(--muted) / 0.5);
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.375rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
/* Provider Actions in Detail Header */
|
||||
.provider-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.provider-detail .provider-detail-header {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.provider-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ let defaultCliTool = 'gemini';
|
||||
let promptConcatFormat = localStorage.getItem('ccw-prompt-format') || 'plain'; // plain, yaml, json
|
||||
let cliToolsConfig = {}; // CLI tools enable/disable config
|
||||
let apiEndpoints = []; // API endpoints from LiteLLM config
|
||||
let cliSettingsEndpoints = []; // CLI Settings endpoints (for Claude wrapper)
|
||||
|
||||
// Smart Context settings
|
||||
let smartContextEnabled = localStorage.getItem('ccw-smart-context') === 'true';
|
||||
@@ -43,10 +44,11 @@ async function loadAllStatuses() {
|
||||
semanticStatus = data.semantic || { available: false };
|
||||
ccwInstallStatus = data.ccwInstall || { installed: true, workflowsInstalled: true, missingFiles: [], installPath: '' };
|
||||
|
||||
// Load CLI tools config and API endpoints
|
||||
// Load CLI tools config, API endpoints, and CLI Settings
|
||||
await Promise.all([
|
||||
loadCliToolsConfig(),
|
||||
loadApiEndpoints()
|
||||
loadApiEndpoints(),
|
||||
loadCliSettingsEndpoints()
|
||||
]);
|
||||
|
||||
// Update badges
|
||||
@@ -285,6 +287,22 @@ async function loadApiEndpoints() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load CLI Settings endpoints (Claude wrapper configurations)
|
||||
*/
|
||||
async function loadCliSettingsEndpoints() {
|
||||
try {
|
||||
const response = await fetch('/api/cli/settings');
|
||||
if (!response.ok) return [];
|
||||
const data = await response.json();
|
||||
cliSettingsEndpoints = data.endpoints || [];
|
||||
return cliSettingsEndpoints;
|
||||
} catch (err) {
|
||||
console.error('Failed to load CLI settings endpoints:', err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Badge Update ==========
|
||||
function updateCliBadge() {
|
||||
const badge = document.getElementById('badgeCliTools');
|
||||
@@ -355,6 +373,52 @@ function renderCliStatus() {
|
||||
const isEnabled = config.enabled !== false;
|
||||
const canSetDefault = isAvailable && isEnabled && !isDefault;
|
||||
|
||||
// Special handling for Claude: show CLI Settings info
|
||||
const isClaude = tool === 'claude';
|
||||
const enabledCliSettings = isClaude ? cliSettingsEndpoints.filter(ep => ep.enabled) : [];
|
||||
const hasCliSettings = enabledCliSettings.length > 0;
|
||||
|
||||
// Build CLI Settings badge for Claude
|
||||
let cliSettingsBadge = '';
|
||||
if (isClaude && hasCliSettings) {
|
||||
cliSettingsBadge = `<span class="cli-tool-badge cli-settings-badge" title="${enabledCliSettings.length} endpoint(s) configured">${enabledCliSettings.length} Endpoint${enabledCliSettings.length > 1 ? 's' : ''}</span>`;
|
||||
}
|
||||
|
||||
// Build CLI Settings info for Claude
|
||||
let cliSettingsInfo = '';
|
||||
if (isClaude) {
|
||||
if (hasCliSettings) {
|
||||
const epNames = enabledCliSettings.slice(0, 2).map(ep => ep.name).join(', ');
|
||||
const moreCount = enabledCliSettings.length > 2 ? ` +${enabledCliSettings.length - 2}` : '';
|
||||
cliSettingsInfo = `
|
||||
<div class="cli-settings-info mt-2 p-2 rounded bg-muted/50 text-xs">
|
||||
<div class="flex items-center gap-1 text-muted-foreground mb-1">
|
||||
<i data-lucide="settings-2" class="w-3 h-3"></i>
|
||||
<span>CLI Wrapper Endpoints:</span>
|
||||
</div>
|
||||
<div class="text-foreground font-medium">${epNames}${moreCount}</div>
|
||||
<a href="#" onclick="navigateToApiSettings('cli-settings'); return false;" class="text-primary hover:underline mt-1 inline-flex items-center gap-1">
|
||||
<i data-lucide="external-link" class="w-3 h-3"></i>
|
||||
Configure
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
cliSettingsInfo = `
|
||||
<div class="cli-settings-info mt-2 p-2 rounded bg-muted/30 text-xs">
|
||||
<div class="flex items-center gap-1 text-muted-foreground">
|
||||
<i data-lucide="info" class="w-3 h-3"></i>
|
||||
<span>No CLI wrapper configured</span>
|
||||
</div>
|
||||
<a href="#" onclick="navigateToApiSettings('cli-settings'); return false;" class="text-primary hover:underline mt-1 inline-flex items-center gap-1">
|
||||
<i data-lucide="plus" class="w-3 h-3"></i>
|
||||
Add Endpoint
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="cli-tool-card tool-${tool} ${isAvailable ? 'available' : 'unavailable'} ${!isEnabled ? 'disabled' : ''}">
|
||||
<div class="cli-tool-header">
|
||||
@@ -362,6 +426,7 @@ function renderCliStatus() {
|
||||
<span class="cli-tool-name">${tool.charAt(0).toUpperCase() + tool.slice(1)}</span>
|
||||
${isDefault ? '<span class="cli-tool-badge">Default</span>' : ''}
|
||||
${!isEnabled && isAvailable ? '<span class="cli-tool-badge-disabled">Disabled</span>' : ''}
|
||||
${cliSettingsBadge}
|
||||
</div>
|
||||
<div class="cli-tool-desc text-xs text-muted-foreground mt-1">
|
||||
${toolDescriptions[tool]}
|
||||
@@ -376,6 +441,7 @@ function renderCliStatus() {
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
${cliSettingsInfo}
|
||||
<div class="cli-tool-actions mt-3 flex gap-2">
|
||||
${isAvailable ? (isEnabled
|
||||
? `<button class="btn-sm btn-outline-warning flex items-center gap-1" onclick="toggleCliTool('${tool}', false)">
|
||||
@@ -1323,3 +1389,39 @@ async function startSemanticInstall() {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Navigation ==========
|
||||
/**
|
||||
* Navigate to API Settings page with optional section
|
||||
* @param {string} section - Target section: 'cli-settings', 'providers', 'endpoints'
|
||||
*/
|
||||
function navigateToApiSettings(section) {
|
||||
// Try to switch to API Settings view
|
||||
if (typeof switchView === 'function') {
|
||||
switchView('api-settings');
|
||||
} else if (window.switchView) {
|
||||
window.switchView('api-settings');
|
||||
}
|
||||
|
||||
// After view switch, select the target section
|
||||
setTimeout(() => {
|
||||
if (section === 'cli-settings') {
|
||||
// Click CLI Settings tab if exists
|
||||
const cliSettingsTab = document.querySelector('[data-section="cli-settings"]');
|
||||
if (cliSettingsTab) {
|
||||
cliSettingsTab.click();
|
||||
} else {
|
||||
// Fallback: try to find and click by text content
|
||||
const tabs = document.querySelectorAll('.api-settings-sidebar .sidebar-item, .api-settings-tabs .tab-btn');
|
||||
tabs.forEach(tab => {
|
||||
if (tab.textContent.includes('CLI') || tab.textContent.includes('Wrapper')) {
|
||||
tab.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Export navigation function
|
||||
window.navigateToApiSettings = navigateToApiSettings;
|
||||
|
||||
|
||||
@@ -27,6 +27,9 @@ let poolDiscoveredProviders = {};
|
||||
// CLI Settings state
|
||||
let cliSettingsData = null;
|
||||
let selectedCliSettingsId = null;
|
||||
let cliConfigMode = 'provider'; // 'provider' | 'direct'
|
||||
let isAddingCliSettings = false;
|
||||
let editingCliSettingsId = null;
|
||||
|
||||
// Cache for ccw-litellm status (frontend cache with TTL)
|
||||
let ccwLitellmStatusCache = null;
|
||||
@@ -3711,14 +3714,21 @@ function renderCliSettingsList() {
|
||||
var html = '';
|
||||
endpoints.forEach(function(endpoint) {
|
||||
var isSelected = endpoint.id === selectedCliSettingsId;
|
||||
html += '<div class="provider-item' + (isSelected ? ' selected' : '') + '" onclick="selectCliSettings(\'' + endpoint.id + '\')">' +
|
||||
var isEditing = endpoint.id === editingCliSettingsId;
|
||||
var settings = endpoint.settings || {};
|
||||
var configMode = settings.configMode || (settings.providerId ? 'provider' : 'direct');
|
||||
var configIcon = configMode === 'provider' ? 'link' : 'key';
|
||||
|
||||
html += '<div class="provider-item' + (isSelected || isEditing ? ' selected' : '') + '" onclick="selectCliSettings(\'' + endpoint.id + '\')">' +
|
||||
'<div class="provider-item-content">' +
|
||||
'<div class="provider-icon">' +
|
||||
'<i data-lucide="settings"></i>' +
|
||||
'<i data-lucide="' + configIcon + '"></i>' +
|
||||
'</div>' +
|
||||
'<div class="provider-info">' +
|
||||
'<div class="provider-name">' + escapeHtml(endpoint.name) + '</div>' +
|
||||
'<div class="provider-type">' + (endpoint.settings.model || 'sonnet') + '</div>' +
|
||||
'<div class="provider-type" style="font-size: 0.75rem; color: var(--text-muted);">' +
|
||||
(configMode === 'provider' ? 'Provider' : 'Direct') +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="provider-status' + (endpoint.enabled ? ' enabled' : ' disabled') + '">' +
|
||||
@@ -3759,10 +3769,45 @@ function renderCliSettingsDetail(endpointId) {
|
||||
|
||||
var settings = endpoint.settings || {};
|
||||
var env = settings.env || {};
|
||||
var configMode = settings.configMode || (settings.providerId ? 'provider' : 'direct');
|
||||
var configModeLabel = configMode === 'provider' ? (t('apiSettings.providerBinding') || 'Provider Binding') : (t('apiSettings.directConfig') || 'Direct Configuration');
|
||||
|
||||
// Build model config display
|
||||
var modelConfigHtml = '';
|
||||
if (env.ANTHROPIC_MODEL || env.ANTHROPIC_DEFAULT_HAIKU_MODEL || env.ANTHROPIC_DEFAULT_SONNET_MODEL || env.ANTHROPIC_DEFAULT_OPUS_MODEL) {
|
||||
modelConfigHtml =
|
||||
'<div class="detail-section">' +
|
||||
'<h3>' + (t('apiSettings.modelConfig') || 'Model Configuration') + '</h3>' +
|
||||
'<div class="detail-grid">' +
|
||||
'<div class="detail-item">' +
|
||||
'<label>ANTHROPIC_MODEL</label>' +
|
||||
'<span class="mono">' + escapeHtml(env.ANTHROPIC_MODEL || '-') + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="detail-item">' +
|
||||
'<label>ANTHROPIC_DEFAULT_HAIKU_MODEL</label>' +
|
||||
'<span class="mono">' + escapeHtml(env.ANTHROPIC_DEFAULT_HAIKU_MODEL || '-') + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="detail-item">' +
|
||||
'<label>ANTHROPIC_DEFAULT_SONNET_MODEL</label>' +
|
||||
'<span class="mono">' + escapeHtml(env.ANTHROPIC_DEFAULT_SONNET_MODEL || '-') + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="detail-item">' +
|
||||
'<label>ANTHROPIC_DEFAULT_OPUS_MODEL</label>' +
|
||||
'<span class="mono">' + escapeHtml(env.ANTHROPIC_DEFAULT_OPUS_MODEL || '-') + '</span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
container.innerHTML =
|
||||
'<div class="provider-detail-header">' +
|
||||
'<div>' +
|
||||
'<h2>' + escapeHtml(endpoint.name) + '</h2>' +
|
||||
'<span class="config-source-badge ' + configMode + '" style="margin-top: 0.5rem; display: inline-block;">' +
|
||||
(configMode === 'provider' ? '<i data-lucide="link" style="width: 12px; height: 12px;"></i> ' : '<i data-lucide="key" style="width: 12px; height: 12px;"></i> ') +
|
||||
configModeLabel +
|
||||
'</span>' +
|
||||
'</div>' +
|
||||
'<div class="provider-detail-actions">' +
|
||||
'<button class="btn btn-ghost" onclick="editCliSettings(\'' + endpoint.id + '\')" title="' + t('common.edit') + '">' +
|
||||
'<i data-lucide="edit-2"></i>' +
|
||||
@@ -3781,15 +3826,12 @@ function renderCliSettingsDetail(endpointId) {
|
||||
'<span class="mono">' + escapeHtml(endpoint.id) + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="detail-item">' +
|
||||
'<label>' + t('apiSettings.model') + '</label>' +
|
||||
'<span>' + escapeHtml(settings.model || 'sonnet') + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="detail-item">' +
|
||||
'<label>' + t('apiSettings.status') + '</label>' +
|
||||
'<span class="status-badge ' + (endpoint.enabled ? 'enabled' : 'disabled') + '">' +
|
||||
(endpoint.enabled ? t('common.enabled') : t('common.disabled')) +
|
||||
'</span>' +
|
||||
'</div>' +
|
||||
(endpoint.description ? '<div class="detail-item detail-item-full"><label>' + t('apiSettings.description') + '</label><span>' + escapeHtml(endpoint.description) + '</span></div>' : '') +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="detail-section">' +
|
||||
@@ -3805,6 +3847,7 @@ function renderCliSettingsDetail(endpointId) {
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
modelConfigHtml +
|
||||
'<div class="detail-section">' +
|
||||
'<h3>' + t('apiSettings.settingsFilePath') + '</h3>' +
|
||||
'<div class="code-block">' +
|
||||
@@ -3817,22 +3860,407 @@ function renderCliSettingsDetail(endpointId) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Render CLI Settings empty state
|
||||
* Render CLI Settings empty state or add form
|
||||
*/
|
||||
function renderCliSettingsEmptyState() {
|
||||
var container = document.getElementById('provider-detail-panel');
|
||||
if (!container) return;
|
||||
|
||||
// If adding new settings, show the form
|
||||
if (isAddingCliSettings) {
|
||||
renderCliSettingsForm(null);
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML =
|
||||
'<div class="provider-empty-state">' +
|
||||
'<i data-lucide="settings" class="empty-icon"></i>' +
|
||||
'<h3>' + t('apiSettings.noCliSettingsSelected') + '</h3>' +
|
||||
'<p>' + t('apiSettings.cliSettingsHint') + '</p>' +
|
||||
'<button class="btn btn-primary" style="margin-top: 1rem;" onclick="startAddCliSettings()">' +
|
||||
'<i data-lucide="plus"></i> ' + t('apiSettings.addCliSettings') +
|
||||
'</button>' +
|
||||
'</div>';
|
||||
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start adding new CLI Settings (show form in panel)
|
||||
*/
|
||||
function startAddCliSettings() {
|
||||
isAddingCliSettings = true;
|
||||
selectedCliSettingsId = null;
|
||||
editingCliSettingsId = null;
|
||||
cliConfigMode = 'provider';
|
||||
renderCliSettingsForm(null);
|
||||
renderCliSettingsList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel adding/editing CLI Settings
|
||||
*/
|
||||
function cancelCliSettingsForm() {
|
||||
isAddingCliSettings = false;
|
||||
editingCliSettingsId = null;
|
||||
|
||||
// Re-render detail panel
|
||||
if (selectedCliSettingsId) {
|
||||
renderCliSettingsDetail(selectedCliSettingsId);
|
||||
} else {
|
||||
renderCliSettingsEmptyState();
|
||||
}
|
||||
renderCliSettingsList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render CLI Settings form in detail panel (for add or edit)
|
||||
*/
|
||||
function renderCliSettingsForm(existingEndpoint) {
|
||||
var container = document.getElementById('provider-detail-panel');
|
||||
if (!container) return;
|
||||
|
||||
var isEdit = !!existingEndpoint;
|
||||
var settings = existingEndpoint ? existingEndpoint.settings : { env: {}, model: '' };
|
||||
var env = settings.env || {};
|
||||
|
||||
// Determine initial config mode for editing
|
||||
if (isEdit) {
|
||||
// If settings has configMode, use it; otherwise detect based on providerId
|
||||
cliConfigMode = settings.configMode || (settings.providerId ? 'provider' : 'direct');
|
||||
}
|
||||
|
||||
// Build mode toggle
|
||||
var modeToggleHtml =
|
||||
'<div class="config-mode-toggle">' +
|
||||
'<button type="button" class="config-mode-btn' + (cliConfigMode === 'provider' ? ' active' : '') + '" data-mode="provider" onclick="switchCliConfigMode(\'provider\')">' +
|
||||
'<i data-lucide="link"></i> ' + (t('apiSettings.providerBinding') || 'Provider Binding') +
|
||||
'</button>' +
|
||||
'<button type="button" class="config-mode-btn' + (cliConfigMode === 'direct' ? ' active' : '') + '" data-mode="direct" onclick="switchCliConfigMode(\'direct\')">' +
|
||||
'<i data-lucide="key"></i> ' + (t('apiSettings.directConfig') || 'Direct Configuration') +
|
||||
'</button>' +
|
||||
'</div>';
|
||||
|
||||
// Common fields
|
||||
var commonFieldsHtml =
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-settings-name">' + t('apiSettings.endpointName') + ' *</label>' +
|
||||
'<input type="text" id="cli-settings-name" class="form-control" value="' + escapeHtml(existingEndpoint ? existingEndpoint.name : '') + '" placeholder="My Claude Endpoint" required />' +
|
||||
'</div>' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-settings-description">' + t('apiSettings.description') + '</label>' +
|
||||
'<input type="text" id="cli-settings-description" class="form-control" value="' + escapeHtml(existingEndpoint ? (existingEndpoint.description || '') : '') + '" placeholder="Optional description" />' +
|
||||
'</div>';
|
||||
|
||||
// Mode-specific form content container
|
||||
var formContentHtml = '<div id="cli-config-mode-content"></div>';
|
||||
|
||||
// Enabled toggle
|
||||
var enabledHtml =
|
||||
'<div class="form-group" style="margin-top: 1rem;">' +
|
||||
'<label class="checkbox-label">' +
|
||||
'<input type="checkbox" id="cli-settings-enabled"' + (existingEndpoint ? (existingEndpoint.enabled ? ' checked' : '') : ' checked') + ' />' +
|
||||
' ' + t('common.enabled') +
|
||||
'</label>' +
|
||||
'</div>';
|
||||
|
||||
// Action buttons
|
||||
var actionsHtml =
|
||||
'<div class="form-actions" style="margin-top: 1.5rem; display: flex; gap: 0.75rem; justify-content: flex-end;">' +
|
||||
'<button type="button" class="btn btn-secondary" onclick="cancelCliSettingsForm()">' + t('common.cancel') + '</button>' +
|
||||
'<button type="button" class="btn btn-primary" onclick="submitCliSettingsForm()">' +
|
||||
'<i data-lucide="save"></i> ' + (isEdit ? t('common.save') : t('common.create')) +
|
||||
'</button>' +
|
||||
'</div>';
|
||||
|
||||
container.innerHTML =
|
||||
'<div class="tool-detail-header">' +
|
||||
'<h3><i data-lucide="settings"></i> ' + (isEdit ? t('apiSettings.editCliSettings') : t('apiSettings.addCliSettings')) + '</h3>' +
|
||||
'</div>' +
|
||||
'<div class="claude-config-form">' +
|
||||
(isEdit ? '<input type="hidden" id="cli-settings-id" value="' + existingEndpoint.id + '">' : '') +
|
||||
'<input type="hidden" id="cli-config-mode" value="' + cliConfigMode + '">' +
|
||||
modeToggleHtml +
|
||||
commonFieldsHtml +
|
||||
formContentHtml +
|
||||
enabledHtml +
|
||||
actionsHtml +
|
||||
'</div>';
|
||||
|
||||
if (window.lucide) lucide.createIcons();
|
||||
|
||||
// Render mode-specific content
|
||||
renderCliConfigModeContent(existingEndpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch CLI config mode
|
||||
*/
|
||||
function switchCliConfigMode(mode) {
|
||||
cliConfigMode = mode;
|
||||
|
||||
// Update hidden input
|
||||
var modeInput = document.getElementById('cli-config-mode');
|
||||
if (modeInput) modeInput.value = mode;
|
||||
|
||||
// Update toggle buttons
|
||||
document.querySelectorAll('.config-mode-btn').forEach(function(btn) {
|
||||
btn.classList.toggle('active', btn.dataset.mode === mode);
|
||||
});
|
||||
|
||||
// Re-render mode content while preserving form data
|
||||
var existingEndpoint = null;
|
||||
var idInput = document.getElementById('cli-settings-id');
|
||||
if (idInput && idInput.value && cliSettingsData && cliSettingsData.endpoints) {
|
||||
existingEndpoint = cliSettingsData.endpoints.find(function(e) { return e.id === idInput.value; });
|
||||
}
|
||||
|
||||
renderCliConfigModeContent(existingEndpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render CLI config mode-specific content
|
||||
*/
|
||||
function renderCliConfigModeContent(existingEndpoint) {
|
||||
var container = document.getElementById('cli-config-mode-content');
|
||||
if (!container) return;
|
||||
|
||||
var settings = existingEndpoint ? existingEndpoint.settings : { env: {}, model: '' };
|
||||
var env = settings.env || {};
|
||||
|
||||
if (cliConfigMode === 'provider') {
|
||||
renderProviderModeContent(container, settings);
|
||||
} else {
|
||||
renderDirectModeContent(container, env);
|
||||
}
|
||||
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render Provider Binding mode content
|
||||
*/
|
||||
function renderProviderModeContent(container, settings) {
|
||||
var providers = getAvailableAnthropicProviders();
|
||||
var hasProviders = providers.length > 0;
|
||||
var selectedProviderId = settings.providerId || '';
|
||||
var providerOptionsHtml = buildCliProviderOptions(selectedProviderId);
|
||||
var env = settings.env || {};
|
||||
|
||||
var noProvidersWarning = !hasProviders ?
|
||||
'<div class="info-message" style="margin-bottom: 1rem; padding: 0.75rem; background: hsl(var(--warning) / 0.1); border-radius: 0.375rem; display: flex; align-items: center; gap: 0.5rem;">' +
|
||||
'<i data-lucide="alert-circle" style="width: 16px; height: 16px; color: hsl(var(--warning));"></i>' +
|
||||
'<span style="font-size: 0.8125rem;">' + (t('apiSettings.noAnthropicProviders') || 'No Anthropic providers configured. Please add a provider first.') + '</span>' +
|
||||
'</div>' : '';
|
||||
|
||||
container.innerHTML = noProvidersWarning +
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-settings-provider">' + t('apiSettings.provider') + ' *</label>' +
|
||||
'<select id="cli-settings-provider" class="form-control" onchange="onCliProviderChange()"' + (!hasProviders ? ' disabled' : '') + '>' +
|
||||
providerOptionsHtml +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
// Model Config Section
|
||||
'<div class="model-config-section">' +
|
||||
'<h4><i data-lucide="cpu"></i> ' + (t('apiSettings.modelConfig') || 'Model Configuration') + '</h4>' +
|
||||
'<div class="model-config-grid">' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-model-default">ANTHROPIC_MODEL</label>' +
|
||||
'<input type="text" id="cli-model-default" class="form-control" placeholder="claude-3-5-sonnet-20241022" value="' + escapeHtml(env.ANTHROPIC_MODEL || '') + '" />' +
|
||||
'</div>' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-model-haiku">ANTHROPIC_DEFAULT_HAIKU_MODEL</label>' +
|
||||
'<input type="text" id="cli-model-haiku" class="form-control" placeholder="claude-3-haiku-20240307" value="' + escapeHtml(env.ANTHROPIC_DEFAULT_HAIKU_MODEL || '') + '" />' +
|
||||
'</div>' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-model-sonnet">ANTHROPIC_DEFAULT_SONNET_MODEL</label>' +
|
||||
'<input type="text" id="cli-model-sonnet" class="form-control" placeholder="claude-3-5-sonnet-20241022" value="' + escapeHtml(env.ANTHROPIC_DEFAULT_SONNET_MODEL || '') + '" />' +
|
||||
'</div>' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-model-opus">ANTHROPIC_DEFAULT_OPUS_MODEL</label>' +
|
||||
'<input type="text" id="cli-model-opus" class="form-control" placeholder="claude-3-opus-20240229" value="' + escapeHtml(env.ANTHROPIC_DEFAULT_OPUS_MODEL || '') + '" />' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render Direct Configuration mode content
|
||||
*/
|
||||
function renderDirectModeContent(container, env) {
|
||||
container.innerHTML =
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-auth-token">ANTHROPIC_AUTH_TOKEN *</label>' +
|
||||
'<input type="password" id="cli-auth-token" class="form-control" placeholder="sk-ant-..." value="' + escapeHtml(env.ANTHROPIC_AUTH_TOKEN || '') + '" />' +
|
||||
'</div>' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-base-url">ANTHROPIC_BASE_URL</label>' +
|
||||
'<input type="text" id="cli-base-url" class="form-control" placeholder="https://api.anthropic.com (optional)" value="' + escapeHtml(env.ANTHROPIC_BASE_URL || '') + '" />' +
|
||||
'</div>' +
|
||||
// Model Config Section
|
||||
'<div class="model-config-section">' +
|
||||
'<h4><i data-lucide="cpu"></i> ' + (t('apiSettings.modelConfig') || 'Model Configuration') + '</h4>' +
|
||||
'<div class="model-config-grid">' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-model-default">ANTHROPIC_MODEL</label>' +
|
||||
'<input type="text" id="cli-model-default" class="form-control" placeholder="claude-3-5-sonnet-20241022" value="' + escapeHtml(env.ANTHROPIC_MODEL || '') + '" />' +
|
||||
'</div>' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-model-haiku">ANTHROPIC_DEFAULT_HAIKU_MODEL</label>' +
|
||||
'<input type="text" id="cli-model-haiku" class="form-control" placeholder="claude-3-haiku-20240307" value="' + escapeHtml(env.ANTHROPIC_DEFAULT_HAIKU_MODEL || '') + '" />' +
|
||||
'</div>' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-model-sonnet">ANTHROPIC_DEFAULT_SONNET_MODEL</label>' +
|
||||
'<input type="text" id="cli-model-sonnet" class="form-control" placeholder="claude-3-5-sonnet-20241022" value="' + escapeHtml(env.ANTHROPIC_DEFAULT_SONNET_MODEL || '') + '" />' +
|
||||
'</div>' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-model-opus">ANTHROPIC_DEFAULT_OPUS_MODEL</label>' +
|
||||
'<input type="text" id="cli-model-opus" class="form-control" placeholder="claude-3-opus-20240229" value="' + escapeHtml(env.ANTHROPIC_DEFAULT_OPUS_MODEL || '') + '" />' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit CLI Settings Form (handles both Provider and Direct modes)
|
||||
*/
|
||||
async function submitCliSettingsForm() {
|
||||
// Get common fields
|
||||
var name = document.getElementById('cli-settings-name').value.trim();
|
||||
var description = document.getElementById('cli-settings-description').value.trim();
|
||||
var enabled = document.getElementById('cli-settings-enabled').checked;
|
||||
var idInput = document.getElementById('cli-settings-id');
|
||||
var id = idInput ? idInput.value : null;
|
||||
var configMode = cliConfigMode;
|
||||
|
||||
// Get model configuration fields
|
||||
var anthropicModel = document.getElementById('cli-model-default').value.trim();
|
||||
var haikuModel = document.getElementById('cli-model-haiku').value.trim();
|
||||
var sonnetModel = document.getElementById('cli-model-sonnet').value.trim();
|
||||
var opusModel = document.getElementById('cli-model-opus').value.trim();
|
||||
|
||||
// Validate common fields
|
||||
if (!name) {
|
||||
showRefreshToast(t('apiSettings.nameRequired'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
var data = {
|
||||
name: name,
|
||||
description: description,
|
||||
enabled: enabled,
|
||||
settings: {
|
||||
env: {
|
||||
DISABLE_AUTOUPDATER: '1'
|
||||
},
|
||||
configMode: configMode,
|
||||
includeCoAuthoredBy: false
|
||||
}
|
||||
};
|
||||
|
||||
// Mode-specific handling
|
||||
if (configMode === 'provider') {
|
||||
// Provider binding mode
|
||||
var providerId = document.getElementById('cli-settings-provider').value;
|
||||
|
||||
if (!providerId) {
|
||||
showRefreshToast(t('apiSettings.providerRequired'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get provider credentials
|
||||
var providers = getAvailableAnthropicProviders();
|
||||
var provider = providers.find(function(p) { return p.id === providerId; });
|
||||
|
||||
if (!provider) {
|
||||
showRefreshToast(t('apiSettings.providerNotFound'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy provider credentials to env
|
||||
data.settings.env.ANTHROPIC_AUTH_TOKEN = provider.apiKey || '';
|
||||
if (provider.apiBase) {
|
||||
data.settings.env.ANTHROPIC_BASE_URL = provider.apiBase;
|
||||
}
|
||||
data.settings.providerId = providerId;
|
||||
|
||||
} else {
|
||||
// Direct configuration mode
|
||||
var authToken = document.getElementById('cli-auth-token').value.trim();
|
||||
var baseUrl = document.getElementById('cli-base-url').value.trim();
|
||||
|
||||
if (!authToken) {
|
||||
showRefreshToast(t('apiSettings.authTokenRequired') || 'Auth token is required', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
data.settings.env.ANTHROPIC_AUTH_TOKEN = authToken;
|
||||
if (baseUrl) {
|
||||
data.settings.env.ANTHROPIC_BASE_URL = baseUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// Add model configuration
|
||||
if (anthropicModel) {
|
||||
data.settings.env.ANTHROPIC_MODEL = anthropicModel;
|
||||
}
|
||||
if (haikuModel) {
|
||||
data.settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = haikuModel;
|
||||
}
|
||||
if (sonnetModel) {
|
||||
data.settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL = sonnetModel;
|
||||
}
|
||||
if (opusModel) {
|
||||
data.settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL = opusModel;
|
||||
}
|
||||
|
||||
// Set ID if editing
|
||||
if (id) {
|
||||
data.id = id;
|
||||
}
|
||||
|
||||
// Save endpoint
|
||||
var result = await saveCliSettingsEndpoint(data);
|
||||
if (result && result.success) {
|
||||
// Reset form state
|
||||
isAddingCliSettings = false;
|
||||
editingCliSettingsId = null;
|
||||
|
||||
// Select the newly created/updated endpoint
|
||||
if (result.endpoint && result.endpoint.id) {
|
||||
selectedCliSettingsId = result.endpoint.id;
|
||||
}
|
||||
|
||||
// Refresh view
|
||||
await loadCliSettings();
|
||||
renderCliSettingsList();
|
||||
if (selectedCliSettingsId) {
|
||||
renderCliSettingsDetail(selectedCliSettingsId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit CLI Settings in panel (new panel-based approach)
|
||||
*/
|
||||
function editCliSettingsInPanel(endpointId) {
|
||||
var endpoint = null;
|
||||
if (cliSettingsData && cliSettingsData.endpoints) {
|
||||
endpoint = cliSettingsData.endpoints.find(function(e) { return e.id === endpointId; });
|
||||
}
|
||||
if (endpoint) {
|
||||
isAddingCliSettings = false;
|
||||
editingCliSettingsId = endpointId;
|
||||
|
||||
// Determine config mode from existing settings
|
||||
var settings = endpoint.settings || {};
|
||||
cliConfigMode = settings.configMode || (settings.providerId ? 'provider' : 'direct');
|
||||
|
||||
renderCliSettingsForm(endpoint);
|
||||
renderCliSettingsList();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available Anthropic providers
|
||||
*/
|
||||
@@ -3964,16 +4392,10 @@ function showAddCliSettingsModal(existingEndpoint) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit CLI Settings
|
||||
* Edit CLI Settings (uses panel-based form)
|
||||
*/
|
||||
function editCliSettings(endpointId) {
|
||||
var endpoint = null;
|
||||
if (cliSettingsData && cliSettingsData.endpoints) {
|
||||
endpoint = cliSettingsData.endpoints.find(function(e) { return e.id === endpointId; });
|
||||
}
|
||||
if (endpoint) {
|
||||
showAddCliSettingsModal(endpoint);
|
||||
}
|
||||
editCliSettingsInPanel(endpointId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4381,8 +4803,150 @@ async function submitModelPool(event) {
|
||||
* Edit model pool
|
||||
*/
|
||||
function editModelPool(poolId) {
|
||||
// TODO: Implement edit modal
|
||||
showRefreshToast('Edit functionality coming soon', 'info');
|
||||
var pool = modelPools.find(function(p) { return p.id === poolId; });
|
||||
if (!pool) {
|
||||
showRefreshToast(t('common.error') + ': Pool not found', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
var modalHtml = '<div class="generic-modal-overlay active" id="edit-pool-modal">' +
|
||||
'<div class="generic-modal" style="max-width: 600px;">' +
|
||||
'<div class="generic-modal-header">' +
|
||||
'<h3 class="generic-modal-title">' + t('apiSettings.editModelPool') + '</h3>' +
|
||||
'<button class="generic-modal-close" onclick="closeEditPoolModal()">×</button>' +
|
||||
'</div>' +
|
||||
'<div class="generic-modal-body">' +
|
||||
'<form id="edit-pool-form" class="api-settings-form" onsubmit="submitEditModelPool(event, \'' + poolId + '\')">' +
|
||||
|
||||
'<div class="form-group">' +
|
||||
'<label>' + t('apiSettings.modelType') + '</label>' +
|
||||
'<input type="text" class="cli-input" value="' + (pool.modelType === 'embedding' ? 'Embedding' : pool.modelType === 'llm' ? 'LLM' : 'Reranker') + '" disabled />' +
|
||||
'</div>' +
|
||||
|
||||
'<div class="form-group">' +
|
||||
'<label>' + t('apiSettings.poolName') + '</label>' +
|
||||
'<input type="text" id="edit-pool-name" class="cli-input" value="' + escapeHtml(pool.name || '') + '" placeholder="e.g., Primary Embedding Pool" />' +
|
||||
'</div>' +
|
||||
|
||||
'<div class="form-group">' +
|
||||
'<label>' + t('apiSettings.targetModel') + '</label>' +
|
||||
'<input type="text" class="cli-input" value="' + escapeHtml(pool.targetModel) + '" disabled />' +
|
||||
'</div>' +
|
||||
|
||||
'<div class="form-group">' +
|
||||
'<label>' + t('apiSettings.strategy') + ' *</label>' +
|
||||
'<select id="edit-pool-strategy" class="cli-input" required>' +
|
||||
'<option value="round_robin"' + (pool.strategy === 'round_robin' ? ' selected' : '') + '>Round Robin</option>' +
|
||||
'<option value="latency_aware"' + (pool.strategy === 'latency_aware' ? ' selected' : '') + '>Latency Aware</option>' +
|
||||
'<option value="weighted_random"' + (pool.strategy === 'weighted_random' ? ' selected' : '') + '>Weighted Random</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
|
||||
'<div class="form-group">' +
|
||||
'<label>' + t('apiSettings.cooldown') + ' (seconds)</label>' +
|
||||
'<input type="number" id="edit-pool-cooldown" class="cli-input" value="' + (pool.defaultCooldown || 60) + '" min="0" />' +
|
||||
'</div>' +
|
||||
|
||||
'<div class="form-group">' +
|
||||
'<label>' + t('apiSettings.maxConcurrent') + '</label>' +
|
||||
'<input type="number" id="edit-pool-max-concurrent" class="cli-input" value="' + (pool.defaultMaxConcurrentPerKey || 4) + '" min="1" />' +
|
||||
'</div>' +
|
||||
|
||||
'<div class="form-group">' +
|
||||
'<label>' + t('apiSettings.description') + '</label>' +
|
||||
'<textarea id="edit-pool-description" class="cli-input" rows="2" placeholder="Optional description">' + escapeHtml(pool.description || '') + '</textarea>' +
|
||||
'</div>' +
|
||||
|
||||
'<div class="form-group">' +
|
||||
'<label class="checkbox-label">' +
|
||||
'<input type="checkbox" id="edit-pool-enabled"' + (pool.enabled ? ' checked' : '') + ' /> ' + t('apiSettings.enablePool') +
|
||||
'</label>' +
|
||||
'</div>' +
|
||||
|
||||
'<div class="form-group">' +
|
||||
'<label class="checkbox-label">' +
|
||||
'<input type="checkbox" id="edit-pool-auto-discover"' + (pool.autoDiscover !== false ? ' checked' : '') + ' /> ' + t('apiSettings.autoDiscoverProviders') +
|
||||
'</label>' +
|
||||
'</div>' +
|
||||
|
||||
'</form>' +
|
||||
'</div>' +
|
||||
'<div class="generic-modal-footer">' +
|
||||
'<button class="btn btn-secondary" onclick="closeEditPoolModal()">' + t('common.cancel') + '</button>' +
|
||||
'<button class="btn btn-primary" onclick="document.getElementById(\'edit-pool-form\').requestSubmit()">' + t('common.save') + '</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close edit pool modal
|
||||
*/
|
||||
function closeEditPoolModal() {
|
||||
var modal = document.getElementById('edit-pool-modal');
|
||||
if (modal) modal.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit edit model pool form
|
||||
*/
|
||||
async function submitEditModelPool(event, poolId) {
|
||||
event.preventDefault();
|
||||
|
||||
var pool = modelPools.find(function(p) { return p.id === poolId; });
|
||||
if (!pool) {
|
||||
showRefreshToast(t('common.error') + ': Pool not found', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
var name = document.getElementById('edit-pool-name').value.trim();
|
||||
var strategy = document.getElementById('edit-pool-strategy').value;
|
||||
var cooldown = parseInt(document.getElementById('edit-pool-cooldown').value || '60');
|
||||
var maxConcurrent = parseInt(document.getElementById('edit-pool-max-concurrent').value || '4');
|
||||
var description = document.getElementById('edit-pool-description').value.trim();
|
||||
var enabled = document.getElementById('edit-pool-enabled').checked;
|
||||
var autoDiscover = document.getElementById('edit-pool-auto-discover').checked;
|
||||
|
||||
var poolData = {
|
||||
id: poolId,
|
||||
modelType: pool.modelType,
|
||||
enabled: enabled,
|
||||
name: name || pool.targetModel,
|
||||
targetModel: pool.targetModel,
|
||||
strategy: strategy,
|
||||
autoDiscover: autoDiscover,
|
||||
defaultCooldown: cooldown,
|
||||
defaultMaxConcurrentPerKey: maxConcurrent,
|
||||
description: description || undefined,
|
||||
excludedProviderIds: pool.excludedProviderIds || []
|
||||
};
|
||||
|
||||
try {
|
||||
await initCsrfToken();
|
||||
var response = await csrfFetch('/api/litellm-api/model-pools/' + poolId, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(poolData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
var err = await response.json();
|
||||
throw new Error(err.error || 'Failed to update pool');
|
||||
}
|
||||
|
||||
showRefreshToast(t('apiSettings.poolUpdated'), 'success');
|
||||
closeEditPoolModal();
|
||||
|
||||
// Reload pools and refresh view
|
||||
await loadModelPools();
|
||||
renderModelPoolsList();
|
||||
renderModelPoolDetail(poolId);
|
||||
} catch (err) {
|
||||
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4426,6 +4990,8 @@ window.closeAddPoolModal = closeAddPoolModal;
|
||||
window.onPoolModelTypeChange = onPoolModelTypeChange;
|
||||
window.submitModelPool = submitModelPool;
|
||||
window.editModelPool = editModelPool;
|
||||
window.closeEditPoolModal = closeEditPoolModal;
|
||||
window.submitEditModelPool = submitEditModelPool;
|
||||
window.deleteModelPool = deleteModelPool;
|
||||
|
||||
// Make CLI Settings functions globally accessible
|
||||
@@ -4441,6 +5007,12 @@ window.editCliSettings = editCliSettings;
|
||||
window.closeCliSettingsModal = closeCliSettingsModal;
|
||||
window.submitCliSettings = submitCliSettings;
|
||||
window.onCliProviderChange = onCliProviderChange;
|
||||
// New panel-based CLI Settings functions
|
||||
window.startAddCliSettings = startAddCliSettings;
|
||||
window.cancelCliSettingsForm = cancelCliSettingsForm;
|
||||
window.switchCliConfigMode = switchCliConfigMode;
|
||||
window.submitCliSettingsForm = submitCliSettingsForm;
|
||||
window.editCliSettingsInPanel = editCliSettingsInPanel;
|
||||
|
||||
|
||||
// ========== Utility Functions ==========
|
||||
|
||||
@@ -1,6 +1,75 @@
|
||||
// CodexLens Manager - Configuration, Model Management, and Semantic Dependencies
|
||||
// Extracted from cli-manager.js for better maintainability
|
||||
|
||||
// ============================================================
|
||||
// CACHE MANAGEMENT
|
||||
// ============================================================
|
||||
|
||||
// Cache TTL in milliseconds (30 seconds default)
|
||||
const CODEXLENS_CACHE_TTL = 30000;
|
||||
|
||||
// Cache storage for CodexLens data
|
||||
const codexLensCache = {
|
||||
workspaceStatus: { data: null, timestamp: 0 },
|
||||
config: { data: null, timestamp: 0 },
|
||||
status: { data: null, timestamp: 0 },
|
||||
env: { data: null, timestamp: 0 },
|
||||
models: { data: null, timestamp: 0 },
|
||||
rerankerModels: { data: null, timestamp: 0 },
|
||||
semanticStatus: { data: null, timestamp: 0 },
|
||||
gpuList: { data: null, timestamp: 0 },
|
||||
indexes: { data: null, timestamp: 0 }
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if cache is valid (not expired)
|
||||
* @param {string} key - Cache key
|
||||
* @param {number} ttl - Optional custom TTL
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isCacheValid(key, ttl = CODEXLENS_CACHE_TTL) {
|
||||
const cache = codexLensCache[key];
|
||||
if (!cache || !cache.data) return false;
|
||||
return (Date.now() - cache.timestamp) < ttl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached data
|
||||
* @param {string} key - Cache key
|
||||
* @returns {*} Cached data or null
|
||||
*/
|
||||
function getCachedData(key) {
|
||||
return codexLensCache[key]?.data || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cache data
|
||||
* @param {string} key - Cache key
|
||||
* @param {*} data - Data to cache
|
||||
*/
|
||||
function setCacheData(key, data) {
|
||||
if (codexLensCache[key]) {
|
||||
codexLensCache[key].data = data;
|
||||
codexLensCache[key].timestamp = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate specific cache or all caches
|
||||
* @param {string} key - Cache key (optional, if not provided clears all)
|
||||
*/
|
||||
function invalidateCache(key) {
|
||||
if (key && codexLensCache[key]) {
|
||||
codexLensCache[key].data = null;
|
||||
codexLensCache[key].timestamp = 0;
|
||||
} else if (!key) {
|
||||
Object.keys(codexLensCache).forEach(function(k) {
|
||||
codexLensCache[k].data = null;
|
||||
codexLensCache[k].timestamp = 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// UTILITY FUNCTIONS
|
||||
// ============================================================
|
||||
@@ -25,8 +94,9 @@ function escapeHtml(str) {
|
||||
/**
|
||||
* Refresh workspace index status (FTS and Vector coverage)
|
||||
* Updates both the detailed panel (if exists) and header badges
|
||||
* @param {boolean} forceRefresh - Force refresh, bypass cache
|
||||
*/
|
||||
async function refreshWorkspaceIndexStatus() {
|
||||
async function refreshWorkspaceIndexStatus(forceRefresh) {
|
||||
var container = document.getElementById('workspaceIndexStatusContent');
|
||||
var headerFtsEl = document.getElementById('headerFtsPercent');
|
||||
var headerVectorEl = document.getElementById('headerVectorPercent');
|
||||
@@ -34,6 +104,13 @@ async function refreshWorkspaceIndexStatus() {
|
||||
// If neither container nor header elements exist, nothing to update
|
||||
if (!container && !headerFtsEl) return;
|
||||
|
||||
// Check cache first (unless force refresh)
|
||||
if (!forceRefresh && isCacheValid('workspaceStatus')) {
|
||||
var cachedResult = getCachedData('workspaceStatus');
|
||||
updateWorkspaceStatusUI(cachedResult, container, headerFtsEl, headerVectorEl);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state in container
|
||||
if (container) {
|
||||
container.innerHTML = '<div class="text-xs text-muted-foreground text-center py-2">' +
|
||||
@@ -46,106 +123,10 @@ async function refreshWorkspaceIndexStatus() {
|
||||
var response = await fetch('/api/codexlens/workspace-status');
|
||||
var result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
var ftsPercent = result.hasIndex ? (result.fts.percent || 0) : 0;
|
||||
var vectorPercent = result.hasIndex ? (result.vector.percent || 0) : 0;
|
||||
// Cache the result
|
||||
setCacheData('workspaceStatus', result);
|
||||
|
||||
// Update header badges (always update if elements exist)
|
||||
if (headerFtsEl) {
|
||||
headerFtsEl.textContent = ftsPercent + '%';
|
||||
headerFtsEl.className = 'text-sm font-medium ' +
|
||||
(ftsPercent >= 100 ? 'text-success' : (ftsPercent > 0 ? 'text-blue-500' : 'text-muted-foreground'));
|
||||
}
|
||||
if (headerVectorEl) {
|
||||
headerVectorEl.textContent = vectorPercent.toFixed(1) + '%';
|
||||
headerVectorEl.className = 'text-sm font-medium ' +
|
||||
(vectorPercent >= 100 ? 'text-success' : (vectorPercent >= 50 ? 'text-purple-500' : (vectorPercent > 0 ? 'text-purple-400' : 'text-muted-foreground')));
|
||||
}
|
||||
|
||||
// Update detailed container (if exists)
|
||||
if (container) {
|
||||
var html = '';
|
||||
|
||||
if (!result.hasIndex) {
|
||||
// No index for current workspace
|
||||
html = '<div class="text-center py-3">' +
|
||||
'<div class="text-sm text-muted-foreground mb-2">' +
|
||||
'<i data-lucide="alert-circle" class="w-4 h-4 inline mr-1"></i> ' +
|
||||
(t('codexlens.noIndexFound') || 'No index found for current workspace') +
|
||||
'</div>' +
|
||||
'<button onclick="runFtsFullIndex()" class="text-xs text-primary hover:underline">' +
|
||||
(t('codexlens.createIndex') || 'Create Index') +
|
||||
'</button>' +
|
||||
'</div>';
|
||||
} else {
|
||||
// FTS Status
|
||||
var ftsColor = ftsPercent >= 100 ? 'bg-success' : (ftsPercent > 0 ? 'bg-blue-500' : 'bg-muted-foreground');
|
||||
var ftsTextColor = ftsPercent >= 100 ? 'text-success' : (ftsPercent > 0 ? 'text-blue-500' : 'text-muted-foreground');
|
||||
|
||||
html += '<div class="space-y-1">' +
|
||||
'<div class="flex items-center justify-between text-xs">' +
|
||||
'<span class="flex items-center gap-1.5">' +
|
||||
'<i data-lucide="file-text" class="w-3.5 h-3.5 text-blue-500"></i> ' +
|
||||
'<span class="font-medium">' + (t('codexlens.ftsIndex') || 'FTS Index') + '</span>' +
|
||||
'</span>' +
|
||||
'<span class="' + ftsTextColor + ' font-medium">' + ftsPercent + '%</span>' +
|
||||
'</div>' +
|
||||
'<div class="h-1.5 bg-muted rounded-full overflow-hidden">' +
|
||||
'<div class="h-full ' + ftsColor + ' transition-all duration-300" style="width: ' + ftsPercent + '%"></div>' +
|
||||
'</div>' +
|
||||
'<div class="text-xs text-muted-foreground">' +
|
||||
(result.fts.indexedFiles || 0) + ' / ' + (result.fts.totalFiles || 0) + ' ' + (t('codexlens.filesIndexed') || 'files indexed') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
// Vector Status
|
||||
var vectorColor = vectorPercent >= 100 ? 'bg-success' : (vectorPercent >= 50 ? 'bg-purple-500' : (vectorPercent > 0 ? 'bg-purple-400' : 'bg-muted-foreground'));
|
||||
var vectorTextColor = vectorPercent >= 100 ? 'text-success' : (vectorPercent >= 50 ? 'text-purple-500' : (vectorPercent > 0 ? 'text-purple-400' : 'text-muted-foreground'));
|
||||
|
||||
html += '<div class="space-y-1 mt-3">' +
|
||||
'<div class="flex items-center justify-between text-xs">' +
|
||||
'<span class="flex items-center gap-1.5">' +
|
||||
'<i data-lucide="brain" class="w-3.5 h-3.5 text-purple-500"></i> ' +
|
||||
'<span class="font-medium">' + (t('codexlens.vectorIndex') || 'Vector Index') + '</span>' +
|
||||
'</span>' +
|
||||
'<span class="' + vectorTextColor + ' font-medium">' + vectorPercent.toFixed(1) + '%</span>' +
|
||||
'</div>' +
|
||||
'<div class="h-1.5 bg-muted rounded-full overflow-hidden">' +
|
||||
'<div class="h-full ' + vectorColor + ' transition-all duration-300" style="width: ' + vectorPercent + '%"></div>' +
|
||||
'</div>' +
|
||||
'<div class="text-xs text-muted-foreground">' +
|
||||
(result.vector.filesWithEmbeddings || 0) + ' / ' + (result.vector.totalFiles || 0) + ' ' + (t('codexlens.filesWithEmbeddings') || 'files with embeddings') +
|
||||
(result.vector.totalChunks > 0 ? ' (' + result.vector.totalChunks + ' chunks)' : '') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
// Vector search availability indicator
|
||||
if (vectorPercent >= 50) {
|
||||
html += '<div class="flex items-center gap-1.5 mt-2 pt-2 border-t border-border">' +
|
||||
'<i data-lucide="check-circle-2" class="w-3.5 h-3.5 text-success"></i>' +
|
||||
'<span class="text-xs text-success">' + (t('codexlens.vectorSearchEnabled') || 'Vector search enabled') + '</span>' +
|
||||
'</div>';
|
||||
} else if (vectorPercent > 0) {
|
||||
html += '<div class="flex items-center gap-1.5 mt-2 pt-2 border-t border-border">' +
|
||||
'<i data-lucide="alert-triangle" class="w-3.5 h-3.5 text-warning"></i>' +
|
||||
'<span class="text-xs text-warning">' + (t('codexlens.vectorSearchPartial') || 'Vector search requires ≥50% coverage') + '</span>' +
|
||||
'</div>';
|
||||
}
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
} else {
|
||||
// Error from API
|
||||
if (headerFtsEl) headerFtsEl.textContent = '--';
|
||||
if (headerVectorEl) headerVectorEl.textContent = '--';
|
||||
if (container) {
|
||||
container.innerHTML = '<div class="text-xs text-destructive text-center py-2">' +
|
||||
'<i data-lucide="alert-circle" class="w-4 h-4 inline mr-1"></i> ' +
|
||||
(result.error || t('common.error') || 'Error loading status') +
|
||||
'</div>';
|
||||
}
|
||||
}
|
||||
updateWorkspaceStatusUI(result, container, headerFtsEl, headerVectorEl);
|
||||
} catch (err) {
|
||||
console.error('[CodexLens] Failed to load workspace status:', err);
|
||||
if (headerFtsEl) headerFtsEl.textContent = '--';
|
||||
@@ -161,24 +142,151 @@ async function refreshWorkspaceIndexStatus() {
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update workspace status UI with result data
|
||||
* @param {Object} result - API result
|
||||
* @param {HTMLElement} container - Container element
|
||||
* @param {HTMLElement} headerFtsEl - FTS header element
|
||||
* @param {HTMLElement} headerVectorEl - Vector header element
|
||||
*/
|
||||
function updateWorkspaceStatusUI(result, container, headerFtsEl, headerVectorEl) {
|
||||
if (result.success) {
|
||||
var ftsPercent = result.hasIndex ? (result.fts.percent || 0) : 0;
|
||||
var vectorPercent = result.hasIndex ? (result.vector.percent || 0) : 0;
|
||||
|
||||
// Update header badges (always update if elements exist)
|
||||
if (headerFtsEl) {
|
||||
headerFtsEl.textContent = ftsPercent + '%';
|
||||
headerFtsEl.className = 'text-sm font-medium ' +
|
||||
(ftsPercent >= 100 ? 'text-success' : (ftsPercent > 0 ? 'text-blue-500' : 'text-muted-foreground'));
|
||||
}
|
||||
if (headerVectorEl) {
|
||||
headerVectorEl.textContent = vectorPercent.toFixed(1) + '%';
|
||||
headerVectorEl.className = 'text-sm font-medium ' +
|
||||
(vectorPercent >= 100 ? 'text-success' : (vectorPercent >= 50 ? 'text-purple-500' : (vectorPercent > 0 ? 'text-purple-400' : 'text-muted-foreground')));
|
||||
}
|
||||
|
||||
// Update detailed container (if exists)
|
||||
if (container) {
|
||||
var html = '';
|
||||
|
||||
if (!result.hasIndex) {
|
||||
// No index for current workspace
|
||||
html = '<div class="text-center py-3">' +
|
||||
'<div class="text-sm text-muted-foreground mb-2">' +
|
||||
'<i data-lucide="alert-circle" class="w-4 h-4 inline mr-1"></i> ' +
|
||||
(t('codexlens.noIndexFound') || 'No index found for current workspace') +
|
||||
'</div>' +
|
||||
'<button onclick="runFtsFullIndex()" class="text-xs text-primary hover:underline">' +
|
||||
(t('codexlens.createIndex') || 'Create Index') +
|
||||
'</button>' +
|
||||
'</div>';
|
||||
} else {
|
||||
// FTS Status
|
||||
var ftsColor = ftsPercent >= 100 ? 'bg-success' : (ftsPercent > 0 ? 'bg-blue-500' : 'bg-muted-foreground');
|
||||
var ftsTextColor = ftsPercent >= 100 ? 'text-success' : (ftsPercent > 0 ? 'text-blue-500' : 'text-muted-foreground');
|
||||
|
||||
html += '<div class="space-y-1">' +
|
||||
'<div class="flex items-center justify-between text-xs">' +
|
||||
'<span class="flex items-center gap-1.5">' +
|
||||
'<i data-lucide="file-text" class="w-3.5 h-3.5 text-blue-500"></i> ' +
|
||||
'<span class="font-medium">' + (t('codexlens.ftsIndex') || 'FTS Index') + '</span>' +
|
||||
'</span>' +
|
||||
'<span class="' + ftsTextColor + ' font-medium">' + ftsPercent + '%</span>' +
|
||||
'</div>' +
|
||||
'<div class="h-1.5 bg-muted rounded-full overflow-hidden">' +
|
||||
'<div class="h-full ' + ftsColor + ' transition-all duration-300" style="width: ' + ftsPercent + '%"></div>' +
|
||||
'</div>' +
|
||||
'<div class="text-xs text-muted-foreground">' +
|
||||
(result.fts.indexedFiles || 0) + ' / ' + (result.fts.totalFiles || 0) + ' ' + (t('codexlens.filesIndexed') || 'files indexed') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
// Vector Status
|
||||
var vectorColor = vectorPercent >= 100 ? 'bg-success' : (vectorPercent >= 50 ? 'bg-purple-500' : (vectorPercent > 0 ? 'bg-purple-400' : 'bg-muted-foreground'));
|
||||
var vectorTextColor = vectorPercent >= 100 ? 'text-success' : (vectorPercent >= 50 ? 'text-purple-500' : (vectorPercent > 0 ? 'text-purple-400' : 'text-muted-foreground'));
|
||||
|
||||
html += '<div class="space-y-1 mt-3">' +
|
||||
'<div class="flex items-center justify-between text-xs">' +
|
||||
'<span class="flex items-center gap-1.5">' +
|
||||
'<i data-lucide="brain" class="w-3.5 h-3.5 text-purple-500"></i> ' +
|
||||
'<span class="font-medium">' + (t('codexlens.vectorIndex') || 'Vector Index') + '</span>' +
|
||||
'</span>' +
|
||||
'<span class="' + vectorTextColor + ' font-medium">' + vectorPercent.toFixed(1) + '%</span>' +
|
||||
'</div>' +
|
||||
'<div class="h-1.5 bg-muted rounded-full overflow-hidden">' +
|
||||
'<div class="h-full ' + vectorColor + ' transition-all duration-300" style="width: ' + vectorPercent + '%"></div>' +
|
||||
'</div>' +
|
||||
'<div class="text-xs text-muted-foreground">' +
|
||||
(result.vector.filesWithEmbeddings || 0) + ' / ' + (result.vector.totalFiles || 0) + ' ' + (t('codexlens.filesWithEmbeddings') || 'files with embeddings') +
|
||||
(result.vector.totalChunks > 0 ? ' (' + result.vector.totalChunks + ' chunks)' : '') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
// Vector search availability indicator
|
||||
if (vectorPercent >= 50) {
|
||||
html += '<div class="flex items-center gap-1.5 mt-2 pt-2 border-t border-border">' +
|
||||
'<i data-lucide="check-circle-2" class="w-3.5 h-3.5 text-success"></i>' +
|
||||
'<span class="text-xs text-success">' + (t('codexlens.vectorSearchEnabled') || 'Vector search enabled') + '</span>' +
|
||||
'</div>';
|
||||
} else if (vectorPercent > 0) {
|
||||
html += '<div class="flex items-center gap-1.5 mt-2 pt-2 border-t border-border">' +
|
||||
'<i data-lucide="alert-triangle" class="w-3.5 h-3.5 text-warning"></i>' +
|
||||
'<span class="text-xs text-warning">' + (t('codexlens.vectorSearchPartial') || 'Vector search requires ≥50% coverage') + '</span>' +
|
||||
'</div>';
|
||||
}
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
} else {
|
||||
// Error from API
|
||||
if (headerFtsEl) headerFtsEl.textContent = '--';
|
||||
if (headerVectorEl) headerVectorEl.textContent = '--';
|
||||
if (container) {
|
||||
container.innerHTML = '<div class="text-xs text-destructive text-center py-2">' +
|
||||
'<i data-lucide="alert-circle" class="w-4 h-4 inline mr-1"></i> ' +
|
||||
(result.error || t('common.error') || 'Error loading status') +
|
||||
'</div>';
|
||||
}
|
||||
}
|
||||
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// CODEXLENS CONFIGURATION MODAL
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Show CodexLens configuration modal
|
||||
* @param {boolean} forceRefresh - Force refresh, bypass cache
|
||||
*/
|
||||
async function showCodexLensConfigModal() {
|
||||
async function showCodexLensConfigModal(forceRefresh) {
|
||||
try {
|
||||
showRefreshToast(t('codexlens.loadingConfig'), 'info');
|
||||
// Check cache first for config and status
|
||||
var config, status;
|
||||
var usedCache = false;
|
||||
|
||||
// Fetch current config and status in parallel
|
||||
const [configResponse, statusResponse] = await Promise.all([
|
||||
fetch('/api/codexlens/config'),
|
||||
fetch('/api/codexlens/status')
|
||||
]);
|
||||
const config = await configResponse.json();
|
||||
const status = await statusResponse.json();
|
||||
if (!forceRefresh && isCacheValid('config') && isCacheValid('status')) {
|
||||
config = getCachedData('config');
|
||||
status = getCachedData('status');
|
||||
usedCache = true;
|
||||
} else {
|
||||
showRefreshToast(t('codexlens.loadingConfig'), 'info');
|
||||
|
||||
// Fetch current config and status in parallel
|
||||
const [configResponse, statusResponse] = await Promise.all([
|
||||
fetch('/api/codexlens/config'),
|
||||
fetch('/api/codexlens/status')
|
||||
]);
|
||||
config = await configResponse.json();
|
||||
status = await statusResponse.json();
|
||||
|
||||
// Cache the results
|
||||
setCacheData('config', config);
|
||||
setCacheData('status', status);
|
||||
}
|
||||
|
||||
// Update window.cliToolsStatus to ensure isInstalled is correct
|
||||
if (!window.cliToolsStatus) {
|
||||
@@ -6642,3 +6750,13 @@ async function initIgnorePatternsCount() {
|
||||
}
|
||||
}
|
||||
window.initIgnorePatternsCount = initIgnorePatternsCount;
|
||||
|
||||
// ============================================================
|
||||
// CACHE MANAGEMENT - Global Exports
|
||||
// ============================================================
|
||||
window.invalidateCodexLensCache = invalidateCache;
|
||||
window.refreshCodexLensData = async function(forceRefresh) {
|
||||
invalidateCache();
|
||||
await refreshWorkspaceIndexStatus(true);
|
||||
showRefreshToast(t('common.refreshed') || 'Refreshed', 'success');
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user