cursor-recursive-rag 0.2.0-alpha.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +179 -203
- package/dist/adapters/llm/anthropic.d.ts +27 -0
- package/dist/adapters/llm/anthropic.d.ts.map +1 -0
- package/dist/adapters/llm/anthropic.js +287 -0
- package/dist/adapters/llm/anthropic.js.map +1 -0
- package/dist/adapters/llm/base.d.ts +62 -0
- package/dist/adapters/llm/base.d.ts.map +1 -0
- package/dist/adapters/llm/base.js +140 -0
- package/dist/adapters/llm/base.js.map +1 -0
- package/dist/adapters/llm/deepseek.d.ts +24 -0
- package/dist/adapters/llm/deepseek.d.ts.map +1 -0
- package/dist/adapters/llm/deepseek.js +228 -0
- package/dist/adapters/llm/deepseek.js.map +1 -0
- package/dist/adapters/llm/groq.d.ts +25 -0
- package/dist/adapters/llm/groq.d.ts.map +1 -0
- package/dist/adapters/llm/groq.js +265 -0
- package/dist/adapters/llm/groq.js.map +1 -0
- package/dist/adapters/llm/index.d.ts +62 -0
- package/dist/adapters/llm/index.d.ts.map +1 -0
- package/dist/adapters/llm/index.js +380 -0
- package/dist/adapters/llm/index.js.map +1 -0
- package/dist/adapters/llm/ollama.d.ts +23 -0
- package/dist/adapters/llm/ollama.d.ts.map +1 -0
- package/dist/adapters/llm/ollama.js +261 -0
- package/dist/adapters/llm/ollama.js.map +1 -0
- package/dist/adapters/llm/openai.d.ts +22 -0
- package/dist/adapters/llm/openai.d.ts.map +1 -0
- package/dist/adapters/llm/openai.js +232 -0
- package/dist/adapters/llm/openai.js.map +1 -0
- package/dist/adapters/llm/openrouter.d.ts +27 -0
- package/dist/adapters/llm/openrouter.d.ts.map +1 -0
- package/dist/adapters/llm/openrouter.js +305 -0
- package/dist/adapters/llm/openrouter.js.map +1 -0
- package/dist/adapters/vector/index.d.ts.map +1 -1
- package/dist/adapters/vector/index.js +8 -0
- package/dist/adapters/vector/index.js.map +1 -1
- package/dist/adapters/vector/redis-native.d.ts +35 -0
- package/dist/adapters/vector/redis-native.d.ts.map +1 -0
- package/dist/adapters/vector/redis-native.js +170 -0
- package/dist/adapters/vector/redis-native.js.map +1 -0
- package/dist/cli/commands/chat.d.ts +4 -0
- package/dist/cli/commands/chat.d.ts.map +1 -0
- package/dist/cli/commands/chat.js +374 -0
- package/dist/cli/commands/chat.js.map +1 -0
- package/dist/cli/commands/maintenance.d.ts +4 -0
- package/dist/cli/commands/maintenance.d.ts.map +1 -0
- package/dist/cli/commands/maintenance.js +237 -0
- package/dist/cli/commands/maintenance.js.map +1 -0
- package/dist/cli/commands/rules.d.ts +9 -0
- package/dist/cli/commands/rules.d.ts.map +1 -0
- package/dist/cli/commands/rules.js +639 -0
- package/dist/cli/commands/rules.js.map +1 -0
- package/dist/cli/commands/setup.js +5 -4
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/index.js +6 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/config/memoryConfig.d.ts +427 -0
- package/dist/config/memoryConfig.d.ts.map +1 -0
- package/dist/config/memoryConfig.js +258 -0
- package/dist/config/memoryConfig.js.map +1 -0
- package/dist/config/rulesConfig.d.ts +486 -0
- package/dist/config/rulesConfig.d.ts.map +1 -0
- package/dist/config/rulesConfig.js +345 -0
- package/dist/config/rulesConfig.js.map +1 -0
- package/dist/dashboard/coreTools.d.ts +14 -0
- package/dist/dashboard/coreTools.d.ts.map +1 -0
- package/dist/dashboard/coreTools.js +413 -0
- package/dist/dashboard/coreTools.js.map +1 -0
- package/dist/dashboard/public/index.html +1982 -13
- package/dist/dashboard/server.d.ts +1 -8
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +846 -13
- package/dist/dashboard/server.js.map +1 -1
- package/dist/dashboard/toolRegistry.d.ts +192 -0
- package/dist/dashboard/toolRegistry.d.ts.map +1 -0
- package/dist/dashboard/toolRegistry.js +322 -0
- package/dist/dashboard/toolRegistry.js.map +1 -0
- package/dist/proxy/index.d.ts +1 -1
- package/dist/proxy/index.d.ts.map +1 -1
- package/dist/proxy/index.js +9 -6
- package/dist/proxy/index.js.map +1 -1
- package/dist/server/index.js +21 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/tools/crawl.d.ts.map +1 -1
- package/dist/server/tools/crawl.js +8 -0
- package/dist/server/tools/crawl.js.map +1 -1
- package/dist/server/tools/index.d.ts.map +1 -1
- package/dist/server/tools/index.js +19 -1
- package/dist/server/tools/index.js.map +1 -1
- package/dist/server/tools/ingest.d.ts.map +1 -1
- package/dist/server/tools/ingest.js +5 -0
- package/dist/server/tools/ingest.js.map +1 -1
- package/dist/server/tools/memory.d.ts +250 -0
- package/dist/server/tools/memory.d.ts.map +1 -0
- package/dist/server/tools/memory.js +472 -0
- package/dist/server/tools/memory.js.map +1 -0
- package/dist/server/tools/recursive-query.d.ts.map +1 -1
- package/dist/server/tools/recursive-query.js +6 -0
- package/dist/server/tools/recursive-query.js.map +1 -1
- package/dist/server/tools/search.d.ts.map +1 -1
- package/dist/server/tools/search.js +6 -0
- package/dist/server/tools/search.js.map +1 -1
- package/dist/services/activity-log.d.ts +10 -0
- package/dist/services/activity-log.d.ts.map +1 -0
- package/dist/services/activity-log.js +53 -0
- package/dist/services/activity-log.js.map +1 -0
- package/dist/services/categoryManager.d.ts +110 -0
- package/dist/services/categoryManager.d.ts.map +1 -0
- package/dist/services/categoryManager.js +549 -0
- package/dist/services/categoryManager.js.map +1 -0
- package/dist/services/contextEnvironment.d.ts +206 -0
- package/dist/services/contextEnvironment.d.ts.map +1 -0
- package/dist/services/contextEnvironment.js +481 -0
- package/dist/services/contextEnvironment.js.map +1 -0
- package/dist/services/conversationProcessor.d.ts +99 -0
- package/dist/services/conversationProcessor.d.ts.map +1 -0
- package/dist/services/conversationProcessor.js +311 -0
- package/dist/services/conversationProcessor.js.map +1 -0
- package/dist/services/cursorChatReader.d.ts +129 -0
- package/dist/services/cursorChatReader.d.ts.map +1 -0
- package/dist/services/cursorChatReader.js +419 -0
- package/dist/services/cursorChatReader.js.map +1 -0
- package/dist/services/decayCalculator.d.ts +85 -0
- package/dist/services/decayCalculator.d.ts.map +1 -0
- package/dist/services/decayCalculator.js +182 -0
- package/dist/services/decayCalculator.js.map +1 -0
- package/dist/services/enhancedVectorStore.d.ts +102 -0
- package/dist/services/enhancedVectorStore.d.ts.map +1 -0
- package/dist/services/enhancedVectorStore.js +245 -0
- package/dist/services/enhancedVectorStore.js.map +1 -0
- package/dist/services/hybridScorer.d.ts +120 -0
- package/dist/services/hybridScorer.d.ts.map +1 -0
- package/dist/services/hybridScorer.js +334 -0
- package/dist/services/hybridScorer.js.map +1 -0
- package/dist/services/knowledgeExtractor.d.ts +45 -0
- package/dist/services/knowledgeExtractor.d.ts.map +1 -0
- package/dist/services/knowledgeExtractor.js +436 -0
- package/dist/services/knowledgeExtractor.js.map +1 -0
- package/dist/services/knowledgeStorage.d.ts +102 -0
- package/dist/services/knowledgeStorage.d.ts.map +1 -0
- package/dist/services/knowledgeStorage.js +383 -0
- package/dist/services/knowledgeStorage.js.map +1 -0
- package/dist/services/maintenanceScheduler.d.ts +89 -0
- package/dist/services/maintenanceScheduler.d.ts.map +1 -0
- package/dist/services/maintenanceScheduler.js +479 -0
- package/dist/services/maintenanceScheduler.js.map +1 -0
- package/dist/services/memoryMetadataStore.d.ts +62 -0
- package/dist/services/memoryMetadataStore.d.ts.map +1 -0
- package/dist/services/memoryMetadataStore.js +570 -0
- package/dist/services/memoryMetadataStore.js.map +1 -0
- package/dist/services/recursiveRetrieval.d.ts +122 -0
- package/dist/services/recursiveRetrieval.d.ts.map +1 -0
- package/dist/services/recursiveRetrieval.js +443 -0
- package/dist/services/recursiveRetrieval.js.map +1 -0
- package/dist/services/relationshipGraph.d.ts +77 -0
- package/dist/services/relationshipGraph.d.ts.map +1 -0
- package/dist/services/relationshipGraph.js +411 -0
- package/dist/services/relationshipGraph.js.map +1 -0
- package/dist/services/rlmSafeguards.d.ts +273 -0
- package/dist/services/rlmSafeguards.d.ts.map +1 -0
- package/dist/services/rlmSafeguards.js +705 -0
- package/dist/services/rlmSafeguards.js.map +1 -0
- package/dist/services/rulesAnalyzer.d.ts +119 -0
- package/dist/services/rulesAnalyzer.d.ts.map +1 -0
- package/dist/services/rulesAnalyzer.js +768 -0
- package/dist/services/rulesAnalyzer.js.map +1 -0
- package/dist/services/rulesMerger.d.ts +75 -0
- package/dist/services/rulesMerger.d.ts.map +1 -0
- package/dist/services/rulesMerger.js +404 -0
- package/dist/services/rulesMerger.js.map +1 -0
- package/dist/services/rulesParser.d.ts +127 -0
- package/dist/services/rulesParser.d.ts.map +1 -0
- package/dist/services/rulesParser.js +594 -0
- package/dist/services/rulesParser.js.map +1 -0
- package/dist/services/smartChunker.d.ts +110 -0
- package/dist/services/smartChunker.d.ts.map +1 -0
- package/dist/services/smartChunker.js +520 -0
- package/dist/services/smartChunker.js.map +1 -0
- package/dist/types/categories.d.ts +105 -0
- package/dist/types/categories.d.ts.map +1 -0
- package/dist/types/categories.js +108 -0
- package/dist/types/categories.js.map +1 -0
- package/dist/types/extractedKnowledge.d.ts +233 -0
- package/dist/types/extractedKnowledge.d.ts.map +1 -0
- package/dist/types/extractedKnowledge.js +56 -0
- package/dist/types/extractedKnowledge.js.map +1 -0
- package/dist/types/index.d.ts +9 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +12 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/llmProvider.d.ts +282 -0
- package/dist/types/llmProvider.d.ts.map +1 -0
- package/dist/types/llmProvider.js +48 -0
- package/dist/types/llmProvider.js.map +1 -0
- package/dist/types/memory.d.ts +227 -0
- package/dist/types/memory.d.ts.map +1 -0
- package/dist/types/memory.js +76 -0
- package/dist/types/memory.js.map +1 -0
- package/dist/types/relationships.d.ts +167 -0
- package/dist/types/relationships.d.ts.map +1 -0
- package/dist/types/relationships.js +106 -0
- package/dist/types/relationships.js.map +1 -0
- package/dist/types/rulesOptimizer.d.ts +345 -0
- package/dist/types/rulesOptimizer.d.ts.map +1 -0
- package/dist/types/rulesOptimizer.js +22 -0
- package/dist/types/rulesOptimizer.js.map +1 -0
- package/docs/cursor-recursive-rag-memory-spec.md +4569 -0
- package/docs/cursor-recursive-rag-tasks.md +1355 -0
- package/package.json +6 -3
- package/restart-rag.sh +16 -0
|
@@ -35,9 +35,24 @@
|
|
|
35
35
|
50% { opacity: 0.5; }
|
|
36
36
|
}
|
|
37
37
|
.pulse-dot { animation: pulse-dot 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
|
|
38
|
+
.modal-backdrop { backdrop-filter: blur(4px); }
|
|
38
39
|
</style>
|
|
39
40
|
</head>
|
|
40
41
|
<body class="bg-gray-950 text-gray-100 min-h-screen">
|
|
42
|
+
<!-- Toast Container -->
|
|
43
|
+
<div id="toast-container" class="fixed top-4 right-4 z-50 space-y-2"></div>
|
|
44
|
+
|
|
45
|
+
<!-- Generic Modal -->
|
|
46
|
+
<div id="app-modal" class="fixed inset-0 bg-black/70 modal-backdrop z-40 hidden flex items-center justify-center p-4">
|
|
47
|
+
<div class="bg-gray-900 border border-gray-700 rounded-xl max-w-md w-full shadow-2xl">
|
|
48
|
+
<div class="p-6">
|
|
49
|
+
<h3 id="modal-title" class="text-lg font-semibold mb-4"></h3>
|
|
50
|
+
<div id="modal-content"></div>
|
|
51
|
+
</div>
|
|
52
|
+
<div id="modal-footer" class="flex justify-end gap-3 px-6 py-4 border-t border-gray-800"></div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
41
56
|
<div id="app" class="flex">
|
|
42
57
|
<!-- Sidebar -->
|
|
43
58
|
<aside class="w-64 bg-gray-900 border-r border-gray-800 min-h-screen p-4 fixed">
|
|
@@ -78,6 +93,12 @@
|
|
|
78
93
|
</svg>
|
|
79
94
|
OpenSkills
|
|
80
95
|
</a>
|
|
96
|
+
<a href="#" onclick="showTab('tools')" id="nav-tools" class="nav-link flex items-center gap-3 px-3 py-2 rounded-lg text-gray-400 hover:bg-gray-800 hover:text-white transition-colors">
|
|
97
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
98
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065zM15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
|
99
|
+
</svg>
|
|
100
|
+
Tools
|
|
101
|
+
</a>
|
|
81
102
|
<a href="#" onclick="showTab('activity')" id="nav-activity" class="nav-link flex items-center gap-3 px-3 py-2 rounded-lg text-gray-400 hover:bg-gray-800 hover:text-white transition-colors">
|
|
82
103
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
83
104
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
|
|
@@ -306,10 +327,211 @@
|
|
|
306
327
|
</div>
|
|
307
328
|
</div>
|
|
308
329
|
|
|
330
|
+
<!-- Tools Tab -->
|
|
331
|
+
<div id="tab-tools" class="tab-content hidden">
|
|
332
|
+
<div class="flex items-center justify-between mb-6">
|
|
333
|
+
<h2 class="text-2xl font-bold">Tools</h2>
|
|
334
|
+
<div class="flex items-center gap-4">
|
|
335
|
+
<span id="tools-count" class="text-sm text-gray-400">0 tools available</span>
|
|
336
|
+
<button onclick="loadTools()" class="text-xs bg-gray-800 hover:bg-gray-700 px-3 py-1 rounded-lg flex items-center gap-2">
|
|
337
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
338
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
|
339
|
+
</svg>
|
|
340
|
+
Refresh
|
|
341
|
+
</button>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
|
|
345
|
+
<!-- Rules Optimizer Panel -->
|
|
346
|
+
<div class="bg-gradient-to-r from-purple-900/30 to-indigo-900/30 border border-purple-700/50 rounded-xl p-6 mb-6">
|
|
347
|
+
<div class="flex items-start justify-between mb-4">
|
|
348
|
+
<div>
|
|
349
|
+
<h3 class="text-lg font-semibold flex items-center gap-2">
|
|
350
|
+
<svg class="w-5 h-5 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
351
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
|
|
352
|
+
</svg>
|
|
353
|
+
Rules Optimizer
|
|
354
|
+
<span class="text-xs bg-purple-600/30 text-purple-400 px-2 py-0.5 rounded">LLM</span>
|
|
355
|
+
</h3>
|
|
356
|
+
<p class="text-sm text-gray-400 mt-1">Automatically analyze, detect duplicates, merge, and clean up Cursor rules</p>
|
|
357
|
+
</div>
|
|
358
|
+
<div id="optimizer-status" class="text-xs px-2 py-1 rounded-full bg-gray-800 text-gray-400">Ready</div>
|
|
359
|
+
</div>
|
|
360
|
+
|
|
361
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
|
362
|
+
<div>
|
|
363
|
+
<label class="block text-sm text-gray-400 mb-2">Rules Folder</label>
|
|
364
|
+
<div class="flex gap-2">
|
|
365
|
+
<input type="text" id="optimizer-folder" value="~/.cursor/rules"
|
|
366
|
+
placeholder="e.g., ~/.cursor/rules or /path/to/rules"
|
|
367
|
+
class="flex-1 bg-gray-800 border border-gray-700 rounded-lg px-4 py-2 text-sm">
|
|
368
|
+
<button onclick="selectRulesFolder()" type="button"
|
|
369
|
+
class="px-3 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors flex items-center gap-1 text-sm"
|
|
370
|
+
title="Browse for folder">
|
|
371
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
372
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
|
|
373
|
+
</svg>
|
|
374
|
+
</button>
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
<div>
|
|
378
|
+
<label class="block text-sm text-gray-400 mb-2">Mode</label>
|
|
379
|
+
<select id="optimizer-mode" class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2 text-sm">
|
|
380
|
+
<option value="dry-run">Dry Run (Preview Only)</option>
|
|
381
|
+
<option value="apply">Apply Changes</option>
|
|
382
|
+
</select>
|
|
383
|
+
</div>
|
|
384
|
+
<div class="flex items-end">
|
|
385
|
+
<button onclick="runRulesOptimizer()" id="optimizer-run-btn"
|
|
386
|
+
class="w-full px-4 py-2 bg-purple-600 hover:bg-purple-700 rounded-lg transition-colors flex items-center justify-center gap-2 text-sm font-medium">
|
|
387
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
388
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
|
389
|
+
</svg>
|
|
390
|
+
<span>Run Optimizer</span>
|
|
391
|
+
</button>
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
|
|
395
|
+
<!-- Optimizer Results -->
|
|
396
|
+
<div id="optimizer-results" class="hidden">
|
|
397
|
+
<div class="border-t border-purple-700/30 pt-4">
|
|
398
|
+
<div class="flex items-center justify-between mb-3">
|
|
399
|
+
<h4 class="text-sm font-medium">Results</h4>
|
|
400
|
+
<div id="optimizer-stats" class="flex gap-4 text-xs"></div>
|
|
401
|
+
</div>
|
|
402
|
+
|
|
403
|
+
<!-- Progress steps -->
|
|
404
|
+
<div id="optimizer-progress" class="flex items-center gap-2 mb-4 text-xs">
|
|
405
|
+
<span class="optimizer-step" data-step="analyze">📋 Analyze</span>
|
|
406
|
+
<span class="text-gray-600">→</span>
|
|
407
|
+
<span class="optimizer-step" data-step="duplicates">🔍 Detect Duplicates</span>
|
|
408
|
+
<span class="text-gray-600">→</span>
|
|
409
|
+
<span class="optimizer-step" data-step="merge">🔗 Merge</span>
|
|
410
|
+
<span class="text-gray-600">→</span>
|
|
411
|
+
<span class="optimizer-step" data-step="cleanup">🗑️ Cleanup</span>
|
|
412
|
+
</div>
|
|
413
|
+
|
|
414
|
+
<!-- Actions list -->
|
|
415
|
+
<div id="optimizer-actions" class="space-y-2 max-h-64 overflow-y-auto">
|
|
416
|
+
<!-- Actions will be populated here -->
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
|
|
422
|
+
<!-- Category Filter -->
|
|
423
|
+
<div class="mb-6">
|
|
424
|
+
<div class="flex flex-wrap gap-2" id="tool-category-filters">
|
|
425
|
+
<button onclick="filterToolCategory('')" class="tool-category-filter px-3 py-1 bg-primary-600 rounded-full text-sm transition-colors" data-category="">
|
|
426
|
+
All
|
|
427
|
+
</button>
|
|
428
|
+
</div>
|
|
429
|
+
</div>
|
|
430
|
+
|
|
431
|
+
<!-- Tools Grid -->
|
|
432
|
+
<div id="tools-grid" class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
433
|
+
<p class="text-gray-500 text-center py-8 col-span-2">Loading tools...</p>
|
|
434
|
+
</div>
|
|
435
|
+
|
|
436
|
+
<!-- Tool Execution Modal -->
|
|
437
|
+
<div id="tool-modal" class="fixed inset-0 bg-black/50 hidden z-50 flex items-center justify-center p-4">
|
|
438
|
+
<div class="bg-gray-900 rounded-xl border border-gray-800 max-w-2xl w-full max-h-[90vh] overflow-hidden flex flex-col">
|
|
439
|
+
<div class="p-6 border-b border-gray-800">
|
|
440
|
+
<div class="flex items-center justify-between">
|
|
441
|
+
<h3 id="tool-modal-title" class="text-xl font-semibold">Tool Name</h3>
|
|
442
|
+
<button onclick="closeToolModal()" class="text-gray-400 hover:text-white">
|
|
443
|
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
444
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
445
|
+
</svg>
|
|
446
|
+
</button>
|
|
447
|
+
</div>
|
|
448
|
+
<p id="tool-modal-description" class="text-sm text-gray-400 mt-2">Tool description</p>
|
|
449
|
+
</div>
|
|
450
|
+
|
|
451
|
+
<div class="p-6 overflow-y-auto flex-1">
|
|
452
|
+
<form id="tool-form" onsubmit="executeTool(event)">
|
|
453
|
+
<div id="tool-form-fields" class="space-y-4">
|
|
454
|
+
<!-- Dynamic form fields will be inserted here -->
|
|
455
|
+
</div>
|
|
456
|
+
|
|
457
|
+
<div class="flex gap-3 mt-6">
|
|
458
|
+
<button type="submit" id="tool-execute-btn" class="flex-1 px-4 py-2 bg-primary-600 hover:bg-primary-700 rounded-lg transition-colors flex items-center justify-center gap-2">
|
|
459
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
460
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/>
|
|
461
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
462
|
+
</svg>
|
|
463
|
+
<span>Execute</span>
|
|
464
|
+
</button>
|
|
465
|
+
<button type="button" onclick="closeToolModal()" class="px-4 py-2 bg-gray-800 hover:bg-gray-700 rounded-lg transition-colors">
|
|
466
|
+
Cancel
|
|
467
|
+
</button>
|
|
468
|
+
</div>
|
|
469
|
+
</form>
|
|
470
|
+
</div>
|
|
471
|
+
|
|
472
|
+
<!-- Result Section -->
|
|
473
|
+
<div id="tool-result-section" class="hidden border-t border-gray-800">
|
|
474
|
+
<div class="p-4 bg-gray-950">
|
|
475
|
+
<div class="flex items-center justify-between mb-3">
|
|
476
|
+
<span class="text-sm font-medium">Result</span>
|
|
477
|
+
<div class="flex items-center gap-2">
|
|
478
|
+
<span id="tool-result-status" class="text-xs px-2 py-1 rounded-full"></span>
|
|
479
|
+
<span id="tool-result-time" class="text-xs text-gray-500"></span>
|
|
480
|
+
</div>
|
|
481
|
+
</div>
|
|
482
|
+
<div id="tool-result-content" class="bg-gray-900 rounded-lg p-4 max-h-64 overflow-y-auto">
|
|
483
|
+
<pre class="text-sm text-gray-300 whitespace-pre-wrap font-mono"></pre>
|
|
484
|
+
</div>
|
|
485
|
+
</div>
|
|
486
|
+
</div>
|
|
487
|
+
</div>
|
|
488
|
+
</div>
|
|
489
|
+
|
|
490
|
+
<!-- Execution History -->
|
|
491
|
+
<div class="mt-8">
|
|
492
|
+
<h3 class="text-lg font-semibold mb-4">Recent Executions</h3>
|
|
493
|
+
<div id="tool-history" class="bg-gray-900 border border-gray-800 rounded-xl overflow-hidden">
|
|
494
|
+
<div class="divide-y divide-gray-800">
|
|
495
|
+
<p class="text-gray-500 text-center py-4 text-sm">No recent executions</p>
|
|
496
|
+
</div>
|
|
497
|
+
</div>
|
|
498
|
+
</div>
|
|
499
|
+
</div>
|
|
500
|
+
|
|
309
501
|
<!-- Settings Tab -->
|
|
310
502
|
<div id="tab-settings" class="tab-content hidden">
|
|
311
503
|
<h2 class="text-2xl font-bold mb-6">Settings</h2>
|
|
312
504
|
|
|
505
|
+
<!-- Connection Status Panel -->
|
|
506
|
+
<div class="bg-gray-900 border border-gray-800 rounded-xl p-6 mb-6">
|
|
507
|
+
<div class="flex items-center justify-between mb-4">
|
|
508
|
+
<h3 class="text-lg font-semibold">Connection Status</h3>
|
|
509
|
+
<button onclick="checkHealth()" class="text-xs bg-gray-800 hover:bg-gray-700 px-3 py-1 rounded-lg flex items-center gap-2">
|
|
510
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
511
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
|
512
|
+
</svg>
|
|
513
|
+
Check
|
|
514
|
+
</button>
|
|
515
|
+
</div>
|
|
516
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
517
|
+
<div id="vector-store-status" class="bg-gray-800 rounded-lg p-4">
|
|
518
|
+
<div class="flex items-center gap-3 mb-2">
|
|
519
|
+
<div id="vectorstore-status-dot" class="w-3 h-3 bg-gray-500 rounded-full"></div>
|
|
520
|
+
<span class="font-medium">Vector Store</span>
|
|
521
|
+
</div>
|
|
522
|
+
<p id="vectorstore-status-text" class="text-sm text-gray-400">Checking...</p>
|
|
523
|
+
<p id="vectorstore-status-count" class="text-xs text-gray-500 mt-1"></p>
|
|
524
|
+
</div>
|
|
525
|
+
<div id="embeddings-status" class="bg-gray-800 rounded-lg p-4">
|
|
526
|
+
<div class="flex items-center gap-3 mb-2">
|
|
527
|
+
<div id="embeddings-status-dot" class="w-3 h-3 bg-gray-500 rounded-full"></div>
|
|
528
|
+
<span class="font-medium">Embeddings</span>
|
|
529
|
+
</div>
|
|
530
|
+
<p id="embeddings-status-text" class="text-sm text-gray-400">Checking...</p>
|
|
531
|
+
</div>
|
|
532
|
+
</div>
|
|
533
|
+
</div>
|
|
534
|
+
|
|
313
535
|
<div class="space-y-6">
|
|
314
536
|
<!-- Vector Store Settings -->
|
|
315
537
|
<div class="bg-gray-900 border border-gray-800 rounded-xl p-6">
|
|
@@ -317,13 +539,33 @@
|
|
|
317
539
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
318
540
|
<div>
|
|
319
541
|
<label class="block text-sm text-gray-400 mb-2">Type</label>
|
|
320
|
-
<select id="config-vectorstore" class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2">
|
|
542
|
+
<select id="config-vectorstore" class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2" onchange="toggleVectorStoreConfig()">
|
|
543
|
+
<option value="redis-stack">Redis Stack (Docker/RediSearch)</option>
|
|
544
|
+
<option value="redis">Redis 8.x Native (Homebrew)</option>
|
|
321
545
|
<option value="memory">Memory (File-based)</option>
|
|
322
546
|
<option value="chroma">ChromaDB (Server)</option>
|
|
323
547
|
<option value="qdrant">Qdrant</option>
|
|
324
548
|
<option value="vectorize">Cloudflare Vectorize</option>
|
|
325
549
|
</select>
|
|
326
550
|
</div>
|
|
551
|
+
<div id="redis-stack-config" class="hidden">
|
|
552
|
+
<label class="block text-sm text-gray-400 mb-2">Redis Stack URL</label>
|
|
553
|
+
<input type="text" id="config-redis-stack-url" placeholder="redis://localhost:6379" class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2">
|
|
554
|
+
<p class="text-xs text-gray-500 mt-1">Requires Redis Stack with RediSearch module (Docker recommended)</p>
|
|
555
|
+
</div>
|
|
556
|
+
<div id="redis-config" class="hidden">
|
|
557
|
+
<label class="block text-sm text-gray-400 mb-2">Redis URL</label>
|
|
558
|
+
<input type="text" id="config-redis-url" placeholder="redis://localhost:6379" class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2">
|
|
559
|
+
<p class="text-xs text-gray-500 mt-1">Uses Redis 8.x native vector commands (VADD/VSIM)</p>
|
|
560
|
+
</div>
|
|
561
|
+
<div id="chroma-config" class="hidden">
|
|
562
|
+
<label class="block text-sm text-gray-400 mb-2">ChromaDB URL</label>
|
|
563
|
+
<input type="text" id="config-chroma-url" placeholder="http://localhost:8000" class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2">
|
|
564
|
+
</div>
|
|
565
|
+
<div id="qdrant-config" class="hidden">
|
|
566
|
+
<label class="block text-sm text-gray-400 mb-2">Qdrant URL</label>
|
|
567
|
+
<input type="text" id="config-qdrant-url" placeholder="http://localhost:6333" class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2">
|
|
568
|
+
</div>
|
|
327
569
|
</div>
|
|
328
570
|
</div>
|
|
329
571
|
|
|
@@ -372,28 +614,227 @@
|
|
|
372
614
|
</div>
|
|
373
615
|
</div>
|
|
374
616
|
|
|
617
|
+
<!-- Rules Analyzer Settings -->
|
|
618
|
+
<div class="bg-gray-900 border border-gray-800 rounded-xl p-6">
|
|
619
|
+
<h3 class="text-lg font-semibold mb-4 flex items-center gap-2">
|
|
620
|
+
<svg class="w-5 h-5 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
621
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
|
622
|
+
</svg>
|
|
623
|
+
Rules Analyzer
|
|
624
|
+
</h3>
|
|
625
|
+
<p class="text-sm text-gray-400 mb-4">Configure how the rules analyzer detects duplicates, outdated patterns, and conflicts in your Cursor rules.</p>
|
|
626
|
+
|
|
627
|
+
<!-- Analysis Settings -->
|
|
628
|
+
<div class="space-y-4 mb-6">
|
|
629
|
+
<h4 class="text-sm font-medium text-gray-300">Analysis Settings</h4>
|
|
630
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
631
|
+
<div>
|
|
632
|
+
<label class="block text-sm text-gray-400 mb-2">Duplicate Threshold</label>
|
|
633
|
+
<input type="number" id="rules-duplicate-threshold" min="0" max="1" step="0.05" value="0.7" class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2">
|
|
634
|
+
<p class="text-xs text-gray-500 mt-1">Similarity score (0-1) to flag as duplicate</p>
|
|
635
|
+
</div>
|
|
636
|
+
<div>
|
|
637
|
+
<label class="block text-sm text-gray-400 mb-2">Max Age (Days)</label>
|
|
638
|
+
<input type="number" id="rules-max-age-days" min="30" value="365" class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2">
|
|
639
|
+
<p class="text-xs text-gray-500 mt-1">Days before flagging as outdated</p>
|
|
640
|
+
</div>
|
|
641
|
+
<div>
|
|
642
|
+
<label class="block text-sm text-gray-400 mb-2">Old Year Threshold</label>
|
|
643
|
+
<input type="number" id="rules-old-year-threshold" min="1" value="2" class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2">
|
|
644
|
+
<p class="text-xs text-gray-500 mt-1">Years back to consider "old"</p>
|
|
645
|
+
</div>
|
|
646
|
+
</div>
|
|
647
|
+
<div class="flex flex-wrap gap-4">
|
|
648
|
+
<div class="flex items-center gap-2">
|
|
649
|
+
<input type="checkbox" id="rules-detect-conflicts" checked class="w-4 h-4 rounded bg-gray-800 border-gray-700 text-primary-600 focus:ring-primary-500">
|
|
650
|
+
<label for="rules-detect-conflicts" class="text-sm text-gray-300">Detect Conflicts</label>
|
|
651
|
+
</div>
|
|
652
|
+
<div class="flex items-center gap-2">
|
|
653
|
+
<input type="checkbox" id="rules-detect-outdated" checked class="w-4 h-4 rounded bg-gray-800 border-gray-700 text-primary-600 focus:ring-primary-500">
|
|
654
|
+
<label for="rules-detect-outdated" class="text-sm text-gray-300">Detect Outdated</label>
|
|
655
|
+
</div>
|
|
656
|
+
<div class="flex items-center gap-2">
|
|
657
|
+
<input type="checkbox" id="rules-use-llm" class="w-4 h-4 rounded bg-gray-800 border-gray-700 text-primary-600 focus:ring-primary-500" onchange="toggleLLMSettings()">
|
|
658
|
+
<label for="rules-use-llm" class="text-sm text-gray-300">Use LLM for Analysis</label>
|
|
659
|
+
</div>
|
|
660
|
+
</div>
|
|
661
|
+
</div>
|
|
662
|
+
|
|
663
|
+
<!-- LLM Provider Settings -->
|
|
664
|
+
<div id="llm-settings-section" class="mb-6 hidden">
|
|
665
|
+
<h4 class="text-sm font-medium text-gray-300 mb-3">LLM Provider Configuration</h4>
|
|
666
|
+
<div id="llm-status" class="mb-3 hidden">
|
|
667
|
+
<div class="flex items-center gap-2 text-sm">
|
|
668
|
+
<span id="llm-status-icon" class="w-2 h-2 rounded-full bg-gray-500"></span>
|
|
669
|
+
<span id="llm-status-text" class="text-gray-400">Not configured</span>
|
|
670
|
+
</div>
|
|
671
|
+
</div>
|
|
672
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
673
|
+
<div>
|
|
674
|
+
<label class="block text-sm text-gray-400 mb-2">Provider</label>
|
|
675
|
+
<select id="llm-provider" onchange="onProviderChange()" class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2">
|
|
676
|
+
<option value="">Select a provider...</option>
|
|
677
|
+
<option value="openai">OpenAI</option>
|
|
678
|
+
<option value="anthropic">Anthropic</option>
|
|
679
|
+
<option value="deepseek">DeepSeek</option>
|
|
680
|
+
<option value="groq">Groq</option>
|
|
681
|
+
<option value="ollama">Ollama (Local)</option>
|
|
682
|
+
<option value="openrouter">OpenRouter</option>
|
|
683
|
+
</select>
|
|
684
|
+
</div>
|
|
685
|
+
<div id="llm-api-key-container">
|
|
686
|
+
<label class="block text-sm text-gray-400 mb-2">API Key</label>
|
|
687
|
+
<div class="flex gap-2">
|
|
688
|
+
<input type="password" id="llm-api-key" placeholder="Enter API key..." class="flex-1 bg-gray-800 border border-gray-700 rounded-lg px-4 py-2">
|
|
689
|
+
<button onclick="testLLMConnection()" id="llm-test-btn" class="px-3 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg text-sm transition-colors">
|
|
690
|
+
Test
|
|
691
|
+
</button>
|
|
692
|
+
</div>
|
|
693
|
+
<p id="llm-key-hint" class="text-xs text-gray-500 mt-1"></p>
|
|
694
|
+
</div>
|
|
695
|
+
<div id="llm-base-url-container" class="hidden">
|
|
696
|
+
<label class="block text-sm text-gray-400 mb-2">Base URL (Optional)</label>
|
|
697
|
+
<input type="text" id="llm-base-url" placeholder="http://localhost:11434" class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2">
|
|
698
|
+
<p class="text-xs text-gray-500 mt-1">Override the default endpoint</p>
|
|
699
|
+
</div>
|
|
700
|
+
<div id="llm-model-container" class="hidden">
|
|
701
|
+
<label class="block text-sm text-gray-400 mb-2">Model</label>
|
|
702
|
+
<select id="llm-model" class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2">
|
|
703
|
+
<option value="">Test connection to load models...</option>
|
|
704
|
+
</select>
|
|
705
|
+
</div>
|
|
706
|
+
</div>
|
|
707
|
+
<div class="flex justify-end mt-4">
|
|
708
|
+
<button onclick="saveLLMConfig()" id="llm-save-btn" class="px-4 py-2 bg-green-600 hover:bg-green-700 rounded-lg transition-colors flex items-center gap-2 text-sm disabled:opacity-50 disabled:cursor-not-allowed" disabled>
|
|
709
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
710
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
|
711
|
+
</svg>
|
|
712
|
+
Save LLM Configuration
|
|
713
|
+
</button>
|
|
714
|
+
</div>
|
|
715
|
+
</div>
|
|
716
|
+
|
|
717
|
+
<!-- Version Check Patterns -->
|
|
718
|
+
<div class="mb-6">
|
|
719
|
+
<div class="flex items-center justify-between mb-3">
|
|
720
|
+
<h4 class="text-sm font-medium text-gray-300">Version Check Patterns</h4>
|
|
721
|
+
<button onclick="showAddVersionCheckModal()" class="text-xs bg-gray-800 hover:bg-gray-700 px-3 py-1 rounded-lg flex items-center gap-1">
|
|
722
|
+
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
723
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
|
724
|
+
</svg>
|
|
725
|
+
Add Pattern
|
|
726
|
+
</button>
|
|
727
|
+
</div>
|
|
728
|
+
<p class="text-xs text-gray-500 mb-3">Define patterns to detect outdated version references in your rules.</p>
|
|
729
|
+
<div id="version-checks-list" class="space-y-2">
|
|
730
|
+
<!-- Version checks will be populated here -->
|
|
731
|
+
</div>
|
|
732
|
+
<div id="version-checks-empty" class="text-sm text-gray-500 italic hidden">No version checks configured. Click "Add Pattern" to create one.</div>
|
|
733
|
+
</div>
|
|
734
|
+
|
|
735
|
+
<!-- Deprecation Patterns -->
|
|
736
|
+
<div class="mb-6">
|
|
737
|
+
<div class="flex items-center justify-between mb-3">
|
|
738
|
+
<h4 class="text-sm font-medium text-gray-300">Deprecation Patterns</h4>
|
|
739
|
+
<button onclick="showAddDeprecationModal()" class="text-xs bg-gray-800 hover:bg-gray-700 px-3 py-1 rounded-lg flex items-center gap-1">
|
|
740
|
+
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
741
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
|
742
|
+
</svg>
|
|
743
|
+
Add Pattern
|
|
744
|
+
</button>
|
|
745
|
+
</div>
|
|
746
|
+
<p class="text-xs text-gray-500 mb-3">Define patterns to detect deprecated code practices in your rules.</p>
|
|
747
|
+
<div id="deprecation-patterns-list" class="space-y-2">
|
|
748
|
+
<!-- Deprecation patterns will be populated here -->
|
|
749
|
+
</div>
|
|
750
|
+
<div id="deprecation-patterns-empty" class="text-sm text-gray-500 italic hidden">No deprecation patterns configured. Click "Add Pattern" to create one.</div>
|
|
751
|
+
</div>
|
|
752
|
+
|
|
753
|
+
<!-- Natural Language Rules (LLM-powered) -->
|
|
754
|
+
<div id="natural-language-rules-section" class="mb-6 hidden">
|
|
755
|
+
<div class="flex items-center justify-between mb-3">
|
|
756
|
+
<h4 class="text-sm font-medium text-gray-300 flex items-center gap-2">
|
|
757
|
+
<svg class="w-4 h-4 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
758
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"/>
|
|
759
|
+
</svg>
|
|
760
|
+
Natural Language Rules
|
|
761
|
+
<span class="text-xs bg-purple-600/30 text-purple-400 px-2 py-0.5 rounded">LLM</span>
|
|
762
|
+
</h4>
|
|
763
|
+
<button onclick="showAddNaturalRuleModal()" class="text-xs bg-purple-800 hover:bg-purple-700 px-3 py-1 rounded-lg flex items-center gap-1">
|
|
764
|
+
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
765
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
|
766
|
+
</svg>
|
|
767
|
+
Add Rule
|
|
768
|
+
</button>
|
|
769
|
+
</div>
|
|
770
|
+
<p class="text-xs text-gray-500 mb-3">
|
|
771
|
+
Define rules in plain English. The LLM will interpret these when analyzing your code rules.
|
|
772
|
+
<br>Examples: "Flag any mention of jQuery", "Warn if using class components in React"
|
|
773
|
+
</p>
|
|
774
|
+
<div id="natural-rules-list" class="space-y-2">
|
|
775
|
+
<!-- Natural language rules will be populated here -->
|
|
776
|
+
</div>
|
|
777
|
+
<div id="natural-rules-empty" class="text-sm text-gray-500 italic">No natural language rules configured. Click "Add Rule" to create one.</div>
|
|
778
|
+
</div>
|
|
779
|
+
|
|
780
|
+
<!-- Example Templates -->
|
|
781
|
+
<div class="border-t border-gray-800 pt-4">
|
|
782
|
+
<h4 class="text-sm font-medium text-gray-300 mb-3">Quick Add from Templates</h4>
|
|
783
|
+
<div class="flex flex-wrap gap-2" id="rules-templates">
|
|
784
|
+
<!-- Templates will be populated here -->
|
|
785
|
+
</div>
|
|
786
|
+
</div>
|
|
787
|
+
|
|
788
|
+
<!-- Save Rules Config Button -->
|
|
789
|
+
<div class="flex justify-end mt-4">
|
|
790
|
+
<button onclick="saveRulesConfig()" class="px-4 py-2 bg-amber-600 hover:bg-amber-700 rounded-lg transition-colors flex items-center gap-2 text-sm">
|
|
791
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
792
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
|
793
|
+
</svg>
|
|
794
|
+
Save Rules Config
|
|
795
|
+
</button>
|
|
796
|
+
</div>
|
|
797
|
+
</div>
|
|
798
|
+
|
|
375
799
|
<!-- Proxy Settings -->
|
|
376
800
|
<div class="bg-gray-900 border border-gray-800 rounded-xl p-6">
|
|
377
|
-
<h3 class="text-lg font-semibold mb-4">Proxy Configuration</h3>
|
|
801
|
+
<h3 class="text-lg font-semibold mb-4">Rotating Proxy Configuration</h3>
|
|
378
802
|
<div class="space-y-4">
|
|
379
803
|
<div class="flex items-center gap-3">
|
|
380
804
|
<input type="checkbox" id="config-proxy-enabled" class="w-4 h-4 rounded bg-gray-800 border-gray-700 text-primary-600 focus:ring-primary-500">
|
|
381
|
-
<label for="config-proxy-enabled" class="text-sm">Enable rotating proxy</label>
|
|
805
|
+
<label for="config-proxy-enabled" class="text-sm">Enable rotating proxy for web crawling</label>
|
|
382
806
|
</div>
|
|
383
807
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
384
808
|
<div>
|
|
385
|
-
<label class="block text-sm text-gray-400 mb-2">
|
|
809
|
+
<label class="block text-sm text-gray-400 mb-2">Provider</label>
|
|
386
810
|
<select id="config-proxy-driver" class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2">
|
|
387
811
|
<option value="none">None</option>
|
|
388
812
|
<option value="packetstream">PacketStream</option>
|
|
389
|
-
<option value="
|
|
813
|
+
<option value="decodo">Decodo (formerly SmartProxy)</option>
|
|
390
814
|
</select>
|
|
391
815
|
</div>
|
|
392
816
|
<div>
|
|
393
|
-
<label class="block text-sm text-gray-400 mb-2">Host</label>
|
|
394
|
-
<input type="text" id="config-proxy-host" placeholder="proxy.packetstream.io" class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2">
|
|
817
|
+
<label class="block text-sm text-gray-400 mb-2">Host <span class="text-gray-600">(optional - uses provider default)</span></label>
|
|
818
|
+
<input type="text" id="config-proxy-host" placeholder="e.g., proxy.packetstream.io" class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2">
|
|
819
|
+
</div>
|
|
820
|
+
<div>
|
|
821
|
+
<label class="block text-sm text-gray-400 mb-2">Port <span class="text-gray-600">(optional - uses provider default)</span></label>
|
|
822
|
+
<input type="number" id="config-proxy-port" placeholder="e.g., 31112" class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2">
|
|
823
|
+
</div>
|
|
824
|
+
<div>
|
|
825
|
+
<label class="block text-sm text-gray-400 mb-2">Username <span class="text-red-400">*</span></label>
|
|
826
|
+
<input type="text" id="config-proxy-username" placeholder="Your proxy username" class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2">
|
|
827
|
+
</div>
|
|
828
|
+
<div>
|
|
829
|
+
<label class="block text-sm text-gray-400 mb-2">Password <span class="text-red-400">*</span></label>
|
|
830
|
+
<input type="password" id="config-proxy-password" placeholder="Your proxy password" class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2">
|
|
395
831
|
</div>
|
|
396
832
|
</div>
|
|
833
|
+
<p class="text-xs text-gray-500 mt-2">
|
|
834
|
+
Rotating proxies help avoid IP blocks when crawling websites.
|
|
835
|
+
Get credentials from <a href="https://packetstream.io" target="_blank" class="text-primary-400 hover:underline">PacketStream</a> or
|
|
836
|
+
<a href="https://decodo.com" target="_blank" class="text-primary-400 hover:underline">Decodo</a>.
|
|
837
|
+
</p>
|
|
397
838
|
</div>
|
|
398
839
|
</div>
|
|
399
840
|
|
|
@@ -417,6 +858,138 @@
|
|
|
417
858
|
let allGatewayTools = [];
|
|
418
859
|
let currentBackendFilter = '';
|
|
419
860
|
|
|
861
|
+
// ==================== MODAL & TOAST SYSTEM ====================
|
|
862
|
+
let modalResolve = null;
|
|
863
|
+
|
|
864
|
+
function showToast(message, type = 'info', duration = 3000) {
|
|
865
|
+
const container = document.getElementById('toast-container');
|
|
866
|
+
const toast = document.createElement('div');
|
|
867
|
+
|
|
868
|
+
const colors = {
|
|
869
|
+
success: 'bg-green-600 border-green-500',
|
|
870
|
+
error: 'bg-red-600 border-red-500',
|
|
871
|
+
warning: 'bg-yellow-600 border-yellow-500',
|
|
872
|
+
info: 'bg-primary-600 border-primary-500',
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
const icons = {
|
|
876
|
+
success: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>',
|
|
877
|
+
error: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>',
|
|
878
|
+
warning: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>',
|
|
879
|
+
info: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>',
|
|
880
|
+
};
|
|
881
|
+
|
|
882
|
+
toast.className = `flex items-center gap-3 px-4 py-3 rounded-lg border ${colors[type]} text-white shadow-lg transform transition-all duration-300 translate-x-full`;
|
|
883
|
+
toast.innerHTML = `
|
|
884
|
+
<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">${icons[type]}</svg>
|
|
885
|
+
<span class="text-sm">${message}</span>
|
|
886
|
+
`;
|
|
887
|
+
|
|
888
|
+
container.appendChild(toast);
|
|
889
|
+
|
|
890
|
+
// Animate in
|
|
891
|
+
requestAnimationFrame(() => {
|
|
892
|
+
toast.classList.remove('translate-x-full');
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
// Remove after duration
|
|
896
|
+
setTimeout(() => {
|
|
897
|
+
toast.classList.add('translate-x-full', 'opacity-0');
|
|
898
|
+
setTimeout(() => toast.remove(), 300);
|
|
899
|
+
}, duration);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function showModal(title, content, buttons = []) {
|
|
903
|
+
return new Promise((resolve) => {
|
|
904
|
+
modalResolve = resolve;
|
|
905
|
+
|
|
906
|
+
const modal = document.getElementById('app-modal');
|
|
907
|
+
document.getElementById('modal-title').textContent = title;
|
|
908
|
+
document.getElementById('modal-content').innerHTML = content;
|
|
909
|
+
|
|
910
|
+
const footer = document.getElementById('modal-footer');
|
|
911
|
+
footer.innerHTML = buttons.map((btn, idx) => `
|
|
912
|
+
<button onclick="handleModalButton(${idx}, '${btn.value || ''}')"
|
|
913
|
+
class="px-4 py-2 rounded-lg transition-colors text-sm ${btn.primary ? 'bg-primary-600 hover:bg-primary-700' : 'bg-gray-700 hover:bg-gray-600'}">
|
|
914
|
+
${btn.label}
|
|
915
|
+
</button>
|
|
916
|
+
`).join('');
|
|
917
|
+
|
|
918
|
+
modal.classList.remove('hidden');
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function handleModalButton(index, value) {
|
|
923
|
+
const modal = document.getElementById('app-modal');
|
|
924
|
+
modal.classList.add('hidden');
|
|
925
|
+
|
|
926
|
+
if (modalResolve) {
|
|
927
|
+
// Get form values if any
|
|
928
|
+
const form = document.getElementById('modal-content').querySelector('form');
|
|
929
|
+
if (form) {
|
|
930
|
+
const formData = new FormData(form);
|
|
931
|
+
const data = Object.fromEntries(formData.entries());
|
|
932
|
+
modalResolve({ index, value, data });
|
|
933
|
+
} else {
|
|
934
|
+
modalResolve({ index, value });
|
|
935
|
+
}
|
|
936
|
+
modalResolve = null;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function closeModal() {
|
|
941
|
+
const modal = document.getElementById('app-modal');
|
|
942
|
+
modal.classList.add('hidden');
|
|
943
|
+
if (modalResolve) {
|
|
944
|
+
modalResolve({ index: -1, value: 'cancel' });
|
|
945
|
+
modalResolve = null;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// Close modal on escape or backdrop click
|
|
950
|
+
document.addEventListener('keydown', (e) => {
|
|
951
|
+
if (e.key === 'Escape') closeModal();
|
|
952
|
+
});
|
|
953
|
+
document.getElementById('app-modal').addEventListener('click', (e) => {
|
|
954
|
+
if (e.target.id === 'app-modal') closeModal();
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
async function showConfirmModal(title, message) {
|
|
958
|
+
const result = await showModal(title, `<p class="text-gray-300">${message}</p>`, [
|
|
959
|
+
{ label: 'Cancel', value: 'cancel' },
|
|
960
|
+
{ label: 'Confirm', value: 'confirm', primary: true },
|
|
961
|
+
]);
|
|
962
|
+
return result.value === 'confirm';
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
async function showInputModal(title, fields) {
|
|
966
|
+
const formHtml = `
|
|
967
|
+
<form class="space-y-4">
|
|
968
|
+
${fields.map(f => `
|
|
969
|
+
<div>
|
|
970
|
+
<label class="block text-sm text-gray-400 mb-2">${f.label}${f.required ? ' <span class="text-red-400">*</span>' : ''}</label>
|
|
971
|
+
${f.type === 'textarea'
|
|
972
|
+
? `<textarea name="${f.name}" placeholder="${f.placeholder || ''}" rows="3" class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2 text-white" ${f.required ? 'required' : ''}>${f.value || ''}</textarea>`
|
|
973
|
+
: `<input type="${f.type || 'text'}" name="${f.name}" placeholder="${f.placeholder || ''}" value="${f.value || ''}" class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2 text-white" ${f.required ? 'required' : ''}>`
|
|
974
|
+
}
|
|
975
|
+
${f.hint ? `<p class="text-xs text-gray-500 mt-1">${f.hint}</p>` : ''}
|
|
976
|
+
</div>
|
|
977
|
+
`).join('')}
|
|
978
|
+
</form>
|
|
979
|
+
`;
|
|
980
|
+
|
|
981
|
+
const result = await showModal(title, formHtml, [
|
|
982
|
+
{ label: 'Cancel', value: 'cancel' },
|
|
983
|
+
{ label: 'Add', value: 'submit', primary: true },
|
|
984
|
+
]);
|
|
985
|
+
|
|
986
|
+
if (result.value === 'submit' && result.data) {
|
|
987
|
+
return result.data;
|
|
988
|
+
}
|
|
989
|
+
return null;
|
|
990
|
+
}
|
|
991
|
+
// ==================== END MODAL & TOAST SYSTEM ====================
|
|
992
|
+
|
|
420
993
|
// Tab Navigation
|
|
421
994
|
function showTab(tabId) {
|
|
422
995
|
document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
|
|
@@ -435,6 +1008,8 @@
|
|
|
435
1008
|
// Load data for specific tabs
|
|
436
1009
|
if (tabId === 'gateway') loadGatewayTools();
|
|
437
1010
|
if (tabId === 'skills') loadSkills();
|
|
1011
|
+
if (tabId === 'tools') loadTools();
|
|
1012
|
+
if (tabId === 'settings') checkHealth();
|
|
438
1013
|
}
|
|
439
1014
|
|
|
440
1015
|
// Fetch Stats
|
|
@@ -678,6 +1253,61 @@
|
|
|
678
1253
|
}
|
|
679
1254
|
}
|
|
680
1255
|
|
|
1256
|
+
// Toggle vector store config fields visibility
|
|
1257
|
+
function toggleVectorStoreConfig() {
|
|
1258
|
+
const vectorStore = document.getElementById('config-vectorstore').value;
|
|
1259
|
+
document.getElementById('redis-stack-config').classList.toggle('hidden', vectorStore !== 'redis-stack');
|
|
1260
|
+
document.getElementById('redis-config').classList.toggle('hidden', vectorStore !== 'redis');
|
|
1261
|
+
document.getElementById('chroma-config').classList.toggle('hidden', vectorStore !== 'chroma');
|
|
1262
|
+
document.getElementById('qdrant-config').classList.toggle('hidden', vectorStore !== 'qdrant');
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// Check health status for vector store and embeddings
|
|
1266
|
+
async function checkHealth() {
|
|
1267
|
+
const vsDot = document.getElementById('vectorstore-status-dot');
|
|
1268
|
+
const vsText = document.getElementById('vectorstore-status-text');
|
|
1269
|
+
const vsCount = document.getElementById('vectorstore-status-count');
|
|
1270
|
+
const embDot = document.getElementById('embeddings-status-dot');
|
|
1271
|
+
const embText = document.getElementById('embeddings-status-text');
|
|
1272
|
+
|
|
1273
|
+
// Set to checking state
|
|
1274
|
+
vsDot.className = 'w-3 h-3 bg-yellow-500 rounded-full pulse-dot';
|
|
1275
|
+
vsText.textContent = 'Checking...';
|
|
1276
|
+
vsCount.textContent = '';
|
|
1277
|
+
embDot.className = 'w-3 h-3 bg-yellow-500 rounded-full pulse-dot';
|
|
1278
|
+
embText.textContent = 'Checking...';
|
|
1279
|
+
|
|
1280
|
+
try {
|
|
1281
|
+
const res = await fetch(`${API_BASE}/api/health`);
|
|
1282
|
+
const health = await res.json();
|
|
1283
|
+
|
|
1284
|
+
// Vector Store status
|
|
1285
|
+
if (health.vectorStore.status === 'connected') {
|
|
1286
|
+
vsDot.className = 'w-3 h-3 bg-green-500 rounded-full pulse-dot';
|
|
1287
|
+
vsText.textContent = `${health.vectorStore.type} - Connected`;
|
|
1288
|
+
vsCount.textContent = `${health.vectorStore.count.toLocaleString()} chunks stored`;
|
|
1289
|
+
} else {
|
|
1290
|
+
vsDot.className = 'w-3 h-3 bg-red-500 rounded-full';
|
|
1291
|
+
vsText.textContent = `${health.vectorStore.type} - Error`;
|
|
1292
|
+
vsCount.textContent = health.vectorStore.error || 'Connection failed';
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// Embeddings status
|
|
1296
|
+
if (health.embeddings.status === 'connected') {
|
|
1297
|
+
embDot.className = 'w-3 h-3 bg-green-500 rounded-full pulse-dot';
|
|
1298
|
+
embText.textContent = `${health.embeddings.type} - Ready`;
|
|
1299
|
+
} else {
|
|
1300
|
+
embDot.className = 'w-3 h-3 bg-red-500 rounded-full';
|
|
1301
|
+
embText.textContent = `${health.embeddings.type} - Error`;
|
|
1302
|
+
}
|
|
1303
|
+
} catch (e) {
|
|
1304
|
+
vsDot.className = 'w-3 h-3 bg-red-500 rounded-full';
|
|
1305
|
+
vsText.textContent = 'Failed to check';
|
|
1306
|
+
embDot.className = 'w-3 h-3 bg-red-500 rounded-full';
|
|
1307
|
+
embText.textContent = 'Failed to check';
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
681
1311
|
// Fetch Config
|
|
682
1312
|
async function fetchConfig() {
|
|
683
1313
|
try {
|
|
@@ -689,6 +1319,25 @@
|
|
|
689
1319
|
document.getElementById('config-vectorstore').value = config.vectorStore || 'memory';
|
|
690
1320
|
document.getElementById('config-embeddings').value = config.embeddings || 'xenova';
|
|
691
1321
|
|
|
1322
|
+
// Vector store specific configs
|
|
1323
|
+
// For redis-stack, use same redis URL field
|
|
1324
|
+
if (config.apiKeys?.redis?.url) {
|
|
1325
|
+
if (config.vectorStore === 'redis-stack') {
|
|
1326
|
+
document.getElementById('config-redis-stack-url').value = config.apiKeys.redis.url;
|
|
1327
|
+
} else {
|
|
1328
|
+
document.getElementById('config-redis-url').value = config.apiKeys.redis.url;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
if (config.vectorStoreConfig?.chromaUrl) {
|
|
1332
|
+
document.getElementById('config-chroma-url').value = config.vectorStoreConfig.chromaUrl;
|
|
1333
|
+
}
|
|
1334
|
+
if (config.apiKeys?.qdrant?.url) {
|
|
1335
|
+
document.getElementById('config-qdrant-url').value = config.apiKeys.qdrant.url;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// Show/hide vector store config fields
|
|
1339
|
+
toggleVectorStoreConfig();
|
|
1340
|
+
|
|
692
1341
|
if (config.mcpGateway) {
|
|
693
1342
|
document.getElementById('config-gateway-enabled').checked = config.mcpGateway.enabled;
|
|
694
1343
|
document.getElementById('config-gateway-url').value = config.mcpGateway.url || 'http://localhost:3010';
|
|
@@ -702,8 +1351,14 @@
|
|
|
702
1351
|
|
|
703
1352
|
if (config.proxy) {
|
|
704
1353
|
document.getElementById('config-proxy-enabled').checked = config.proxy.enabled;
|
|
705
|
-
|
|
1354
|
+
// Handle legacy 'smartproxy' -> 'decodo' rename
|
|
1355
|
+
let driver = config.proxy.driver || 'none';
|
|
1356
|
+
if (driver === 'smartproxy') driver = 'decodo';
|
|
1357
|
+
document.getElementById('config-proxy-driver').value = driver;
|
|
706
1358
|
document.getElementById('config-proxy-host').value = config.proxy.host || '';
|
|
1359
|
+
document.getElementById('config-proxy-port').value = config.proxy.port || '';
|
|
1360
|
+
document.getElementById('config-proxy-username').value = config.proxy.username || '';
|
|
1361
|
+
document.getElementById('config-proxy-password').value = config.proxy.password || '';
|
|
707
1362
|
}
|
|
708
1363
|
} catch (e) {
|
|
709
1364
|
console.error('Failed to fetch config:', e);
|
|
@@ -712,9 +1367,17 @@
|
|
|
712
1367
|
|
|
713
1368
|
// Save Settings
|
|
714
1369
|
async function saveSettings() {
|
|
1370
|
+
const vectorStore = document.getElementById('config-vectorstore').value;
|
|
1371
|
+
const redisStackUrl = document.getElementById('config-redis-stack-url').value;
|
|
1372
|
+
const redisUrl = document.getElementById('config-redis-url').value;
|
|
1373
|
+
const chromaUrl = document.getElementById('config-chroma-url').value;
|
|
1374
|
+
const qdrantUrl = document.getElementById('config-qdrant-url').value;
|
|
1375
|
+
|
|
715
1376
|
const updates = {
|
|
716
|
-
vectorStore:
|
|
1377
|
+
vectorStore: vectorStore,
|
|
717
1378
|
embeddings: document.getElementById('config-embeddings').value,
|
|
1379
|
+
apiKeys: {},
|
|
1380
|
+
vectorStoreConfig: {},
|
|
718
1381
|
mcpGateway: {
|
|
719
1382
|
enabled: document.getElementById('config-gateway-enabled').checked,
|
|
720
1383
|
url: document.getElementById('config-gateway-url').value || 'http://localhost:3010'
|
|
@@ -726,10 +1389,27 @@
|
|
|
726
1389
|
proxy: {
|
|
727
1390
|
enabled: document.getElementById('config-proxy-enabled').checked,
|
|
728
1391
|
driver: document.getElementById('config-proxy-driver').value,
|
|
729
|
-
host: document.getElementById('config-proxy-host').value || undefined
|
|
1392
|
+
host: document.getElementById('config-proxy-host').value || undefined,
|
|
1393
|
+
port: document.getElementById('config-proxy-port').value ? parseInt(document.getElementById('config-proxy-port').value) : undefined,
|
|
1394
|
+
username: document.getElementById('config-proxy-username').value || undefined,
|
|
1395
|
+
password: document.getElementById('config-proxy-password').value || undefined
|
|
730
1396
|
}
|
|
731
1397
|
};
|
|
732
1398
|
|
|
1399
|
+
// Add vector store specific config
|
|
1400
|
+
if (vectorStore === 'redis-stack' && redisStackUrl) {
|
|
1401
|
+
updates.apiKeys.redis = { url: redisStackUrl };
|
|
1402
|
+
}
|
|
1403
|
+
if (vectorStore === 'redis' && redisUrl) {
|
|
1404
|
+
updates.apiKeys.redis = { url: redisUrl };
|
|
1405
|
+
}
|
|
1406
|
+
if (vectorStore === 'chroma' && chromaUrl) {
|
|
1407
|
+
updates.vectorStoreConfig.chromaUrl = chromaUrl;
|
|
1408
|
+
}
|
|
1409
|
+
if (vectorStore === 'qdrant' && qdrantUrl) {
|
|
1410
|
+
updates.apiKeys.qdrant = { url: qdrantUrl };
|
|
1411
|
+
}
|
|
1412
|
+
|
|
733
1413
|
try {
|
|
734
1414
|
const res = await fetch(`${API_BASE}/api/config`, {
|
|
735
1415
|
method: 'POST',
|
|
@@ -738,13 +1418,13 @@
|
|
|
738
1418
|
});
|
|
739
1419
|
|
|
740
1420
|
if (res.ok) {
|
|
741
|
-
|
|
1421
|
+
showToast('Settings saved successfully!', 'success');
|
|
742
1422
|
refreshAll();
|
|
743
1423
|
} else {
|
|
744
|
-
|
|
1424
|
+
showToast('Failed to save settings', 'error');
|
|
745
1425
|
}
|
|
746
1426
|
} catch (e) {
|
|
747
|
-
|
|
1427
|
+
showToast('Failed to save settings: ' + e.message, 'error');
|
|
748
1428
|
}
|
|
749
1429
|
}
|
|
750
1430
|
|
|
@@ -800,6 +1480,1295 @@
|
|
|
800
1480
|
if (e.key === 'Enter') searchSkills();
|
|
801
1481
|
});
|
|
802
1482
|
|
|
1483
|
+
// ==================== TOOLS FUNCTIONALITY ====================
|
|
1484
|
+
let allTools = [];
|
|
1485
|
+
let currentToolCategoryFilter = '';
|
|
1486
|
+
let currentToolName = '';
|
|
1487
|
+
let executionHistory = [];
|
|
1488
|
+
|
|
1489
|
+
const CATEGORY_COLORS = {
|
|
1490
|
+
search: 'bg-blue-600',
|
|
1491
|
+
ingest: 'bg-green-600',
|
|
1492
|
+
maintenance: 'bg-orange-600',
|
|
1493
|
+
memory: 'bg-purple-600',
|
|
1494
|
+
chat: 'bg-pink-600',
|
|
1495
|
+
utility: 'bg-gray-600',
|
|
1496
|
+
};
|
|
1497
|
+
|
|
1498
|
+
const CATEGORY_LABELS = {
|
|
1499
|
+
search: 'Search',
|
|
1500
|
+
ingest: 'Ingest',
|
|
1501
|
+
maintenance: 'Maintenance',
|
|
1502
|
+
memory: 'Memory',
|
|
1503
|
+
chat: 'Chat',
|
|
1504
|
+
utility: 'Utility',
|
|
1505
|
+
};
|
|
1506
|
+
|
|
1507
|
+
async function loadTools() {
|
|
1508
|
+
const gridEl = document.getElementById('tools-grid');
|
|
1509
|
+
gridEl.innerHTML = '<p class="text-gray-500 text-center py-8 col-span-2">Loading tools...</p>';
|
|
1510
|
+
|
|
1511
|
+
try {
|
|
1512
|
+
const res = await fetch(`${API_BASE}/api/tools`);
|
|
1513
|
+
const data = await res.json();
|
|
1514
|
+
|
|
1515
|
+
allTools = data.tools || [];
|
|
1516
|
+
document.getElementById('tools-count').textContent = `${allTools.length} tools available`;
|
|
1517
|
+
|
|
1518
|
+
// Build category filters
|
|
1519
|
+
const filtersEl = document.getElementById('tool-category-filters');
|
|
1520
|
+
const categories = data.categories || [];
|
|
1521
|
+
filtersEl.innerHTML = `
|
|
1522
|
+
<button onclick="filterToolCategory('')" class="tool-category-filter px-3 py-1 bg-primary-600 rounded-full text-sm transition-colors" data-category="">
|
|
1523
|
+
All (${allTools.length})
|
|
1524
|
+
</button>
|
|
1525
|
+
${categories.filter(c => c.count > 0).map(c => `
|
|
1526
|
+
<button onclick="filterToolCategory('${c.name}')" class="tool-category-filter px-3 py-1 bg-gray-800 hover:bg-gray-700 rounded-full text-sm transition-colors" data-category="${c.name}">
|
|
1527
|
+
${CATEGORY_LABELS[c.name] || c.name} (${c.count})
|
|
1528
|
+
</button>
|
|
1529
|
+
`).join('')}
|
|
1530
|
+
`;
|
|
1531
|
+
|
|
1532
|
+
renderTools(allTools);
|
|
1533
|
+
loadToolHistory();
|
|
1534
|
+
} catch (e) {
|
|
1535
|
+
gridEl.innerHTML = `<p class="text-red-500 text-center py-8 col-span-2">Failed to load tools: ${e.message}</p>`;
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
function filterToolCategory(category) {
|
|
1540
|
+
currentToolCategoryFilter = category;
|
|
1541
|
+
|
|
1542
|
+
document.querySelectorAll('.tool-category-filter').forEach(btn => {
|
|
1543
|
+
if (btn.dataset.category === category) {
|
|
1544
|
+
btn.classList.remove('bg-gray-800');
|
|
1545
|
+
btn.classList.add('bg-primary-600');
|
|
1546
|
+
} else {
|
|
1547
|
+
btn.classList.remove('bg-primary-600');
|
|
1548
|
+
btn.classList.add('bg-gray-800');
|
|
1549
|
+
}
|
|
1550
|
+
});
|
|
1551
|
+
|
|
1552
|
+
const filtered = category ? allTools.filter(t => t.category === category) : allTools;
|
|
1553
|
+
renderTools(filtered);
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
function renderTools(tools) {
|
|
1557
|
+
const gridEl = document.getElementById('tools-grid');
|
|
1558
|
+
|
|
1559
|
+
if (tools.length === 0) {
|
|
1560
|
+
gridEl.innerHTML = '<p class="text-gray-500 text-center py-8 col-span-2">No tools found</p>';
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
gridEl.innerHTML = tools.map(tool => `
|
|
1565
|
+
<div class="bg-gray-900 border border-gray-800 rounded-xl p-5 hover:border-gray-700 transition-colors">
|
|
1566
|
+
<div class="flex items-start justify-between mb-3">
|
|
1567
|
+
<div class="flex items-center gap-2">
|
|
1568
|
+
<h4 class="font-semibold">${tool.displayName || tool.name}</h4>
|
|
1569
|
+
${tool.isLongRunning ? '<span class="text-xs px-2 py-0.5 bg-yellow-600/30 text-yellow-400 rounded-full">Long-running</span>' : ''}
|
|
1570
|
+
</div>
|
|
1571
|
+
<span class="text-xs px-2 py-1 ${CATEGORY_COLORS[tool.category] || 'bg-gray-700'} rounded-full">
|
|
1572
|
+
${CATEGORY_LABELS[tool.category] || tool.category}
|
|
1573
|
+
</span>
|
|
1574
|
+
</div>
|
|
1575
|
+
<p class="text-sm text-gray-400 mb-4">${tool.description}</p>
|
|
1576
|
+
<div class="flex items-center justify-between">
|
|
1577
|
+
<span class="text-xs text-gray-500">${tool.parameters?.length || 0} parameter(s)</span>
|
|
1578
|
+
<button onclick="openToolModal('${tool.name}')" class="px-3 py-1.5 bg-primary-600 hover:bg-primary-700 rounded-lg text-sm transition-colors flex items-center gap-1.5">
|
|
1579
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
1580
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/>
|
|
1581
|
+
</svg>
|
|
1582
|
+
Run
|
|
1583
|
+
</button>
|
|
1584
|
+
</div>
|
|
1585
|
+
</div>
|
|
1586
|
+
`).join('');
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
function openToolModal(toolName) {
|
|
1590
|
+
currentToolName = toolName;
|
|
1591
|
+
const tool = allTools.find(t => t.name === toolName);
|
|
1592
|
+
if (!tool) return;
|
|
1593
|
+
|
|
1594
|
+
document.getElementById('tool-modal-title').textContent = tool.displayName || tool.name;
|
|
1595
|
+
document.getElementById('tool-modal-description').textContent = tool.description;
|
|
1596
|
+
|
|
1597
|
+
// Generate form fields
|
|
1598
|
+
const fieldsEl = document.getElementById('tool-form-fields');
|
|
1599
|
+
const params = tool.parameters || [];
|
|
1600
|
+
|
|
1601
|
+
if (params.length === 0) {
|
|
1602
|
+
fieldsEl.innerHTML = '<p class="text-gray-400 text-sm">This tool has no parameters.</p>';
|
|
1603
|
+
} else {
|
|
1604
|
+
fieldsEl.innerHTML = params.map(p => generateFormField(p)).join('');
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
// Reset result section
|
|
1608
|
+
document.getElementById('tool-result-section').classList.add('hidden');
|
|
1609
|
+
document.getElementById('tool-execute-btn').disabled = false;
|
|
1610
|
+
document.getElementById('tool-execute-btn').innerHTML = `
|
|
1611
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
1612
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/>
|
|
1613
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
1614
|
+
</svg>
|
|
1615
|
+
<span>Execute</span>
|
|
1616
|
+
`;
|
|
1617
|
+
|
|
1618
|
+
document.getElementById('tool-modal').classList.remove('hidden');
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
function generateFormField(param) {
|
|
1622
|
+
const id = `param-${param.name}`;
|
|
1623
|
+
const required = param.required ? '<span class="text-red-400">*</span>' : '';
|
|
1624
|
+
const defaultVal = param.default !== undefined ? param.default : '';
|
|
1625
|
+
|
|
1626
|
+
let input = '';
|
|
1627
|
+
|
|
1628
|
+
switch (param.type) {
|
|
1629
|
+
case 'boolean':
|
|
1630
|
+
input = `
|
|
1631
|
+
<div class="flex items-center gap-3">
|
|
1632
|
+
<input type="checkbox" id="${id}" name="${param.name}"
|
|
1633
|
+
${defaultVal === true ? 'checked' : ''}
|
|
1634
|
+
class="w-4 h-4 rounded bg-gray-800 border-gray-700 text-primary-600 focus:ring-primary-500">
|
|
1635
|
+
<label for="${id}" class="text-sm text-gray-300">${param.description}</label>
|
|
1636
|
+
</div>
|
|
1637
|
+
`;
|
|
1638
|
+
break;
|
|
1639
|
+
case 'number':
|
|
1640
|
+
input = `
|
|
1641
|
+
<input type="number" id="${id}" name="${param.name}"
|
|
1642
|
+
value="${defaultVal}"
|
|
1643
|
+
placeholder="${param.description}"
|
|
1644
|
+
class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2 focus:outline-none focus:border-primary-500 focus:ring-1 focus:ring-primary-500"
|
|
1645
|
+
${param.required ? 'required' : ''}>
|
|
1646
|
+
`;
|
|
1647
|
+
break;
|
|
1648
|
+
case 'array':
|
|
1649
|
+
input = `
|
|
1650
|
+
<input type="text" id="${id}" name="${param.name}"
|
|
1651
|
+
value="${Array.isArray(defaultVal) ? defaultVal.join(', ') : ''}"
|
|
1652
|
+
placeholder="${param.description} (comma-separated)"
|
|
1653
|
+
class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2 focus:outline-none focus:border-primary-500 focus:ring-1 focus:ring-primary-500">
|
|
1654
|
+
<p class="text-xs text-gray-500 mt-1">Enter comma-separated values</p>
|
|
1655
|
+
`;
|
|
1656
|
+
break;
|
|
1657
|
+
default:
|
|
1658
|
+
if (param.enum && param.enum.length > 0) {
|
|
1659
|
+
input = `
|
|
1660
|
+
<select id="${id}" name="${param.name}"
|
|
1661
|
+
class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2 focus:outline-none focus:border-primary-500 focus:ring-1 focus:ring-primary-500"
|
|
1662
|
+
${param.required ? 'required' : ''}>
|
|
1663
|
+
${!param.required ? '<option value="">Select...</option>' : ''}
|
|
1664
|
+
${param.enum.map(v => `<option value="${v}" ${defaultVal === v ? 'selected' : ''}>${v}</option>`).join('')}
|
|
1665
|
+
</select>
|
|
1666
|
+
`;
|
|
1667
|
+
} else {
|
|
1668
|
+
input = `
|
|
1669
|
+
<input type="text" id="${id}" name="${param.name}"
|
|
1670
|
+
value="${defaultVal}"
|
|
1671
|
+
placeholder="${param.description}"
|
|
1672
|
+
class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2 focus:outline-none focus:border-primary-500 focus:ring-1 focus:ring-primary-500"
|
|
1673
|
+
${param.required ? 'required' : ''}>
|
|
1674
|
+
`;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
if (param.type === 'boolean') {
|
|
1679
|
+
return `<div class="mb-4">${input}</div>`;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
return `
|
|
1683
|
+
<div>
|
|
1684
|
+
<label for="${id}" class="block text-sm font-medium text-gray-300 mb-2">
|
|
1685
|
+
${param.name} ${required}
|
|
1686
|
+
</label>
|
|
1687
|
+
${input}
|
|
1688
|
+
${param.type !== 'array' ? `<p class="text-xs text-gray-500 mt-1">${param.description}</p>` : ''}
|
|
1689
|
+
</div>
|
|
1690
|
+
`;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
function closeToolModal() {
|
|
1694
|
+
document.getElementById('tool-modal').classList.add('hidden');
|
|
1695
|
+
currentToolName = '';
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
async function executeTool(event) {
|
|
1699
|
+
event.preventDefault();
|
|
1700
|
+
|
|
1701
|
+
const tool = allTools.find(t => t.name === currentToolName);
|
|
1702
|
+
if (!tool) return;
|
|
1703
|
+
|
|
1704
|
+
const executeBtn = document.getElementById('tool-execute-btn');
|
|
1705
|
+
executeBtn.disabled = true;
|
|
1706
|
+
executeBtn.innerHTML = `
|
|
1707
|
+
<svg class="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
1708
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
1709
|
+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
1710
|
+
</svg>
|
|
1711
|
+
<span>Executing...</span>
|
|
1712
|
+
`;
|
|
1713
|
+
|
|
1714
|
+
// Collect form data
|
|
1715
|
+
const formData = new FormData(document.getElementById('tool-form'));
|
|
1716
|
+
const params = {};
|
|
1717
|
+
|
|
1718
|
+
for (const param of tool.parameters || []) {
|
|
1719
|
+
const value = formData.get(param.name);
|
|
1720
|
+
|
|
1721
|
+
if (param.type === 'boolean') {
|
|
1722
|
+
params[param.name] = formData.has(param.name);
|
|
1723
|
+
} else if (param.type === 'number' && value !== '') {
|
|
1724
|
+
params[param.name] = parseFloat(value);
|
|
1725
|
+
} else if (param.type === 'array' && value) {
|
|
1726
|
+
params[param.name] = value.split(',').map(v => v.trim()).filter(v => v);
|
|
1727
|
+
} else if (value !== '') {
|
|
1728
|
+
params[param.name] = value;
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
try {
|
|
1733
|
+
const startTime = Date.now();
|
|
1734
|
+
const res = await fetch(`${API_BASE}/api/tools/${currentToolName}/execute`, {
|
|
1735
|
+
method: 'POST',
|
|
1736
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1737
|
+
body: JSON.stringify(params),
|
|
1738
|
+
});
|
|
1739
|
+
|
|
1740
|
+
const result = await res.json();
|
|
1741
|
+
const elapsed = Date.now() - startTime;
|
|
1742
|
+
|
|
1743
|
+
// Handle async jobs
|
|
1744
|
+
if (result.async && result.jobId) {
|
|
1745
|
+
showToolResult({
|
|
1746
|
+
success: true,
|
|
1747
|
+
message: `Job started: ${result.jobId}`,
|
|
1748
|
+
data: result.message,
|
|
1749
|
+
}, elapsed);
|
|
1750
|
+
pollJobStatus(currentToolName, result.jobId);
|
|
1751
|
+
} else {
|
|
1752
|
+
showToolResult(result, elapsed);
|
|
1753
|
+
addToHistory(currentToolName, params, result, elapsed);
|
|
1754
|
+
}
|
|
1755
|
+
} catch (e) {
|
|
1756
|
+
showToolResult({ success: false, error: e.message }, 0);
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
executeBtn.disabled = false;
|
|
1760
|
+
executeBtn.innerHTML = `
|
|
1761
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
1762
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/>
|
|
1763
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
1764
|
+
</svg>
|
|
1765
|
+
<span>Execute</span>
|
|
1766
|
+
`;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
async function pollJobStatus(toolName, jobId) {
|
|
1770
|
+
const maxAttempts = 60;
|
|
1771
|
+
let attempts = 0;
|
|
1772
|
+
|
|
1773
|
+
const poll = async () => {
|
|
1774
|
+
if (attempts >= maxAttempts) {
|
|
1775
|
+
showToolResult({ success: false, error: 'Job timed out' }, 0);
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
try {
|
|
1780
|
+
const res = await fetch(`${API_BASE}/api/tools/${toolName}/status/${jobId}`);
|
|
1781
|
+
const job = await res.json();
|
|
1782
|
+
|
|
1783
|
+
if (job.status === 'completed' || job.status === 'failed') {
|
|
1784
|
+
const elapsed = job.completedAt ? new Date(job.completedAt) - new Date(job.startedAt) : 0;
|
|
1785
|
+
showToolResult(job.result, elapsed);
|
|
1786
|
+
addToHistory(toolName, {}, job.result, elapsed);
|
|
1787
|
+
loadToolHistory();
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
// Update progress if available
|
|
1792
|
+
if (job.progress !== undefined) {
|
|
1793
|
+
document.getElementById('tool-result-content').querySelector('pre').textContent =
|
|
1794
|
+
`Progress: ${job.progress}%\n${job.progressMessage || 'Running...'}`;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
attempts++;
|
|
1798
|
+
setTimeout(poll, 1000);
|
|
1799
|
+
} catch (e) {
|
|
1800
|
+
attempts++;
|
|
1801
|
+
setTimeout(poll, 2000);
|
|
1802
|
+
}
|
|
1803
|
+
};
|
|
1804
|
+
|
|
1805
|
+
// Show initial running state
|
|
1806
|
+
const resultSection = document.getElementById('tool-result-section');
|
|
1807
|
+
resultSection.classList.remove('hidden');
|
|
1808
|
+
document.getElementById('tool-result-status').textContent = 'Running';
|
|
1809
|
+
document.getElementById('tool-result-status').className = 'text-xs px-2 py-1 rounded-full bg-yellow-600/30 text-yellow-400';
|
|
1810
|
+
document.getElementById('tool-result-time').textContent = '';
|
|
1811
|
+
document.getElementById('tool-result-content').querySelector('pre').textContent = 'Job started, waiting for completion...';
|
|
1812
|
+
|
|
1813
|
+
poll();
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
function showToolResult(result, elapsed) {
|
|
1817
|
+
const resultSection = document.getElementById('tool-result-section');
|
|
1818
|
+
resultSection.classList.remove('hidden');
|
|
1819
|
+
|
|
1820
|
+
const statusEl = document.getElementById('tool-result-status');
|
|
1821
|
+
const timeEl = document.getElementById('tool-result-time');
|
|
1822
|
+
const contentEl = document.getElementById('tool-result-content').querySelector('pre');
|
|
1823
|
+
|
|
1824
|
+
if (result.success) {
|
|
1825
|
+
statusEl.textContent = 'Success';
|
|
1826
|
+
statusEl.className = 'text-xs px-2 py-1 rounded-full bg-green-600/30 text-green-400';
|
|
1827
|
+
} else {
|
|
1828
|
+
statusEl.textContent = 'Error';
|
|
1829
|
+
statusEl.className = 'text-xs px-2 py-1 rounded-full bg-red-600/30 text-red-400';
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
timeEl.textContent = elapsed > 0 ? `${(elapsed / 1000).toFixed(2)}s` : '';
|
|
1833
|
+
|
|
1834
|
+
if (result.error) {
|
|
1835
|
+
contentEl.textContent = result.error;
|
|
1836
|
+
} else if (result.data) {
|
|
1837
|
+
contentEl.textContent = typeof result.data === 'string' ? result.data : JSON.stringify(result.data, null, 2);
|
|
1838
|
+
} else if (result.message) {
|
|
1839
|
+
contentEl.textContent = result.message;
|
|
1840
|
+
} else {
|
|
1841
|
+
contentEl.textContent = JSON.stringify(result, null, 2);
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
function addToHistory(toolName, params, result, elapsed) {
|
|
1846
|
+
executionHistory.unshift({
|
|
1847
|
+
toolName,
|
|
1848
|
+
params,
|
|
1849
|
+
result,
|
|
1850
|
+
elapsed,
|
|
1851
|
+
timestamp: new Date().toISOString(),
|
|
1852
|
+
});
|
|
1853
|
+
|
|
1854
|
+
// Keep only last 20 executions
|
|
1855
|
+
if (executionHistory.length > 20) {
|
|
1856
|
+
executionHistory = executionHistory.slice(0, 20);
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
renderToolHistory();
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
async function loadToolHistory() {
|
|
1863
|
+
try {
|
|
1864
|
+
const res = await fetch(`${API_BASE}/api/tools/jobs`);
|
|
1865
|
+
const data = await res.json();
|
|
1866
|
+
|
|
1867
|
+
if (data.jobs && data.jobs.length > 0) {
|
|
1868
|
+
renderToolHistoryFromJobs(data.jobs);
|
|
1869
|
+
} else {
|
|
1870
|
+
renderToolHistory();
|
|
1871
|
+
}
|
|
1872
|
+
} catch (e) {
|
|
1873
|
+
renderToolHistory();
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
function renderToolHistoryFromJobs(jobs) {
|
|
1878
|
+
const historyEl = document.getElementById('tool-history');
|
|
1879
|
+
|
|
1880
|
+
if (jobs.length === 0 && executionHistory.length === 0) {
|
|
1881
|
+
historyEl.innerHTML = `
|
|
1882
|
+
<div class="divide-y divide-gray-800">
|
|
1883
|
+
<p class="text-gray-500 text-center py-4 text-sm">No recent executions</p>
|
|
1884
|
+
</div>
|
|
1885
|
+
`;
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
const allItems = [
|
|
1890
|
+
...jobs.map(j => ({
|
|
1891
|
+
toolName: j.toolName,
|
|
1892
|
+
success: j.success,
|
|
1893
|
+
timestamp: j.startedAt,
|
|
1894
|
+
status: j.status,
|
|
1895
|
+
})),
|
|
1896
|
+
...executionHistory.map(h => ({
|
|
1897
|
+
toolName: h.toolName,
|
|
1898
|
+
success: h.result?.success,
|
|
1899
|
+
timestamp: h.timestamp,
|
|
1900
|
+
status: 'completed',
|
|
1901
|
+
})),
|
|
1902
|
+
].sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)).slice(0, 10);
|
|
1903
|
+
|
|
1904
|
+
historyEl.innerHTML = `
|
|
1905
|
+
<div class="divide-y divide-gray-800">
|
|
1906
|
+
${allItems.map(item => `
|
|
1907
|
+
<div class="flex items-center justify-between p-3">
|
|
1908
|
+
<div class="flex items-center gap-3">
|
|
1909
|
+
<div class="w-2 h-2 rounded-full ${item.success === true ? 'bg-green-500' : item.success === false ? 'bg-red-500' : 'bg-yellow-500'}"></div>
|
|
1910
|
+
<span class="font-medium">${item.toolName}</span>
|
|
1911
|
+
</div>
|
|
1912
|
+
<div class="flex items-center gap-3">
|
|
1913
|
+
<span class="text-xs text-gray-500">${new Date(item.timestamp).toLocaleTimeString()}</span>
|
|
1914
|
+
<button onclick="openToolModal('${item.toolName}')" class="text-xs px-2 py-1 bg-gray-800 hover:bg-gray-700 rounded transition-colors">
|
|
1915
|
+
Re-run
|
|
1916
|
+
</button>
|
|
1917
|
+
</div>
|
|
1918
|
+
</div>
|
|
1919
|
+
`).join('')}
|
|
1920
|
+
</div>
|
|
1921
|
+
`;
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
function renderToolHistory() {
|
|
1925
|
+
if (executionHistory.length === 0) {
|
|
1926
|
+
document.getElementById('tool-history').innerHTML = `
|
|
1927
|
+
<div class="divide-y divide-gray-800">
|
|
1928
|
+
<p class="text-gray-500 text-center py-4 text-sm">No recent executions</p>
|
|
1929
|
+
</div>
|
|
1930
|
+
`;
|
|
1931
|
+
return;
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
renderToolHistoryFromJobs([]);
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
// Close modal on escape key
|
|
1938
|
+
document.addEventListener('keydown', (e) => {
|
|
1939
|
+
if (e.key === 'Escape') closeToolModal();
|
|
1940
|
+
});
|
|
1941
|
+
|
|
1942
|
+
// Close modal on backdrop click
|
|
1943
|
+
document.getElementById('tool-modal').addEventListener('click', (e) => {
|
|
1944
|
+
if (e.target.id === 'tool-modal') closeToolModal();
|
|
1945
|
+
});
|
|
1946
|
+
// ==================== END TOOLS FUNCTIONALITY ====================
|
|
1947
|
+
|
|
1948
|
+
// ==================== RULES ANALYZER CONFIGURATION ====================
|
|
1949
|
+
let rulesConfig = null;
|
|
1950
|
+
let rulesExamples = null;
|
|
1951
|
+
let llmProviders = null;
|
|
1952
|
+
let llmModels = [];
|
|
1953
|
+
let llmConfigured = false;
|
|
1954
|
+
|
|
1955
|
+
const PROVIDER_KEY_HINTS = {
|
|
1956
|
+
openai: 'Starts with sk-...',
|
|
1957
|
+
anthropic: 'Starts with sk-ant-...',
|
|
1958
|
+
deepseek: 'Starts with sk-...',
|
|
1959
|
+
groq: 'Starts with gsk_...',
|
|
1960
|
+
openrouter: 'Starts with sk-or-...',
|
|
1961
|
+
ollama: 'No API key required',
|
|
1962
|
+
};
|
|
1963
|
+
|
|
1964
|
+
async function loadRulesConfig() {
|
|
1965
|
+
try {
|
|
1966
|
+
const res = await fetch(`${API_BASE}/api/rules/config`);
|
|
1967
|
+
const data = await res.json();
|
|
1968
|
+
rulesConfig = data.config;
|
|
1969
|
+
rulesExamples = data.examples;
|
|
1970
|
+
|
|
1971
|
+
// Populate form fields
|
|
1972
|
+
document.getElementById('rules-duplicate-threshold').value = rulesConfig.analysis.duplicateThreshold;
|
|
1973
|
+
document.getElementById('rules-max-age-days').value = rulesConfig.analysis.maxAgeDays;
|
|
1974
|
+
document.getElementById('rules-old-year-threshold').value = rulesConfig.analysis.oldYearThreshold;
|
|
1975
|
+
document.getElementById('rules-detect-conflicts').checked = rulesConfig.analysis.detectConflicts;
|
|
1976
|
+
document.getElementById('rules-detect-outdated').checked = rulesConfig.analysis.detectOutdated;
|
|
1977
|
+
document.getElementById('rules-use-llm').checked = rulesConfig.analysis.useLLM;
|
|
1978
|
+
|
|
1979
|
+
renderVersionChecks();
|
|
1980
|
+
renderDeprecationPatterns();
|
|
1981
|
+
renderNaturalRules();
|
|
1982
|
+
renderTemplates();
|
|
1983
|
+
|
|
1984
|
+
// Load LLM config
|
|
1985
|
+
await loadLLMConfig();
|
|
1986
|
+
} catch (e) {
|
|
1987
|
+
console.error('Failed to load rules config:', e);
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
async function loadLLMConfig() {
|
|
1992
|
+
try {
|
|
1993
|
+
const res = await fetch(`${API_BASE}/api/rules/llm/config`);
|
|
1994
|
+
const llmConfig = await res.json();
|
|
1995
|
+
|
|
1996
|
+
if (llmConfig.provider) {
|
|
1997
|
+
document.getElementById('llm-provider').value = llmConfig.provider;
|
|
1998
|
+
onProviderChange();
|
|
1999
|
+
|
|
2000
|
+
if (llmConfig.hasApiKey) {
|
|
2001
|
+
document.getElementById('llm-api-key').placeholder = '***configured*** (enter new key to change)';
|
|
2002
|
+
llmConfigured = true;
|
|
2003
|
+
updateLLMStatus(true, `${llmConfig.provider} configured${llmConfig.model ? ` (${llmConfig.model})` : ''}`);
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
if (llmConfig.baseUrl) {
|
|
2007
|
+
document.getElementById('llm-base-url').value = llmConfig.baseUrl;
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
if (llmConfig.model) {
|
|
2011
|
+
// Add the saved model as an option
|
|
2012
|
+
const modelSelect = document.getElementById('llm-model');
|
|
2013
|
+
modelSelect.innerHTML = `<option value="${llmConfig.model}">${llmConfig.model}</option>`;
|
|
2014
|
+
modelSelect.value = llmConfig.model;
|
|
2015
|
+
document.getElementById('llm-model-container').classList.remove('hidden');
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// Show/hide LLM section based on checkbox
|
|
2020
|
+
toggleLLMSettings();
|
|
2021
|
+
} catch (e) {
|
|
2022
|
+
console.error('Failed to load LLM config:', e);
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
function toggleLLMSettings() {
|
|
2027
|
+
const checkbox = document.getElementById('rules-use-llm');
|
|
2028
|
+
const llmSection = document.getElementById('llm-settings-section');
|
|
2029
|
+
const naturalRulesSection = document.getElementById('natural-language-rules-section');
|
|
2030
|
+
|
|
2031
|
+
if (checkbox.checked) {
|
|
2032
|
+
llmSection.classList.remove('hidden');
|
|
2033
|
+
naturalRulesSection.classList.remove('hidden');
|
|
2034
|
+
if (!llmConfigured) {
|
|
2035
|
+
updateLLMStatus(false, 'Please configure an LLM provider below');
|
|
2036
|
+
}
|
|
2037
|
+
} else {
|
|
2038
|
+
llmSection.classList.add('hidden');
|
|
2039
|
+
naturalRulesSection.classList.add('hidden');
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
function onProviderChange() {
|
|
2044
|
+
const provider = document.getElementById('llm-provider').value;
|
|
2045
|
+
const apiKeyContainer = document.getElementById('llm-api-key-container');
|
|
2046
|
+
const baseUrlContainer = document.getElementById('llm-base-url-container');
|
|
2047
|
+
const modelContainer = document.getElementById('llm-model-container');
|
|
2048
|
+
const keyHint = document.getElementById('llm-key-hint');
|
|
2049
|
+
|
|
2050
|
+
// Reset models
|
|
2051
|
+
document.getElementById('llm-model').innerHTML = '<option value="">Test connection to load models...</option>';
|
|
2052
|
+
modelContainer.classList.add('hidden');
|
|
2053
|
+
document.getElementById('llm-save-btn').disabled = true;
|
|
2054
|
+
|
|
2055
|
+
if (!provider) {
|
|
2056
|
+
apiKeyContainer.classList.add('hidden');
|
|
2057
|
+
baseUrlContainer.classList.add('hidden');
|
|
2058
|
+
return;
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
// Show/hide API key based on provider
|
|
2062
|
+
if (provider === 'ollama') {
|
|
2063
|
+
apiKeyContainer.classList.add('hidden');
|
|
2064
|
+
baseUrlContainer.classList.remove('hidden');
|
|
2065
|
+
document.getElementById('llm-base-url').placeholder = 'http://localhost:11434';
|
|
2066
|
+
} else {
|
|
2067
|
+
apiKeyContainer.classList.remove('hidden');
|
|
2068
|
+
baseUrlContainer.classList.add('hidden');
|
|
2069
|
+
keyHint.textContent = PROVIDER_KEY_HINTS[provider] || '';
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
function updateLLMStatus(success, message) {
|
|
2074
|
+
const statusDiv = document.getElementById('llm-status');
|
|
2075
|
+
const icon = document.getElementById('llm-status-icon');
|
|
2076
|
+
const text = document.getElementById('llm-status-text');
|
|
2077
|
+
|
|
2078
|
+
statusDiv.classList.remove('hidden');
|
|
2079
|
+
icon.className = `w-2 h-2 rounded-full ${success ? 'bg-green-500' : 'bg-yellow-500'}`;
|
|
2080
|
+
text.textContent = message;
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
async function testLLMConnection() {
|
|
2084
|
+
const provider = document.getElementById('llm-provider').value;
|
|
2085
|
+
const apiKey = document.getElementById('llm-api-key').value;
|
|
2086
|
+
const baseUrl = document.getElementById('llm-base-url').value;
|
|
2087
|
+
|
|
2088
|
+
if (!provider) {
|
|
2089
|
+
showToast('Please select a provider first', 'warning');
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
if (provider !== 'ollama' && !apiKey) {
|
|
2094
|
+
showToast('Please enter an API key', 'warning');
|
|
2095
|
+
return;
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
const testBtn = document.getElementById('llm-test-btn');
|
|
2099
|
+
testBtn.disabled = true;
|
|
2100
|
+
testBtn.textContent = 'Testing...';
|
|
2101
|
+
|
|
2102
|
+
try {
|
|
2103
|
+
const res = await fetch(`${API_BASE}/api/rules/llm/test`, {
|
|
2104
|
+
method: 'POST',
|
|
2105
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2106
|
+
body: JSON.stringify({ provider, apiKey, baseUrl: baseUrl || undefined })
|
|
2107
|
+
});
|
|
2108
|
+
|
|
2109
|
+
const data = await res.json();
|
|
2110
|
+
|
|
2111
|
+
if (data.success) {
|
|
2112
|
+
llmModels = data.models;
|
|
2113
|
+
const modelSelect = document.getElementById('llm-model');
|
|
2114
|
+
modelSelect.innerHTML = data.models.map(m =>
|
|
2115
|
+
`<option value="${m.id}">${m.name} (${m.contextLength?.toLocaleString() || '?'} ctx)</option>`
|
|
2116
|
+
).join('');
|
|
2117
|
+
|
|
2118
|
+
document.getElementById('llm-model-container').classList.remove('hidden');
|
|
2119
|
+
document.getElementById('llm-save-btn').disabled = false;
|
|
2120
|
+
updateLLMStatus(true, `Connected! ${data.models.length} models available`);
|
|
2121
|
+
showToast('Connection successful!', 'success');
|
|
2122
|
+
} else {
|
|
2123
|
+
updateLLMStatus(false, data.error || 'Connection failed');
|
|
2124
|
+
document.getElementById('llm-save-btn').disabled = true;
|
|
2125
|
+
showToast(data.error || 'Connection failed', 'error');
|
|
2126
|
+
}
|
|
2127
|
+
} catch (e) {
|
|
2128
|
+
updateLLMStatus(false, 'Connection error: ' + e.message);
|
|
2129
|
+
showToast('Connection error: ' + e.message, 'error');
|
|
2130
|
+
} finally {
|
|
2131
|
+
testBtn.disabled = false;
|
|
2132
|
+
testBtn.textContent = 'Test';
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
async function saveLLMConfig() {
|
|
2137
|
+
const provider = document.getElementById('llm-provider').value;
|
|
2138
|
+
const apiKey = document.getElementById('llm-api-key').value;
|
|
2139
|
+
const baseUrl = document.getElementById('llm-base-url').value;
|
|
2140
|
+
const model = document.getElementById('llm-model').value;
|
|
2141
|
+
|
|
2142
|
+
if (!provider) {
|
|
2143
|
+
showToast('Please select a provider', 'warning');
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
try {
|
|
2148
|
+
const res = await fetch(`${API_BASE}/api/rules/llm/config`, {
|
|
2149
|
+
method: 'PUT',
|
|
2150
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2151
|
+
body: JSON.stringify({
|
|
2152
|
+
provider,
|
|
2153
|
+
apiKey: apiKey || undefined,
|
|
2154
|
+
model: model || undefined,
|
|
2155
|
+
baseUrl: baseUrl || undefined
|
|
2156
|
+
})
|
|
2157
|
+
});
|
|
2158
|
+
|
|
2159
|
+
const data = await res.json();
|
|
2160
|
+
|
|
2161
|
+
if (data.success) {
|
|
2162
|
+
llmConfigured = true;
|
|
2163
|
+
updateLLMStatus(true, `${provider} configured${model ? ` (${model})` : ''}`);
|
|
2164
|
+
document.getElementById('llm-api-key').placeholder = '***configured*** (enter new key to change)';
|
|
2165
|
+
document.getElementById('llm-api-key').value = '';
|
|
2166
|
+
showToast('LLM configuration saved!', 'success');
|
|
2167
|
+
} else {
|
|
2168
|
+
showToast('Error: ' + data.error, 'error');
|
|
2169
|
+
}
|
|
2170
|
+
} catch (e) {
|
|
2171
|
+
showToast('Failed to save: ' + e.message, 'error');
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
function renderVersionChecks() {
|
|
2176
|
+
const list = document.getElementById('version-checks-list');
|
|
2177
|
+
const empty = document.getElementById('version-checks-empty');
|
|
2178
|
+
|
|
2179
|
+
if (!rulesConfig.versionChecks || rulesConfig.versionChecks.length === 0) {
|
|
2180
|
+
list.innerHTML = '';
|
|
2181
|
+
empty.classList.remove('hidden');
|
|
2182
|
+
return;
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
empty.classList.add('hidden');
|
|
2186
|
+
list.innerHTML = rulesConfig.versionChecks.map((check, index) => `
|
|
2187
|
+
<div class="flex items-center gap-3 bg-gray-800 rounded-lg p-3 ${!check.enabled ? 'opacity-50' : ''}">
|
|
2188
|
+
<input type="checkbox" ${check.enabled ? 'checked' : ''}
|
|
2189
|
+
onchange="toggleVersionCheck(${index})"
|
|
2190
|
+
class="w-4 h-4 rounded bg-gray-700 border-gray-600 text-primary-600 focus:ring-primary-500">
|
|
2191
|
+
<div class="flex-1">
|
|
2192
|
+
<span class="font-medium text-sm">${check.name}</span>
|
|
2193
|
+
<span class="text-xs text-gray-500 ml-2">v${check.currentVersion}</span>
|
|
2194
|
+
<code class="text-xs text-gray-400 ml-2 bg-gray-900 px-1 rounded">${escapeHtml(check.pattern)}</code>
|
|
2195
|
+
</div>
|
|
2196
|
+
<button onclick="deleteVersionCheck(${index})" class="text-gray-500 hover:text-red-400 transition-colors">
|
|
2197
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
2198
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
|
2199
|
+
</svg>
|
|
2200
|
+
</button>
|
|
2201
|
+
</div>
|
|
2202
|
+
`).join('');
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
function renderDeprecationPatterns() {
|
|
2206
|
+
const list = document.getElementById('deprecation-patterns-list');
|
|
2207
|
+
const empty = document.getElementById('deprecation-patterns-empty');
|
|
2208
|
+
|
|
2209
|
+
if (!rulesConfig.deprecationPatterns || rulesConfig.deprecationPatterns.length === 0) {
|
|
2210
|
+
list.innerHTML = '';
|
|
2211
|
+
empty.classList.remove('hidden');
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
empty.classList.add('hidden');
|
|
2216
|
+
list.innerHTML = rulesConfig.deprecationPatterns.map((pattern, index) => `
|
|
2217
|
+
<div class="flex items-center gap-3 bg-gray-800 rounded-lg p-3 ${!pattern.enabled ? 'opacity-50' : ''}">
|
|
2218
|
+
<input type="checkbox" ${pattern.enabled ? 'checked' : ''}
|
|
2219
|
+
onchange="toggleDeprecationPattern(${index})"
|
|
2220
|
+
class="w-4 h-4 rounded bg-gray-700 border-gray-600 text-primary-600 focus:ring-primary-500">
|
|
2221
|
+
<div class="flex-1">
|
|
2222
|
+
<span class="font-medium text-sm">${pattern.name}</span>
|
|
2223
|
+
<p class="text-xs text-gray-400 mt-1">${escapeHtml(pattern.reason)}</p>
|
|
2224
|
+
<code class="text-xs text-gray-500 bg-gray-900 px-1 rounded mt-1 inline-block">${escapeHtml(pattern.pattern)}</code>
|
|
2225
|
+
</div>
|
|
2226
|
+
<button onclick="deleteDeprecationPattern(${index})" class="text-gray-500 hover:text-red-400 transition-colors">
|
|
2227
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
2228
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
|
2229
|
+
</svg>
|
|
2230
|
+
</button>
|
|
2231
|
+
</div>
|
|
2232
|
+
`).join('');
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
function renderTemplates() {
|
|
2236
|
+
const container = document.getElementById('rules-templates');
|
|
2237
|
+
if (!rulesExamples) return;
|
|
2238
|
+
|
|
2239
|
+
const versionBtns = rulesExamples.versionChecks.map(v => `
|
|
2240
|
+
<button onclick="addVersionCheckFromTemplate('${v.name}', '${v.pattern.replace(/'/g, "\\'")}', '${v.currentVersion}')"
|
|
2241
|
+
class="text-xs px-2 py-1 bg-gray-800 hover:bg-gray-700 rounded transition-colors">
|
|
2242
|
+
${v.name} ${v.currentVersion}
|
|
2243
|
+
</button>
|
|
2244
|
+
`).join('');
|
|
2245
|
+
|
|
2246
|
+
const deprecationBtns = rulesExamples.deprecationPatterns.map(d => `
|
|
2247
|
+
<button onclick="addDeprecationFromTemplate('${d.name}', '${d.pattern.replace(/'/g, "\\'")}', '${d.reason.replace(/'/g, "\\'")}')"
|
|
2248
|
+
class="text-xs px-2 py-1 bg-gray-800 hover:bg-gray-700 rounded transition-colors">
|
|
2249
|
+
${d.name}
|
|
2250
|
+
</button>
|
|
2251
|
+
`).join('');
|
|
2252
|
+
|
|
2253
|
+
container.innerHTML = `
|
|
2254
|
+
<span class="text-xs text-gray-500">Versions:</span> ${versionBtns}
|
|
2255
|
+
<span class="text-xs text-gray-500 ml-2">Patterns:</span> ${deprecationBtns}
|
|
2256
|
+
`;
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
function escapeHtml(str) {
|
|
2260
|
+
const div = document.createElement('div');
|
|
2261
|
+
div.textContent = str;
|
|
2262
|
+
return div.innerHTML;
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
// ==================== RULES OPTIMIZER ====================
|
|
2266
|
+
let optimizerRunning = false;
|
|
2267
|
+
|
|
2268
|
+
let currentBrowsePath = '';
|
|
2269
|
+
|
|
2270
|
+
async function selectRulesFolder() {
|
|
2271
|
+
const homeDir = await getHomeDirectory();
|
|
2272
|
+
currentBrowsePath = homeDir;
|
|
2273
|
+
|
|
2274
|
+
await showFolderBrowser(homeDir);
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
async function showFolderBrowser(startPath) {
|
|
2278
|
+
currentBrowsePath = startPath;
|
|
2279
|
+
|
|
2280
|
+
// Fetch directory listing from server
|
|
2281
|
+
let folders = [];
|
|
2282
|
+
let currentDir = startPath;
|
|
2283
|
+
let parentDir = '';
|
|
2284
|
+
|
|
2285
|
+
try {
|
|
2286
|
+
const res = await fetch(`${API_BASE}/api/system/browse`, {
|
|
2287
|
+
method: 'POST',
|
|
2288
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2289
|
+
body: JSON.stringify({ directory: startPath })
|
|
2290
|
+
});
|
|
2291
|
+
|
|
2292
|
+
if (res.ok) {
|
|
2293
|
+
const data = await res.json();
|
|
2294
|
+
folders = data.folders || [];
|
|
2295
|
+
currentDir = data.current || startPath;
|
|
2296
|
+
parentDir = data.parent || '';
|
|
2297
|
+
}
|
|
2298
|
+
} catch (e) {
|
|
2299
|
+
console.error('Failed to browse:', e);
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
const homeDir = await getHomeDirectory();
|
|
2303
|
+
const quickPaths = [
|
|
2304
|
+
{ name: '🏠 Home', path: homeDir },
|
|
2305
|
+
{ name: '📁 .cursor/rules', path: `${homeDir}/.cursor/rules` },
|
|
2306
|
+
{ name: '📁 .codex/skills', path: `${homeDir}/.codex/skills` },
|
|
2307
|
+
];
|
|
2308
|
+
|
|
2309
|
+
const content = `
|
|
2310
|
+
<div class="mb-4">
|
|
2311
|
+
<div class="flex items-center gap-2 mb-3">
|
|
2312
|
+
<button onclick="navigateToFolder('${parentDir.replace(/'/g, "\\'")}')"
|
|
2313
|
+
class="p-2 bg-gray-800 hover:bg-gray-700 rounded-lg transition-colors ${!parentDir ? 'opacity-50 cursor-not-allowed' : ''}"
|
|
2314
|
+
${!parentDir ? 'disabled' : ''} title="Go up">
|
|
2315
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
2316
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18"/>
|
|
2317
|
+
</svg>
|
|
2318
|
+
</button>
|
|
2319
|
+
<input type="text" id="browse-path-input" value="${currentDir}"
|
|
2320
|
+
class="flex-1 bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm font-mono"
|
|
2321
|
+
onkeydown="if(event.key==='Enter') navigateToFolder(this.value)">
|
|
2322
|
+
<button onclick="navigateToFolder(document.getElementById('browse-path-input').value)"
|
|
2323
|
+
class="px-3 py-2 bg-gray-800 hover:bg-gray-700 rounded-lg transition-colors text-sm">Go</button>
|
|
2324
|
+
</div>
|
|
2325
|
+
|
|
2326
|
+
<div class="flex gap-2 mb-3 flex-wrap">
|
|
2327
|
+
${quickPaths.map(p => `
|
|
2328
|
+
<button onclick="navigateToFolder('${p.path.replace(/'/g, "\\'")}')"
|
|
2329
|
+
class="text-xs px-2 py-1 bg-gray-800 hover:bg-gray-700 rounded transition-colors">
|
|
2330
|
+
${p.name}
|
|
2331
|
+
</button>
|
|
2332
|
+
`).join('')}
|
|
2333
|
+
</div>
|
|
2334
|
+
</div>
|
|
2335
|
+
|
|
2336
|
+
<div class="max-h-64 overflow-y-auto border border-gray-700 rounded-lg">
|
|
2337
|
+
${folders.length === 0
|
|
2338
|
+
? '<p class="text-gray-500 text-sm p-4 text-center">No subfolders found</p>'
|
|
2339
|
+
: folders.map(f => `
|
|
2340
|
+
<button onclick="navigateToFolder('${f.path.replace(/'/g, "\\'")}')"
|
|
2341
|
+
class="w-full flex items-center gap-3 px-4 py-2 hover:bg-gray-800 transition-colors text-left border-b border-gray-800 last:border-0">
|
|
2342
|
+
<svg class="w-5 h-5 text-yellow-500 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
|
2343
|
+
<path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"/>
|
|
2344
|
+
</svg>
|
|
2345
|
+
<span class="text-sm truncate">${f.name}</span>
|
|
2346
|
+
</button>
|
|
2347
|
+
`).join('')
|
|
2348
|
+
}
|
|
2349
|
+
</div>
|
|
2350
|
+
|
|
2351
|
+
<p class="text-xs text-gray-500 mt-3">Click a folder to navigate, or use "Select This Folder" to choose the current directory.</p>
|
|
2352
|
+
`;
|
|
2353
|
+
|
|
2354
|
+
const result = await showModal('Browse for Rules Folder', content, [
|
|
2355
|
+
{ label: 'Cancel', value: 'cancel' },
|
|
2356
|
+
{ label: 'Select This Folder', value: 'select', primary: true },
|
|
2357
|
+
]);
|
|
2358
|
+
|
|
2359
|
+
if (result.value === 'select') {
|
|
2360
|
+
const selectedPath = document.getElementById('browse-path-input')?.value || currentDir;
|
|
2361
|
+
document.getElementById('optimizer-folder').value = selectedPath;
|
|
2362
|
+
showToast(`Folder set to: ${selectedPath}`, 'success');
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
async function navigateToFolder(path) {
|
|
2367
|
+
if (!path) return;
|
|
2368
|
+
closeModal();
|
|
2369
|
+
await showFolderBrowser(path);
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
function setFolderPath(path) {
|
|
2373
|
+
if (path) {
|
|
2374
|
+
document.getElementById('optimizer-folder').value = path;
|
|
2375
|
+
closeModal();
|
|
2376
|
+
showToast(`Folder set to: ${path}`, 'success');
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
async function getHomeDirectory() {
|
|
2381
|
+
try {
|
|
2382
|
+
const res = await fetch(`${API_BASE}/api/system/home`);
|
|
2383
|
+
if (res.ok) {
|
|
2384
|
+
const data = await res.json();
|
|
2385
|
+
return data.home || '~';
|
|
2386
|
+
}
|
|
2387
|
+
} catch (e) {
|
|
2388
|
+
// Fallback
|
|
2389
|
+
}
|
|
2390
|
+
return '~';
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
function updateOptimizerStep(step, status) {
|
|
2394
|
+
const steps = document.querySelectorAll('.optimizer-step');
|
|
2395
|
+
steps.forEach(el => {
|
|
2396
|
+
const stepName = el.dataset.step;
|
|
2397
|
+
if (stepName === step) {
|
|
2398
|
+
if (status === 'active') {
|
|
2399
|
+
el.classList.add('text-purple-400', 'font-medium');
|
|
2400
|
+
el.classList.remove('text-gray-500', 'text-green-400');
|
|
2401
|
+
} else if (status === 'done') {
|
|
2402
|
+
el.classList.add('text-green-400');
|
|
2403
|
+
el.classList.remove('text-purple-400', 'text-gray-500');
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
});
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
function resetOptimizerSteps() {
|
|
2410
|
+
const steps = document.querySelectorAll('.optimizer-step');
|
|
2411
|
+
steps.forEach(el => {
|
|
2412
|
+
el.classList.remove('text-purple-400', 'text-green-400', 'font-medium');
|
|
2413
|
+
el.classList.add('text-gray-500');
|
|
2414
|
+
});
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
async function runRulesOptimizer() {
|
|
2418
|
+
if (optimizerRunning) return;
|
|
2419
|
+
|
|
2420
|
+
const folder = document.getElementById('optimizer-folder').value.trim();
|
|
2421
|
+
const mode = document.getElementById('optimizer-mode').value;
|
|
2422
|
+
const dryRun = mode === 'dry-run';
|
|
2423
|
+
|
|
2424
|
+
if (!folder) {
|
|
2425
|
+
showToast('Please enter a rules folder path', 'warning');
|
|
2426
|
+
return;
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
// Expand ~ to home directory hint
|
|
2430
|
+
const expandedFolder = folder.startsWith('~')
|
|
2431
|
+
? folder.replace('~', '/Users/' + (window.navigator.userAgent.includes('Mac') ? '' : ''))
|
|
2432
|
+
: folder;
|
|
2433
|
+
|
|
2434
|
+
optimizerRunning = true;
|
|
2435
|
+
const runBtn = document.getElementById('optimizer-run-btn');
|
|
2436
|
+
const statusEl = document.getElementById('optimizer-status');
|
|
2437
|
+
const resultsEl = document.getElementById('optimizer-results');
|
|
2438
|
+
const actionsEl = document.getElementById('optimizer-actions');
|
|
2439
|
+
const statsEl = document.getElementById('optimizer-stats');
|
|
2440
|
+
|
|
2441
|
+
runBtn.disabled = true;
|
|
2442
|
+
runBtn.innerHTML = '<svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg><span>Running...</span>';
|
|
2443
|
+
statusEl.textContent = 'Running...';
|
|
2444
|
+
statusEl.className = 'text-xs px-2 py-1 rounded-full bg-purple-600/30 text-purple-400';
|
|
2445
|
+
|
|
2446
|
+
resultsEl.classList.remove('hidden');
|
|
2447
|
+
actionsEl.innerHTML = '<p class="text-gray-500 text-sm">Analyzing rules...</p>';
|
|
2448
|
+
statsEl.innerHTML = '';
|
|
2449
|
+
resetOptimizerSteps();
|
|
2450
|
+
|
|
2451
|
+
try {
|
|
2452
|
+
// Step 1: Analyze
|
|
2453
|
+
updateOptimizerStep('analyze', 'active');
|
|
2454
|
+
actionsEl.innerHTML = '<p class="text-gray-400 text-sm">📋 Analyzing rules in folder...</p>';
|
|
2455
|
+
|
|
2456
|
+
const analyzeRes = await fetch(`${API_BASE}/api/rules/auto-optimize`, {
|
|
2457
|
+
method: 'POST',
|
|
2458
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2459
|
+
body: JSON.stringify({ folder, dryRun })
|
|
2460
|
+
});
|
|
2461
|
+
|
|
2462
|
+
const data = await analyzeRes.json();
|
|
2463
|
+
|
|
2464
|
+
if (!analyzeRes.ok || data.error) {
|
|
2465
|
+
throw new Error(data.error || 'Optimization failed');
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
updateOptimizerStep('analyze', 'done');
|
|
2469
|
+
updateOptimizerStep('duplicates', 'done');
|
|
2470
|
+
updateOptimizerStep('merge', 'done');
|
|
2471
|
+
updateOptimizerStep('cleanup', 'done');
|
|
2472
|
+
|
|
2473
|
+
// Show stats
|
|
2474
|
+
statsEl.innerHTML = `
|
|
2475
|
+
<span class="text-gray-400">Total: <span class="text-white">${data.stats.totalRules}</span></span>
|
|
2476
|
+
<span class="text-gray-400">Duplicates: <span class="${data.stats.duplicates > 0 ? 'text-yellow-400' : 'text-green-400'}">${data.stats.duplicates}</span></span>
|
|
2477
|
+
<span class="text-gray-400">Conflicts: <span class="${data.stats.conflicts > 0 ? 'text-red-400' : 'text-green-400'}">${data.stats.conflicts}</span></span>
|
|
2478
|
+
<span class="text-gray-400">Outdated: <span class="${data.stats.outdated > 0 ? 'text-orange-400' : 'text-green-400'}">${data.stats.outdated}</span></span>
|
|
2479
|
+
${data.usedLLM ? `<span class="text-gray-400">Merged: <span class="text-green-400">${data.stats.merged}</span></span>` : ''}
|
|
2480
|
+
`;
|
|
2481
|
+
|
|
2482
|
+
// Show actions
|
|
2483
|
+
if (data.actions && data.actions.length > 0) {
|
|
2484
|
+
const actionIcons = {
|
|
2485
|
+
merge: '🔗',
|
|
2486
|
+
delete: '🗑️',
|
|
2487
|
+
create: '✨',
|
|
2488
|
+
warning: '⚠️',
|
|
2489
|
+
outdated: '📅',
|
|
2490
|
+
};
|
|
2491
|
+
const actionColors = {
|
|
2492
|
+
merge: 'border-blue-700/50 bg-blue-900/20',
|
|
2493
|
+
delete: 'border-red-700/50 bg-red-900/20',
|
|
2494
|
+
create: 'border-green-700/50 bg-green-900/20',
|
|
2495
|
+
warning: 'border-yellow-700/50 bg-yellow-900/20',
|
|
2496
|
+
outdated: 'border-orange-700/50 bg-orange-900/20',
|
|
2497
|
+
};
|
|
2498
|
+
const actionTextColors = {
|
|
2499
|
+
merge: 'text-blue-400',
|
|
2500
|
+
delete: 'text-red-400',
|
|
2501
|
+
create: 'text-green-400',
|
|
2502
|
+
warning: 'text-yellow-400',
|
|
2503
|
+
outdated: 'text-orange-400',
|
|
2504
|
+
};
|
|
2505
|
+
|
|
2506
|
+
actionsEl.innerHTML = data.actions.map(action => `
|
|
2507
|
+
<div class="flex items-start gap-3 p-3 rounded-lg border ${actionColors[action.type] || 'border-gray-700 bg-gray-800/50'}">
|
|
2508
|
+
<span class="text-lg">${actionIcons[action.type] || '📄'}</span>
|
|
2509
|
+
<div class="flex-1 min-w-0">
|
|
2510
|
+
<div class="flex items-center gap-2">
|
|
2511
|
+
<span class="text-xs font-medium uppercase ${actionTextColors[action.type] || 'text-gray-400'}">${action.type}</span>
|
|
2512
|
+
${action.type === 'merge' || action.type === 'delete'
|
|
2513
|
+
? (dryRun ? '<span class="text-xs bg-gray-700 px-1.5 py-0.5 rounded">Preview</span>' : '<span class="text-xs bg-green-700/50 text-green-300 px-1.5 py-0.5 rounded">Applied</span>')
|
|
2514
|
+
: (action.severity ? `<span class="text-xs bg-${action.severity === 'error' ? 'red' : action.severity === 'warning' ? 'yellow' : 'blue'}-700/30 px-1.5 py-0.5 rounded">${action.severity}</span>` : '')
|
|
2515
|
+
}
|
|
2516
|
+
</div>
|
|
2517
|
+
<p class="text-sm text-gray-300 truncate mt-1" title="${escapeHtml(action.path)}">${escapeHtml(action.path.split('/').pop())}</p>
|
|
2518
|
+
<p class="text-xs text-gray-500 mt-1">${escapeHtml(action.reason)}</p>
|
|
2519
|
+
</div>
|
|
2520
|
+
</div>
|
|
2521
|
+
`).join('');
|
|
2522
|
+
} else {
|
|
2523
|
+
actionsEl.innerHTML = '<p class="text-gray-400 text-sm text-center py-4">✅ No issues found - your rules are clean!</p>';
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
statusEl.textContent = dryRun ? 'Preview Complete' : 'Complete';
|
|
2527
|
+
statusEl.className = 'text-xs px-2 py-1 rounded-full bg-green-600/30 text-green-400';
|
|
2528
|
+
|
|
2529
|
+
showToast(data.message, 'success');
|
|
2530
|
+
|
|
2531
|
+
} catch (e) {
|
|
2532
|
+
statusEl.textContent = 'Error';
|
|
2533
|
+
statusEl.className = 'text-xs px-2 py-1 rounded-full bg-red-600/30 text-red-400';
|
|
2534
|
+
actionsEl.innerHTML = `<p class="text-red-400 text-sm">❌ ${escapeHtml(e.message)}</p>`;
|
|
2535
|
+
showToast(e.message, 'error');
|
|
2536
|
+
} finally {
|
|
2537
|
+
optimizerRunning = false;
|
|
2538
|
+
runBtn.disabled = false;
|
|
2539
|
+
runBtn.innerHTML = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg><span>Run Optimizer</span>';
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
async function toggleVersionCheck(index) {
|
|
2544
|
+
try {
|
|
2545
|
+
await fetch(`${API_BASE}/api/rules/config/version-checks/${index}/toggle`, { method: 'POST' });
|
|
2546
|
+
rulesConfig.versionChecks[index].enabled = !rulesConfig.versionChecks[index].enabled;
|
|
2547
|
+
renderVersionChecks();
|
|
2548
|
+
} catch (e) {
|
|
2549
|
+
showToast('Failed to toggle: ' + e.message, 'error');
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
async function toggleDeprecationPattern(index) {
|
|
2554
|
+
try {
|
|
2555
|
+
await fetch(`${API_BASE}/api/rules/config/deprecation-patterns/${index}/toggle`, { method: 'POST' });
|
|
2556
|
+
rulesConfig.deprecationPatterns[index].enabled = !rulesConfig.deprecationPatterns[index].enabled;
|
|
2557
|
+
renderDeprecationPatterns();
|
|
2558
|
+
} catch (e) {
|
|
2559
|
+
showToast('Failed to toggle: ' + e.message, 'error');
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
async function deleteVersionCheck(index) {
|
|
2564
|
+
const confirmed = await showConfirmModal('Delete Version Check', 'Are you sure you want to delete this version check pattern?');
|
|
2565
|
+
if (!confirmed) return;
|
|
2566
|
+
try {
|
|
2567
|
+
await fetch(`${API_BASE}/api/rules/config/version-checks/${index}`, { method: 'DELETE' });
|
|
2568
|
+
rulesConfig.versionChecks.splice(index, 1);
|
|
2569
|
+
renderVersionChecks();
|
|
2570
|
+
showToast('Version check deleted', 'success');
|
|
2571
|
+
} catch (e) {
|
|
2572
|
+
showToast('Failed to delete: ' + e.message, 'error');
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
async function deleteDeprecationPattern(index) {
|
|
2577
|
+
const confirmed = await showConfirmModal('Delete Deprecation Pattern', 'Are you sure you want to delete this deprecation pattern?');
|
|
2578
|
+
if (!confirmed) return;
|
|
2579
|
+
try {
|
|
2580
|
+
await fetch(`${API_BASE}/api/rules/config/deprecation-patterns/${index}`, { method: 'DELETE' });
|
|
2581
|
+
rulesConfig.deprecationPatterns.splice(index, 1);
|
|
2582
|
+
renderDeprecationPatterns();
|
|
2583
|
+
showToast('Deprecation pattern deleted', 'success');
|
|
2584
|
+
} catch (e) {
|
|
2585
|
+
showToast('Failed to delete: ' + e.message, 'error');
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
async function showAddVersionCheckModal() {
|
|
2590
|
+
const data = await showInputModal('Add Version Check Pattern', [
|
|
2591
|
+
{ name: 'name', label: 'Technology Name', placeholder: 'e.g., Python', required: true },
|
|
2592
|
+
{ name: 'pattern', label: 'Regex Pattern', placeholder: 'e.g., \\bpython\\s+(\\d+)', required: true, hint: 'Use one capture group for version number' },
|
|
2593
|
+
{ name: 'version', label: 'Current Version', placeholder: 'e.g., 3.12', required: true },
|
|
2594
|
+
]);
|
|
2595
|
+
|
|
2596
|
+
if (data) {
|
|
2597
|
+
await addVersionCheckFromTemplate(data.name, data.pattern, data.version);
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
async function addVersionCheckFromTemplate(name, pattern, version) {
|
|
2602
|
+
if (rulesConfig.versionChecks.some(v => v.name === name)) {
|
|
2603
|
+
showToast(`${name} version check already exists`, 'warning');
|
|
2604
|
+
return;
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
try {
|
|
2608
|
+
const res = await fetch(`${API_BASE}/api/rules/config/version-checks`, {
|
|
2609
|
+
method: 'POST',
|
|
2610
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2611
|
+
body: JSON.stringify({ name, pattern, currentVersion: version, enabled: true })
|
|
2612
|
+
});
|
|
2613
|
+
const data = await res.json();
|
|
2614
|
+
if (data.error) {
|
|
2615
|
+
showToast('Error: ' + data.error, 'error');
|
|
2616
|
+
return;
|
|
2617
|
+
}
|
|
2618
|
+
rulesConfig = data.config;
|
|
2619
|
+
renderVersionChecks();
|
|
2620
|
+
showToast(`Added ${name} version check`, 'success');
|
|
2621
|
+
} catch (e) {
|
|
2622
|
+
showToast('Failed to add: ' + e.message, 'error');
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
async function showAddDeprecationModal() {
|
|
2627
|
+
const data = await showInputModal('Add Deprecation Pattern', [
|
|
2628
|
+
{ name: 'name', label: 'Pattern Name', placeholder: 'e.g., var-usage', required: true },
|
|
2629
|
+
{ name: 'pattern', label: 'Regex Pattern', placeholder: 'e.g., \\bvar\\s+\\w+\\s*=', required: true },
|
|
2630
|
+
{ name: 'reason', label: 'Why is this deprecated?', placeholder: 'e.g., var is discouraged in modern JavaScript', required: true },
|
|
2631
|
+
{ name: 'suggestion', label: 'Suggested Alternative', placeholder: 'e.g., Use const or let instead' },
|
|
2632
|
+
]);
|
|
2633
|
+
|
|
2634
|
+
if (data) {
|
|
2635
|
+
await addDeprecationFromTemplate(data.name, data.pattern, data.reason, data.suggestion);
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
async function addDeprecationFromTemplate(name, pattern, reason, suggestion = '') {
|
|
2640
|
+
if (rulesConfig.deprecationPatterns.some(d => d.name === name)) {
|
|
2641
|
+
showToast(`${name} deprecation pattern already exists`, 'warning');
|
|
2642
|
+
return;
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
try {
|
|
2646
|
+
const res = await fetch(`${API_BASE}/api/rules/config/deprecation-patterns`, {
|
|
2647
|
+
method: 'POST',
|
|
2648
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2649
|
+
body: JSON.stringify({ name, pattern, reason, suggestion, enabled: true })
|
|
2650
|
+
});
|
|
2651
|
+
const data = await res.json();
|
|
2652
|
+
if (data.error) {
|
|
2653
|
+
showToast('Error: ' + data.error, 'error');
|
|
2654
|
+
return;
|
|
2655
|
+
}
|
|
2656
|
+
rulesConfig = data.config;
|
|
2657
|
+
renderDeprecationPatterns();
|
|
2658
|
+
showToast(`Added ${name} deprecation pattern`, 'success');
|
|
2659
|
+
} catch (e) {
|
|
2660
|
+
showToast('Failed to add: ' + e.message, 'error');
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
// Natural Language Rules
|
|
2665
|
+
async function showAddNaturalRuleModal() {
|
|
2666
|
+
const data = await showInputModal('Add Natural Language Rule', [
|
|
2667
|
+
{ name: 'rule', label: 'Rule Description', type: 'textarea', placeholder: 'e.g., Flag any mention of jQuery or legacy DOM manipulation patterns', required: true, hint: 'Describe in plain English what the LLM should look for when analyzing rules' },
|
|
2668
|
+
{ name: 'severity', label: 'Severity', placeholder: 'warning, error, or info', value: 'warning' },
|
|
2669
|
+
]);
|
|
2670
|
+
|
|
2671
|
+
if (data && data.rule) {
|
|
2672
|
+
await addNaturalRule(data.rule, data.severity || 'warning');
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
async function addNaturalRule(rule, severity) {
|
|
2677
|
+
if (!rulesConfig.naturalRules) {
|
|
2678
|
+
rulesConfig.naturalRules = [];
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
rulesConfig.naturalRules.push({ rule, severity, enabled: true });
|
|
2682
|
+
renderNaturalRules();
|
|
2683
|
+
showToast('Natural language rule added', 'success');
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
async function toggleNaturalRule(index) {
|
|
2687
|
+
rulesConfig.naturalRules[index].enabled = !rulesConfig.naturalRules[index].enabled;
|
|
2688
|
+
renderNaturalRules();
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
async function deleteNaturalRule(index) {
|
|
2692
|
+
const confirmed = await showConfirmModal('Delete Natural Language Rule', 'Are you sure you want to delete this rule?');
|
|
2693
|
+
if (!confirmed) return;
|
|
2694
|
+
rulesConfig.naturalRules.splice(index, 1);
|
|
2695
|
+
renderNaturalRules();
|
|
2696
|
+
showToast('Natural language rule deleted', 'success');
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2699
|
+
function renderNaturalRules() {
|
|
2700
|
+
const list = document.getElementById('natural-rules-list');
|
|
2701
|
+
const empty = document.getElementById('natural-rules-empty');
|
|
2702
|
+
|
|
2703
|
+
if (!rulesConfig.naturalRules || rulesConfig.naturalRules.length === 0) {
|
|
2704
|
+
list.innerHTML = '';
|
|
2705
|
+
empty.classList.remove('hidden');
|
|
2706
|
+
return;
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
empty.classList.add('hidden');
|
|
2710
|
+
const severityColors = {
|
|
2711
|
+
error: 'bg-red-600/30 text-red-400',
|
|
2712
|
+
warning: 'bg-yellow-600/30 text-yellow-400',
|
|
2713
|
+
info: 'bg-blue-600/30 text-blue-400',
|
|
2714
|
+
};
|
|
2715
|
+
|
|
2716
|
+
list.innerHTML = rulesConfig.naturalRules.map((nr, index) => `
|
|
2717
|
+
<div class="flex items-start gap-3 bg-gray-800 rounded-lg p-3 ${!nr.enabled ? 'opacity-50' : ''}">
|
|
2718
|
+
<input type="checkbox" ${nr.enabled ? 'checked' : ''}
|
|
2719
|
+
onchange="toggleNaturalRule(${index})"
|
|
2720
|
+
class="w-4 h-4 mt-1 rounded bg-gray-700 border-gray-600 text-purple-600 focus:ring-purple-500">
|
|
2721
|
+
<div class="flex-1">
|
|
2722
|
+
<div class="flex items-center gap-2 mb-1">
|
|
2723
|
+
<span class="text-xs px-2 py-0.5 rounded ${severityColors[nr.severity] || severityColors.warning}">${nr.severity}</span>
|
|
2724
|
+
</div>
|
|
2725
|
+
<p class="text-sm text-gray-300">${escapeHtml(nr.rule)}</p>
|
|
2726
|
+
</div>
|
|
2727
|
+
<button onclick="deleteNaturalRule(${index})" class="text-gray-500 hover:text-red-400 transition-colors">
|
|
2728
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
2729
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
|
2730
|
+
</svg>
|
|
2731
|
+
</button>
|
|
2732
|
+
</div>
|
|
2733
|
+
`).join('');
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
async function saveRulesConfig() {
|
|
2737
|
+
// Update config from form
|
|
2738
|
+
rulesConfig.analysis.duplicateThreshold = parseFloat(document.getElementById('rules-duplicate-threshold').value);
|
|
2739
|
+
rulesConfig.analysis.maxAgeDays = parseInt(document.getElementById('rules-max-age-days').value);
|
|
2740
|
+
rulesConfig.analysis.oldYearThreshold = parseInt(document.getElementById('rules-old-year-threshold').value);
|
|
2741
|
+
rulesConfig.analysis.detectConflicts = document.getElementById('rules-detect-conflicts').checked;
|
|
2742
|
+
rulesConfig.analysis.detectOutdated = document.getElementById('rules-detect-outdated').checked;
|
|
2743
|
+
rulesConfig.analysis.useLLM = document.getElementById('rules-use-llm').checked;
|
|
2744
|
+
|
|
2745
|
+
try {
|
|
2746
|
+
const res = await fetch(`${API_BASE}/api/rules/config`, {
|
|
2747
|
+
method: 'PUT',
|
|
2748
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2749
|
+
body: JSON.stringify(rulesConfig)
|
|
2750
|
+
});
|
|
2751
|
+
const data = await res.json();
|
|
2752
|
+
if (data.error) {
|
|
2753
|
+
showToast('Error: ' + data.error, 'error');
|
|
2754
|
+
return;
|
|
2755
|
+
}
|
|
2756
|
+
showToast('Rules configuration saved!', 'success');
|
|
2757
|
+
} catch (e) {
|
|
2758
|
+
showToast('Failed to save: ' + e.message, 'error');
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
// Load rules config when settings tab is shown
|
|
2763
|
+
const originalShowTab = showTab;
|
|
2764
|
+
showTab = function(tabId) {
|
|
2765
|
+
originalShowTab(tabId);
|
|
2766
|
+
if (tabId === 'settings' && !rulesConfig) {
|
|
2767
|
+
loadRulesConfig();
|
|
2768
|
+
}
|
|
2769
|
+
};
|
|
2770
|
+
// ==================== END RULES ANALYZER CONFIGURATION ====================
|
|
2771
|
+
|
|
803
2772
|
// Initial load
|
|
804
2773
|
fetchConfig().then(() => {
|
|
805
2774
|
fetchStats();
|