mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-10 02:24:35 +08:00
feat: Add custom model download functionality and enhance model management
- Implemented `model-download-custom` command to download HuggingFace models. - Added support for discovering manually placed models in the cache. - Enhanced the model list view to display recommended and discovered models separately. - Introduced JSON editor for direct configuration mode in API settings. - Added validation and formatting features for JSON input. - Updated translations for new API settings and common actions. - Improved user interface for model management, including action buttons and tooltips.
This commit is contained in:
@@ -497,6 +497,46 @@ export async function handleCodexLensConfigRoutes(ctx: RouteContext): Promise<bo
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: CodexLens Model Download Custom (download any HuggingFace model)
|
||||
if (pathname === '/api/codexlens/models/download-custom' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
const { model_name, model_type } = body as { model_name?: unknown; model_type?: unknown };
|
||||
const resolvedModelName = typeof model_name === 'string' && model_name.trim().length > 0 ? model_name.trim() : undefined;
|
||||
const resolvedModelType = typeof model_type === 'string' ? model_type.trim() : 'embedding';
|
||||
|
||||
if (!resolvedModelName) {
|
||||
return { success: false, error: 'model_name is required', status: 400 };
|
||||
}
|
||||
|
||||
// Validate model name format
|
||||
if (!resolvedModelName.includes('/')) {
|
||||
return { success: false, error: 'Invalid model_name format. Expected: org/model-name', status: 400 };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await executeCodexLens([
|
||||
'model-download-custom', resolvedModelName,
|
||||
'--type', resolvedModelType,
|
||||
'--json'
|
||||
], { timeout: 600000 }); // 10 min for download
|
||||
|
||||
if (result.success) {
|
||||
try {
|
||||
const parsed = extractJSON(result.output ?? '');
|
||||
return { success: true, ...parsed };
|
||||
} catch {
|
||||
return { success: true, output: result.output };
|
||||
}
|
||||
} else {
|
||||
return { success: false, error: result.error, status: 500 };
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
return { success: false, error: err instanceof Error ? err.message : String(err), status: 500 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: CodexLens Model Delete (delete embedding model by profile)
|
||||
if (pathname === '/api/codexlens/models/delete' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
@@ -526,6 +566,47 @@ export async function handleCodexLensConfigRoutes(ctx: RouteContext): Promise<bo
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: CodexLens Model Delete by Path (delete discovered/manually placed model)
|
||||
if (pathname === '/api/codexlens/models/delete-path' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
const { cache_path } = body as { cache_path?: unknown };
|
||||
const resolvedPath = typeof cache_path === 'string' && cache_path.trim().length > 0 ? cache_path.trim() : undefined;
|
||||
|
||||
if (!resolvedPath) {
|
||||
return { success: false, error: 'cache_path is required', status: 400 };
|
||||
}
|
||||
|
||||
// Security: Validate that the path is within the HuggingFace cache directory
|
||||
const { homedir } = await import('os');
|
||||
const { join, resolve, normalize } = await import('path');
|
||||
const { rm } = await import('fs/promises');
|
||||
|
||||
const hfCacheDir = process.env.HF_HOME || join(homedir(), '.cache', 'huggingface');
|
||||
const normalizedCachePath = normalize(resolve(resolvedPath));
|
||||
const normalizedHfCacheDir = normalize(resolve(hfCacheDir));
|
||||
|
||||
// Ensure the path is within the HuggingFace cache directory
|
||||
if (!normalizedCachePath.startsWith(normalizedHfCacheDir)) {
|
||||
return { success: false, error: 'Path must be within the HuggingFace cache directory', status: 400 };
|
||||
}
|
||||
|
||||
// Ensure it's a models-- directory
|
||||
const pathParts = normalizedCachePath.split(/[/\\]/);
|
||||
const lastPart = pathParts[pathParts.length - 1];
|
||||
if (!lastPart.startsWith('models--')) {
|
||||
return { success: false, error: 'Path must be a model cache directory (models--*)', status: 400 };
|
||||
}
|
||||
|
||||
try {
|
||||
await rm(normalizedCachePath, { recursive: true, force: true });
|
||||
return { success: true, message: 'Model deleted successfully', cache_path: normalizedCachePath };
|
||||
} catch (err: unknown) {
|
||||
return { success: false, error: err instanceof Error ? err.message : String(err), status: 500 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: CodexLens Model Info (get model info by profile)
|
||||
if (pathname === '/api/codexlens/models/info' && req.method === 'GET') {
|
||||
const profile = url.searchParams.get('profile');
|
||||
|
||||
@@ -2457,4 +2457,315 @@ select.cli-input {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
CLI Settings Form
|
||||
=========================== */
|
||||
|
||||
/* Tool Detail Header */
|
||||
.tool-detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
background: hsl(var(--muted) / 0.2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tool-detail-header h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin: 0;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.tool-detail-header h3 svg,
|
||||
.tool-detail-header h3 i {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
/* Claude Config Form Container */
|
||||
.claude-config-form {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* Config Mode Toggle */
|
||||
.config-mode-toggle {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.25rem;
|
||||
background: hsl(var(--muted) / 0.3);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.config-mode-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.625rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--muted-foreground));
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.config-mode-btn:hover {
|
||||
color: hsl(var(--foreground));
|
||||
background: hsl(var(--muted) / 0.5);
|
||||
}
|
||||
|
||||
.config-mode-btn.active {
|
||||
color: hsl(var(--primary));
|
||||
background: hsl(var(--card));
|
||||
border-color: hsl(var(--border));
|
||||
box-shadow: 0 1px 3px hsl(var(--foreground) / 0.05);
|
||||
}
|
||||
|
||||
.config-mode-btn svg,
|
||||
.config-mode-btn i {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
/* Model Config Section */
|
||||
.model-config-section {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
background: hsl(var(--muted) / 0.2);
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.model-config-section h4 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.model-config-section h4 svg,
|
||||
.model-config-section h4 i {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.model-config-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.model-config-grid .form-group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.model-config-grid .form-group label {
|
||||
font-size: 0.75rem;
|
||||
font-family: 'SF Mono', 'Consolas', 'Liberation Mono', monospace;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
/* Form Actions - Sticky at bottom */
|
||||
.claude-config-form .form-actions {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
margin-top: auto;
|
||||
padding: 1rem 0 0;
|
||||
background: hsl(var(--card));
|
||||
border-top: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 640px) {
|
||||
.model-config-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.config-mode-toggle {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
JSON Editor Section
|
||||
=========================== */
|
||||
|
||||
.json-editor-section {
|
||||
margin-top: 1rem;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
background: hsl(var(--card));
|
||||
}
|
||||
|
||||
.json-editor-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 1rem;
|
||||
background: hsl(var(--muted) / 0.3);
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.json-editor-header h4 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.json-editor-header h4 svg,
|
||||
.json-editor-header h4 i {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.json-editor-actions {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.json-editor-actions .btn-ghost {
|
||||
padding: 0.375rem 0.625rem;
|
||||
font-size: 0.75rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: hsl(var(--muted-foreground));
|
||||
cursor: pointer;
|
||||
border-radius: 0.25rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.json-editor-actions .btn-ghost:hover {
|
||||
background: hsl(var(--muted) / 0.5);
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.json-editor-actions .btn-ghost svg,
|
||||
.json-editor-actions .btn-ghost i {
|
||||
width: 0.875rem;
|
||||
height: 0.875rem;
|
||||
}
|
||||
|
||||
.json-editor-body {
|
||||
display: flex;
|
||||
position: relative;
|
||||
min-height: 200px;
|
||||
max-height: 350px;
|
||||
}
|
||||
|
||||
.json-line-numbers {
|
||||
width: 2.5rem;
|
||||
padding: 0.75rem 0.5rem;
|
||||
background: hsl(var(--muted) / 0.2);
|
||||
border-right: 1px solid hsl(var(--border));
|
||||
font-family: 'SF Mono', 'Consolas', 'Liberation Mono', monospace;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
color: hsl(var(--muted-foreground));
|
||||
text-align: right;
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.json-line-numbers span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.json-textarea {
|
||||
flex: 1;
|
||||
padding: 0.75rem;
|
||||
border: none;
|
||||
resize: none;
|
||||
font-family: 'SF Mono', 'Consolas', 'Liberation Mono', monospace;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5;
|
||||
color: hsl(var(--foreground));
|
||||
background: hsl(var(--background));
|
||||
outline: none;
|
||||
overflow-y: auto;
|
||||
tab-size: 2;
|
||||
}
|
||||
|
||||
.json-textarea::placeholder {
|
||||
color: hsl(var(--muted-foreground) / 0.5);
|
||||
}
|
||||
|
||||
.json-textarea.invalid {
|
||||
background: hsl(var(--destructive) / 0.05);
|
||||
border-left: 2px solid hsl(var(--destructive));
|
||||
}
|
||||
|
||||
.json-editor-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 1rem;
|
||||
background: hsl(var(--muted) / 0.2);
|
||||
border-top: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.json-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.json-status svg,
|
||||
.json-status i {
|
||||
width: 0.875rem;
|
||||
height: 0.875rem;
|
||||
}
|
||||
|
||||
.json-status.valid {
|
||||
color: hsl(142 76% 36%);
|
||||
}
|
||||
|
||||
.json-status.invalid {
|
||||
color: hsl(var(--destructive));
|
||||
}
|
||||
|
||||
.json-hint {
|
||||
font-size: 0.6875rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
/* Button styles for JSON editor */
|
||||
.btn-sm {
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
@@ -28,6 +28,7 @@ const i18n = {
|
||||
'common.deleteFailed': 'Delete failed',
|
||||
'common.retry': 'Retry',
|
||||
'common.refresh': 'Refresh',
|
||||
'common.format': 'Format',
|
||||
'common.back': 'Back',
|
||||
'common.search': 'Search...',
|
||||
'common.minutes': 'minutes',
|
||||
@@ -1771,6 +1772,14 @@ const i18n = {
|
||||
'apiSettings.settingsFilePath': 'Settings File Path',
|
||||
'apiSettings.nameRequired': 'Name is required',
|
||||
'apiSettings.status': 'Status',
|
||||
'apiSettings.providerBinding': 'Provider Binding',
|
||||
'apiSettings.directConfig': 'Direct Configuration',
|
||||
'apiSettings.modelConfig': 'Model Configuration',
|
||||
'apiSettings.configJson': 'Configuration JSON',
|
||||
'apiSettings.syncToJson': 'Sync to JSON',
|
||||
'apiSettings.jsonEditorHint': 'Edit JSON directly to add advanced settings',
|
||||
'apiSettings.jsonValid': 'Valid JSON',
|
||||
'apiSettings.jsonInvalid': 'Invalid JSON',
|
||||
|
||||
// Model Pools (High Availability)
|
||||
'apiSettings.modelPools': 'Model Pools',
|
||||
@@ -2153,6 +2162,7 @@ const i18n = {
|
||||
'common.deleteFailed': '删除失败',
|
||||
'common.retry': '重试',
|
||||
'common.refresh': '刷新',
|
||||
'common.format': '格式化',
|
||||
'common.back': '返回',
|
||||
'common.search': '搜索...',
|
||||
'common.minutes': '分钟',
|
||||
@@ -3906,6 +3916,14 @@ const i18n = {
|
||||
'apiSettings.nameRequired': '名称为必填项',
|
||||
'apiSettings.tokenRequired': 'API 令牌为必填项',
|
||||
'apiSettings.status': '状态',
|
||||
'apiSettings.providerBinding': '供应商绑定',
|
||||
'apiSettings.directConfig': '直接配置',
|
||||
'apiSettings.modelConfig': '模型配置',
|
||||
'apiSettings.configJson': '配置 JSON',
|
||||
'apiSettings.syncToJson': '同步到 JSON',
|
||||
'apiSettings.jsonEditorHint': '直接编辑 JSON 添加高级配置',
|
||||
'apiSettings.jsonValid': 'JSON 有效',
|
||||
'apiSettings.jsonInvalid': 'JSON 无效',
|
||||
|
||||
// Model Pools (High Availability)
|
||||
'apiSettings.modelPools': '高可用',
|
||||
|
||||
@@ -4031,10 +4031,13 @@ function renderCliConfigModeContent(existingEndpoint) {
|
||||
if (cliConfigMode === 'provider') {
|
||||
renderProviderModeContent(container, settings);
|
||||
} else {
|
||||
renderDirectModeContent(container, env);
|
||||
renderDirectModeContent(container, env, settings);
|
||||
}
|
||||
|
||||
if (window.lucide) lucide.createIcons();
|
||||
|
||||
// Initialize JSON editor after rendering
|
||||
initCliJsonEditor(settings);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4081,13 +4084,16 @@ function renderProviderModeContent(container, settings) {
|
||||
'<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>';
|
||||
'</div>' +
|
||||
// JSON Preview/Editor Section
|
||||
buildJsonEditorSection(settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render Direct Configuration mode content
|
||||
*/
|
||||
function renderDirectModeContent(container, env) {
|
||||
function renderDirectModeContent(container, env, settings) {
|
||||
settings = settings || { env: env };
|
||||
container.innerHTML =
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-auth-token">ANTHROPIC_AUTH_TOKEN *</label>' +
|
||||
@@ -4118,9 +4124,227 @@ function renderDirectModeContent(container, env) {
|
||||
'<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>' +
|
||||
// JSON Preview/Editor Section
|
||||
buildJsonEditorSection(settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build JSON Editor Section HTML
|
||||
*/
|
||||
function buildJsonEditorSection(settings) {
|
||||
return '<div class="json-editor-section">' +
|
||||
'<div class="json-editor-header">' +
|
||||
'<h4><i data-lucide="code-2"></i> ' + (t('apiSettings.configJson') || 'Configuration JSON') + '</h4>' +
|
||||
'<div class="json-editor-actions">' +
|
||||
'<button type="button" class="btn btn-sm btn-ghost" onclick="formatCliJson()" title="' + (t('common.format') || 'Format') + '">' +
|
||||
'<i data-lucide="align-left"></i> ' + (t('common.format') || 'Format') +
|
||||
'</button>' +
|
||||
'<button type="button" class="btn btn-sm btn-ghost" onclick="syncFormToJson()" title="' + (t('apiSettings.syncToJson') || 'Sync to JSON') + '">' +
|
||||
'<i data-lucide="refresh-cw"></i>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="json-editor-body">' +
|
||||
'<div class="json-line-numbers" id="cli-json-line-numbers"></div>' +
|
||||
'<textarea id="cli-json-editor" class="json-textarea" spellcheck="false" placeholder="{\n "env": {},\n "model": ""\n}"></textarea>' +
|
||||
'</div>' +
|
||||
'<div class="json-editor-footer">' +
|
||||
'<span class="json-status" id="cli-json-status"></span>' +
|
||||
'<span class="json-hint">' + (t('apiSettings.jsonEditorHint') || 'Edit JSON directly to add advanced settings') + '</span>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize JSON Editor with settings data
|
||||
*/
|
||||
function initCliJsonEditor(settings) {
|
||||
var editor = document.getElementById('cli-json-editor');
|
||||
if (!editor) return;
|
||||
|
||||
// Build initial JSON from settings (without sensitive fields for display)
|
||||
var displaySettings = buildDisplaySettings(settings);
|
||||
var jsonStr = JSON.stringify(displaySettings, null, 2);
|
||||
|
||||
editor.value = jsonStr;
|
||||
updateJsonLineNumbers();
|
||||
validateCliJson();
|
||||
|
||||
// Add event listeners
|
||||
editor.addEventListener('input', function() {
|
||||
updateJsonLineNumbers();
|
||||
validateCliJson();
|
||||
});
|
||||
|
||||
editor.addEventListener('scroll', function() {
|
||||
var lineNumbers = document.getElementById('cli-json-line-numbers');
|
||||
if (lineNumbers) {
|
||||
lineNumbers.scrollTop = editor.scrollTop;
|
||||
}
|
||||
});
|
||||
|
||||
editor.addEventListener('keydown', function(e) {
|
||||
// Handle Tab key for indentation
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
var start = this.selectionStart;
|
||||
var end = this.selectionEnd;
|
||||
this.value = this.value.substring(0, start) + ' ' + this.value.substring(end);
|
||||
this.selectionStart = this.selectionEnd = start + 2;
|
||||
updateJsonLineNumbers();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Build display settings object (hide sensitive values)
|
||||
*/
|
||||
function buildDisplaySettings(settings) {
|
||||
var result = {};
|
||||
|
||||
// Copy non-env fields
|
||||
for (var key in settings) {
|
||||
if (key !== 'env' && key !== 'configMode' && key !== 'providerId') {
|
||||
result[key] = settings[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Copy env with masked sensitive values
|
||||
if (settings.env) {
|
||||
result.env = {};
|
||||
for (var envKey in settings.env) {
|
||||
if (envKey === 'ANTHROPIC_AUTH_TOKEN') {
|
||||
// Mask the token
|
||||
var token = settings.env[envKey] || '';
|
||||
result.env[envKey] = token ? (token.substring(0, 10) + '...') : '';
|
||||
} else {
|
||||
result.env[envKey] = settings.env[envKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update JSON line numbers
|
||||
*/
|
||||
function updateJsonLineNumbers() {
|
||||
var editor = document.getElementById('cli-json-editor');
|
||||
var lineNumbers = document.getElementById('cli-json-line-numbers');
|
||||
if (!editor || !lineNumbers) return;
|
||||
|
||||
var lines = editor.value.split('\n').length;
|
||||
var html = '';
|
||||
for (var i = 1; i <= lines; i++) {
|
||||
html += '<span>' + i + '</span>';
|
||||
}
|
||||
lineNumbers.innerHTML = html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate JSON in editor
|
||||
*/
|
||||
function validateCliJson() {
|
||||
var editor = document.getElementById('cli-json-editor');
|
||||
var status = document.getElementById('cli-json-status');
|
||||
if (!editor || !status) return false;
|
||||
|
||||
try {
|
||||
JSON.parse(editor.value);
|
||||
status.innerHTML = '<i data-lucide="check-circle"></i> ' + (t('apiSettings.jsonValid') || 'Valid JSON');
|
||||
status.className = 'json-status valid';
|
||||
editor.classList.remove('invalid');
|
||||
if (window.lucide) lucide.createIcons();
|
||||
return true;
|
||||
} catch (e) {
|
||||
status.innerHTML = '<i data-lucide="alert-circle"></i> ' + (t('apiSettings.jsonInvalid') || 'Invalid JSON') + ': ' + e.message;
|
||||
status.className = 'json-status invalid';
|
||||
editor.classList.add('invalid');
|
||||
if (window.lucide) lucide.createIcons();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format JSON in editor
|
||||
*/
|
||||
function formatCliJson() {
|
||||
var editor = document.getElementById('cli-json-editor');
|
||||
if (!editor) return;
|
||||
|
||||
try {
|
||||
var obj = JSON.parse(editor.value);
|
||||
editor.value = JSON.stringify(obj, null, 2);
|
||||
updateJsonLineNumbers();
|
||||
validateCliJson();
|
||||
} catch (e) {
|
||||
showRefreshToast(t('apiSettings.jsonInvalid') || 'Invalid JSON', 'error');
|
||||
}
|
||||
}
|
||||
window.formatCliJson = formatCliJson;
|
||||
|
||||
/**
|
||||
* Sync form fields to JSON editor
|
||||
*/
|
||||
function syncFormToJson() {
|
||||
var editor = document.getElementById('cli-json-editor');
|
||||
if (!editor) return;
|
||||
|
||||
// Get current JSON
|
||||
var currentObj = {};
|
||||
try {
|
||||
currentObj = JSON.parse(editor.value);
|
||||
} catch (e) {
|
||||
currentObj = { env: {} };
|
||||
}
|
||||
|
||||
// Update env from form fields
|
||||
currentObj.env = currentObj.env || {};
|
||||
|
||||
// Model fields
|
||||
var modelDefault = document.getElementById('cli-model-default');
|
||||
var modelHaiku = document.getElementById('cli-model-haiku');
|
||||
var modelSonnet = document.getElementById('cli-model-sonnet');
|
||||
var modelOpus = document.getElementById('cli-model-opus');
|
||||
|
||||
if (modelDefault && modelDefault.value) currentObj.env.ANTHROPIC_MODEL = modelDefault.value;
|
||||
if (modelHaiku && modelHaiku.value) currentObj.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = modelHaiku.value;
|
||||
if (modelSonnet && modelSonnet.value) currentObj.env.ANTHROPIC_DEFAULT_SONNET_MODEL = modelSonnet.value;
|
||||
if (modelOpus && modelOpus.value) currentObj.env.ANTHROPIC_DEFAULT_OPUS_MODEL = modelOpus.value;
|
||||
|
||||
// Direct mode fields
|
||||
if (cliConfigMode === 'direct') {
|
||||
var authToken = document.getElementById('cli-auth-token');
|
||||
var baseUrl = document.getElementById('cli-base-url');
|
||||
if (authToken && authToken.value) currentObj.env.ANTHROPIC_AUTH_TOKEN = authToken.value;
|
||||
if (baseUrl && baseUrl.value) currentObj.env.ANTHROPIC_BASE_URL = baseUrl.value;
|
||||
}
|
||||
|
||||
// Ensure DISABLE_AUTOUPDATER
|
||||
currentObj.env.DISABLE_AUTOUPDATER = '1';
|
||||
|
||||
editor.value = JSON.stringify(currentObj, null, 2);
|
||||
updateJsonLineNumbers();
|
||||
validateCliJson();
|
||||
}
|
||||
window.syncFormToJson = syncFormToJson;
|
||||
|
||||
/**
|
||||
* Get settings from JSON editor (merges with form data)
|
||||
*/
|
||||
function getSettingsFromJsonEditor() {
|
||||
var editor = document.getElementById('cli-json-editor');
|
||||
if (!editor) return null;
|
||||
|
||||
try {
|
||||
return JSON.parse(editor.value);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit CLI Settings Form (handles both Provider and Direct modes)
|
||||
*/
|
||||
@@ -4214,6 +4438,33 @@ async function submitCliSettingsForm() {
|
||||
data.settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL = opusModel;
|
||||
}
|
||||
|
||||
// Merge additional settings from JSON editor
|
||||
var jsonSettings = getSettingsFromJsonEditor();
|
||||
if (jsonSettings) {
|
||||
// Merge env variables (JSON editor values take precedence for non-core fields)
|
||||
if (jsonSettings.env) {
|
||||
for (var envKey in jsonSettings.env) {
|
||||
// Skip core fields that are managed by form inputs
|
||||
if (envKey === 'ANTHROPIC_AUTH_TOKEN' || envKey === 'ANTHROPIC_BASE_URL') {
|
||||
// Only use JSON editor value if form field is empty
|
||||
if (!data.settings.env[envKey] && jsonSettings.env[envKey] && !jsonSettings.env[envKey].endsWith('...')) {
|
||||
data.settings.env[envKey] = jsonSettings.env[envKey];
|
||||
}
|
||||
} else if (!['ANTHROPIC_MODEL', 'ANTHROPIC_DEFAULT_HAIKU_MODEL', 'ANTHROPIC_DEFAULT_SONNET_MODEL', 'ANTHROPIC_DEFAULT_OPUS_MODEL'].includes(envKey)) {
|
||||
// For non-model env vars, use JSON editor value
|
||||
data.settings.env[envKey] = jsonSettings.env[envKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge non-env settings from JSON editor
|
||||
for (var settingKey in jsonSettings) {
|
||||
if (settingKey !== 'env' && settingKey !== 'configMode' && settingKey !== 'providerId') {
|
||||
data.settings[settingKey] = jsonSettings[settingKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set ID if editing
|
||||
if (id) {
|
||||
data.id = id;
|
||||
|
||||
@@ -2540,7 +2540,15 @@ async function loadModelList() {
|
||||
// Show models for local backend
|
||||
if (embeddingBackend !== 'litellm') {
|
||||
var models = result.result.models;
|
||||
models.forEach(function(model) {
|
||||
var predefinedModels = models.filter(function(m) { return m.source !== 'discovered'; });
|
||||
var discoveredModels = models.filter(function(m) { return m.source === 'discovered'; });
|
||||
|
||||
// Split predefined models into recommended and others
|
||||
var recommendedModels = predefinedModels.filter(function(m) { return m.recommended; });
|
||||
var otherModels = predefinedModels.filter(function(m) { return !m.recommended; });
|
||||
|
||||
// Helper function to render model card
|
||||
function renderModelCard(model) {
|
||||
var statusIcon = model.installed
|
||||
? '<i data-lucide="check-circle" class="w-3.5 h-3.5 text-success"></i>'
|
||||
: '<i data-lucide="circle" class="w-3.5 h-3.5 text-muted"></i>';
|
||||
@@ -2553,11 +2561,15 @@ async function loadModelList() {
|
||||
? '<button class="text-xs text-destructive hover:underline" onclick="deleteModel(\'' + model.profile + '\')">Delete</button>'
|
||||
: '<button class="text-xs text-primary hover:underline" onclick="downloadModel(\'' + model.profile + '\')">Download</button>';
|
||||
|
||||
html +=
|
||||
'<div class="flex items-center justify-between p-2 bg-muted/30 rounded" id="model-' + model.profile + '">' +
|
||||
var recommendedBadge = model.recommended
|
||||
? '<span class="text-[10px] px-1 py-0.5 bg-success/20 text-success rounded">Rec</span>'
|
||||
: '';
|
||||
|
||||
return '<div class="flex items-center justify-between p-2 bg-muted/30 rounded" id="model-' + model.profile + '">' +
|
||||
'<div class="flex items-center gap-2">' +
|
||||
statusIcon +
|
||||
'<span class="text-sm font-medium">' + model.profile + '</span>' +
|
||||
recommendedBadge +
|
||||
'<button class="text-muted-foreground hover:text-foreground p-0.5" onclick="copyToClipboard(\'' + escapeHtml(model.model_name) + '\')" title="' + escapeHtml(model.model_name) + '">' +
|
||||
'<i data-lucide="copy" class="w-3 h-3"></i>' +
|
||||
'</button>' +
|
||||
@@ -2568,7 +2580,108 @@ async function loadModelList() {
|
||||
actionBtn +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
});
|
||||
}
|
||||
|
||||
// Show recommended models (always visible)
|
||||
if (recommendedModels.length > 0) {
|
||||
html += '<div class="text-xs font-medium text-muted-foreground mb-1 mt-2 flex items-center gap-1">' +
|
||||
'<i data-lucide="star" class="w-3 h-3"></i> Recommended Models (' + recommendedModels.length + ')</div>';
|
||||
recommendedModels.forEach(function(model) {
|
||||
html += renderModelCard(model);
|
||||
});
|
||||
}
|
||||
|
||||
// Show other models (collapsed by default)
|
||||
if (otherModels.length > 0) {
|
||||
html += '<div class="mt-3">' +
|
||||
'<button onclick="toggleOtherModels()" class="text-xs font-medium text-muted-foreground mb-1 flex items-center gap-1 hover:text-foreground">' +
|
||||
'<i data-lucide="chevron-right" class="w-3 h-3 transition-transform" id="otherModelsChevron"></i>' +
|
||||
'Other Models (' + otherModels.length + ')' +
|
||||
'</button>' +
|
||||
'<div id="otherModelsContainer" class="hidden space-y-1">';
|
||||
otherModels.forEach(function(model) {
|
||||
html += renderModelCard(model);
|
||||
});
|
||||
html += '</div></div>';
|
||||
}
|
||||
|
||||
// Show discovered models (user manually placed)
|
||||
if (discoveredModels.length > 0) {
|
||||
html += '<div class="text-xs font-medium text-muted-foreground mb-1 mt-3 flex items-center gap-1">' +
|
||||
'<i data-lucide="folder-search" class="w-3 h-3"></i> Discovered Models</div>';
|
||||
discoveredModels.forEach(function(model) {
|
||||
var sizeText = model.actual_size_mb ? model.actual_size_mb.toFixed(0) + ' MB' : 'Unknown';
|
||||
var safeProfile = model.profile.replace(/[^a-zA-Z0-9-_]/g, '-');
|
||||
|
||||
html +=
|
||||
'<div class="flex items-center justify-between p-2 bg-amber-500/10 border border-amber-500/20 rounded" id="model-' + safeProfile + '">' +
|
||||
'<div class="flex items-center gap-2">' +
|
||||
'<i data-lucide="check-circle" class="w-3.5 h-3.5 text-amber-500"></i>' +
|
||||
'<span class="text-sm font-medium">' + escapeHtml(model.model_name) + '</span>' +
|
||||
'<span class="text-[10px] px-1 py-0.5 bg-amber-500/20 text-amber-600 rounded">Manual</span>' +
|
||||
'<span class="text-xs text-muted-foreground">' + (model.dimensions || '?') + 'd</span>' +
|
||||
'</div>' +
|
||||
'<div class="flex items-center gap-3">' +
|
||||
'<span class="text-xs text-muted-foreground">' + sizeText + '</span>' +
|
||||
'<button class="text-xs text-destructive hover:underline" onclick="deleteDiscoveredModel(\'' + escapeHtml(model.cache_path) + '\')">Delete</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
});
|
||||
}
|
||||
|
||||
// Show manual install guide
|
||||
var guide = result.result.manual_install_guide;
|
||||
if (guide) {
|
||||
html += '<div class="mt-4 p-3 bg-blue-500/5 border border-blue-500/20 rounded">' +
|
||||
'<div class="flex items-center gap-1 text-xs font-medium text-blue-600 mb-2">' +
|
||||
'<i data-lucide="download" class="w-3 h-3"></i> Manual Model Installation' +
|
||||
'</div>' +
|
||||
'<div class="text-xs text-muted-foreground space-y-1">';
|
||||
if (guide.steps) {
|
||||
guide.steps.forEach(function(step) {
|
||||
html += '<div>' + escapeHtml(step) + '</div>';
|
||||
});
|
||||
}
|
||||
if (guide.example) {
|
||||
html += '<div class="mt-2 font-mono text-[10px] bg-muted/50 p-1.5 rounded overflow-x-auto">' +
|
||||
'<code>' + escapeHtml(guide.example) + '</code>' +
|
||||
'</div>';
|
||||
}
|
||||
// Show multi-platform paths
|
||||
if (guide.paths) {
|
||||
html += '<div class="mt-2 space-y-1">' +
|
||||
'<div class="text-[10px] font-medium">Cache paths:</div>' +
|
||||
'<div class="font-mono text-[10px] bg-muted/50 p-1.5 rounded space-y-0.5">';
|
||||
if (guide.paths.windows) {
|
||||
html += '<div><span class="text-muted-foreground">Windows:</span> ' + escapeHtml(guide.paths.windows) + '</div>';
|
||||
}
|
||||
if (guide.paths.linux) {
|
||||
html += '<div><span class="text-muted-foreground">Linux:</span> ' + escapeHtml(guide.paths.linux) + '</div>';
|
||||
}
|
||||
if (guide.paths.macos) {
|
||||
html += '<div><span class="text-muted-foreground">macOS:</span> ' + escapeHtml(guide.paths.macos) + '</div>';
|
||||
}
|
||||
html += '</div></div>';
|
||||
}
|
||||
html += '</div></div>';
|
||||
}
|
||||
|
||||
// Custom model download section
|
||||
html += '<div class="mt-4 p-3 bg-green-500/5 border border-green-500/20 rounded">' +
|
||||
'<div class="flex items-center gap-1 text-xs font-medium text-green-600 mb-2">' +
|
||||
'<i data-lucide="plus-circle" class="w-3 h-3"></i> Download Custom Model' +
|
||||
'</div>' +
|
||||
'<div class="flex gap-2">' +
|
||||
'<input type="text" id="customModelInput" placeholder="e.g., BAAI/bge-small-en-v1.5" ' +
|
||||
'class="flex-1 text-xs px-2 py-1.5 border border-border rounded bg-background focus:border-primary focus:ring-1 focus:ring-primary outline-none" />' +
|
||||
'<button onclick="downloadCustomModel()" class="text-xs px-3 py-1.5 bg-primary text-primary-foreground rounded hover:bg-primary/90">' +
|
||||
'Download' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'<div class="text-[10px] text-muted-foreground mt-2">' +
|
||||
'Enter any HuggingFace model name compatible with FastEmbed' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
} else {
|
||||
// LiteLLM backend - show API info
|
||||
html +=
|
||||
@@ -2588,6 +2701,19 @@ async function loadModelList() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle visibility of other (non-recommended) models
|
||||
*/
|
||||
function toggleOtherModels() {
|
||||
var container = document.getElementById('otherModelsContainer');
|
||||
var chevron = document.getElementById('otherModelsChevron');
|
||||
if (container && chevron) {
|
||||
var isHidden = container.classList.contains('hidden');
|
||||
container.classList.toggle('hidden');
|
||||
chevron.style.transform = isHidden ? 'rotate(90deg)' : '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download model (simplified version)
|
||||
*/
|
||||
@@ -2675,6 +2801,82 @@ async function deleteModel(profile) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a custom HuggingFace model by name
|
||||
*/
|
||||
async function downloadCustomModel() {
|
||||
var input = document.getElementById('customModelInput');
|
||||
if (!input) return;
|
||||
|
||||
var modelName = input.value.trim();
|
||||
if (!modelName) {
|
||||
showRefreshToast('Please enter a model name', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!modelName.includes('/')) {
|
||||
showRefreshToast('Invalid format. Use: org/model-name', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable input and show loading
|
||||
input.disabled = true;
|
||||
var originalPlaceholder = input.placeholder;
|
||||
input.placeholder = 'Downloading...';
|
||||
input.value = '';
|
||||
|
||||
try {
|
||||
var response = await fetch('/api/codexlens/models/download-custom', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ model_name: modelName, model_type: 'embedding' })
|
||||
});
|
||||
|
||||
var result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showRefreshToast('Custom model downloaded: ' + modelName, 'success');
|
||||
loadModelList();
|
||||
} else {
|
||||
showRefreshToast('Download failed: ' + result.error, 'error');
|
||||
input.disabled = false;
|
||||
input.placeholder = originalPlaceholder;
|
||||
}
|
||||
} catch (err) {
|
||||
showRefreshToast('Error: ' + err.message, 'error');
|
||||
input.disabled = false;
|
||||
input.placeholder = originalPlaceholder;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a discovered (manually placed) model by its cache path
|
||||
*/
|
||||
async function deleteDiscoveredModel(cachePath) {
|
||||
if (!confirm('Delete this manually placed model?\n\nPath: ' + cachePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var response = await fetch('/api/codexlens/models/delete-path', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ cache_path: cachePath })
|
||||
});
|
||||
|
||||
var result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showRefreshToast('Model deleted successfully', 'success');
|
||||
loadModelList();
|
||||
} else {
|
||||
showRefreshToast('Delete failed: ' + result.error, 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showRefreshToast('Error: ' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// RERANKER MODEL MANAGEMENT
|
||||
// ============================================================
|
||||
|
||||
Reference in New Issue
Block a user