mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-10 02:24:35 +08:00
feat: Implement Skills Manager View and Notifier Module
- Added `skills-manager.js` for managing Claude Code skills with functionalities for loading, displaying, and editing skills. - Introduced a Notifier module in `notifier.ts` for CLI to server communication, enabling notifications for UI updates on data changes. - Created comprehensive documentation for the Chain Search implementation, including usage examples and performance tips. - Developed a test suite for the Chain Search engine, covering basic search, quick search, symbol search, and files-only search functionalities.
This commit is contained in:
@@ -115,6 +115,7 @@
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
background: hsl(var(--muted) / 0.3);
|
||||
gap: 1rem;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.prompt-timeline-header h3 {
|
||||
@@ -125,6 +126,7 @@
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.prompt-timeline-filters {
|
||||
@@ -133,12 +135,16 @@
|
||||
gap: 0.5rem;
|
||||
flex: 1;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.prompt-search-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
min-width: 120px;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
@@ -149,6 +155,7 @@
|
||||
transform: translateY(-50%);
|
||||
color: hsl(var(--muted-foreground));
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.prompt-search-input {
|
||||
@@ -225,28 +232,102 @@
|
||||
.prompt-session-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
gap: 0;
|
||||
position: relative;
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
/* Prompt Items */
|
||||
/* Timeline axis - subtle vertical line */
|
||||
.prompt-session-items::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0.4375rem;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
background: hsl(var(--border));
|
||||
}
|
||||
|
||||
/* Prompt Items - Card style matching memory timeline */
|
||||
.prompt-item {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
padding: 0.875rem;
|
||||
background: hsl(var(--card));
|
||||
margin-bottom: 0.625rem;
|
||||
background: hsl(var(--background));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
transition: all 0.15s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Timeline dot - clean circle */
|
||||
.prompt-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -2rem;
|
||||
top: 1rem;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: hsl(var(--background));
|
||||
border: 2px solid hsl(var(--muted-foreground) / 0.4);
|
||||
border-radius: 50%;
|
||||
transform: translateX(50%);
|
||||
z-index: 1;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* Timeline connector line to card */
|
||||
.prompt-item::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -1.25rem;
|
||||
top: 1.25rem;
|
||||
width: 1rem;
|
||||
height: 2px;
|
||||
background: hsl(var(--border));
|
||||
}
|
||||
|
||||
.prompt-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.prompt-item:hover {
|
||||
border-color: hsl(var(--primary) / 0.3);
|
||||
box-shadow: 0 2px 8px hsl(var(--primary) / 0.1);
|
||||
background: hsl(var(--hover));
|
||||
}
|
||||
|
||||
.prompt-item:hover::before {
|
||||
border-color: hsl(var(--primary));
|
||||
background: hsl(var(--primary) / 0.1);
|
||||
}
|
||||
|
||||
.prompt-item:hover::after {
|
||||
background: hsl(var(--primary) / 0.3);
|
||||
}
|
||||
|
||||
.prompt-item-expanded {
|
||||
max-height: none;
|
||||
background: hsl(var(--muted) / 0.3);
|
||||
border-color: hsl(var(--primary) / 0.5);
|
||||
}
|
||||
|
||||
.prompt-item-expanded::before {
|
||||
background: hsl(var(--primary));
|
||||
border-color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.prompt-item-expanded::after {
|
||||
background: hsl(var(--primary) / 0.5);
|
||||
}
|
||||
|
||||
/* Inner content layout */
|
||||
.prompt-item-inner {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.prompt-item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -665,3 +746,322 @@
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* ========== Insights History Cards ========== */
|
||||
.insights-history-container {
|
||||
padding: 0.75rem;
|
||||
max-height: calc(100vh - 300px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.insights-history-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.insight-history-card {
|
||||
padding: 0.875rem;
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-left-width: 3px;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.insight-history-card:hover {
|
||||
background: hsl(var(--muted) / 0.5);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px hsl(var(--foreground) / 0.08);
|
||||
}
|
||||
|
||||
.insight-history-card.high {
|
||||
border-left-color: hsl(0, 84%, 60%);
|
||||
}
|
||||
|
||||
.insight-history-card.medium {
|
||||
border-left-color: hsl(48, 96%, 53%);
|
||||
}
|
||||
|
||||
.insight-history-card.low {
|
||||
border-left-color: hsl(142, 71%, 45%);
|
||||
}
|
||||
|
||||
.insight-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.625rem;
|
||||
}
|
||||
|
||||
.insight-card-tool {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.insight-card-tool i {
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.insight-card-time {
|
||||
font-size: 0.6875rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.insight-card-stats {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 0.625rem;
|
||||
}
|
||||
|
||||
.insight-stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.insight-stat-value {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.insight-stat-label {
|
||||
font-size: 0.625rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
.insight-card-preview {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.pattern-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pattern-preview.high {
|
||||
background: hsl(0, 84%, 95%);
|
||||
}
|
||||
|
||||
.pattern-preview.medium {
|
||||
background: hsl(48, 96%, 95%);
|
||||
}
|
||||
|
||||
.pattern-preview.low {
|
||||
background: hsl(142, 71%, 95%);
|
||||
}
|
||||
|
||||
.pattern-preview .pattern-type {
|
||||
font-weight: 600;
|
||||
text-transform: capitalize;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pattern-preview .pattern-desc {
|
||||
color: hsl(var(--muted-foreground));
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Insight Detail Panel */
|
||||
.insight-detail-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 400px;
|
||||
max-width: 90vw;
|
||||
height: 100vh;
|
||||
background: hsl(var(--card));
|
||||
border-left: 1px solid hsl(var(--border));
|
||||
box-shadow: -4px 0 16px hsl(var(--foreground) / 0.1);
|
||||
z-index: 1000;
|
||||
overflow-y: auto;
|
||||
animation: slideInRight 0.2s ease;
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.insight-detail {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.insight-detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.insight-detail-header h4 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.insight-detail-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1.25rem;
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.insight-detail-meta span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.insight-patterns,
|
||||
.insight-suggestions {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.insight-patterns h5,
|
||||
.insight-suggestions h5 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 0.75rem 0;
|
||||
}
|
||||
|
||||
.patterns-list,
|
||||
.suggestions-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.625rem;
|
||||
}
|
||||
|
||||
.pattern-item {
|
||||
padding: 0.75rem;
|
||||
background: hsl(var(--background));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-left-width: 3px;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.pattern-item.high {
|
||||
border-left-color: hsl(0, 84%, 60%);
|
||||
}
|
||||
|
||||
.pattern-item.medium {
|
||||
border-left-color: hsl(48, 96%, 53%);
|
||||
}
|
||||
|
||||
.pattern-item.low {
|
||||
border-left-color: hsl(142, 71%, 45%);
|
||||
}
|
||||
|
||||
.pattern-item .pattern-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.pattern-type-badge {
|
||||
padding: 0.125rem 0.5rem;
|
||||
background: hsl(var(--muted));
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.pattern-severity {
|
||||
font-size: 0.6875rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.pattern-occurrences {
|
||||
margin-left: auto;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.pattern-item .pattern-description {
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.pattern-item .pattern-suggestion {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
gap: 0.375rem;
|
||||
padding: 0.5rem;
|
||||
background: hsl(var(--muted) / 0.5);
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.suggestion-item {
|
||||
padding: 0.75rem;
|
||||
background: hsl(var(--background));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.suggestion-item .suggestion-title {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
.suggestion-item .suggestion-description {
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
line-height: 1.5;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.suggestion-item .suggestion-example {
|
||||
padding: 0.5rem;
|
||||
background: hsl(var(--muted) / 0.5);
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.suggestion-item .suggestion-example code {
|
||||
font-size: 0.75rem;
|
||||
font-family: monospace;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.insight-detail-actions {
|
||||
margin-top: 1.5rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
216
ccw/src/templates/dashboard-css/12-skills-rules.css
Normal file
216
ccw/src/templates/dashboard-css/12-skills-rules.css
Normal file
@@ -0,0 +1,216 @@
|
||||
/* ==========================================
|
||||
SKILLS & RULES MANAGER STYLES
|
||||
========================================== */
|
||||
|
||||
/* Skills Manager */
|
||||
.skills-manager {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.skills-manager.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 300px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.skills-section {
|
||||
margin-bottom: 2rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.skills-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.skill-card {
|
||||
position: relative;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.skill-card:hover {
|
||||
border-color: hsl(var(--primary));
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.skills-empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 160px;
|
||||
}
|
||||
|
||||
/* Skill Detail Panel */
|
||||
.skill-detail-panel {
|
||||
animation: slideIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
.skill-detail-overlay {
|
||||
animation: fadeIn 0.15s ease-out;
|
||||
}
|
||||
|
||||
/* Rules Manager */
|
||||
.rules-manager {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rules-manager.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 300px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.rules-section {
|
||||
margin-bottom: 2rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rules-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rule-card {
|
||||
position: relative;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.rule-card:hover {
|
||||
border-color: hsl(var(--success));
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.rules-empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 160px;
|
||||
}
|
||||
|
||||
/* Rule Detail Panel */
|
||||
.rule-detail-panel {
|
||||
animation: slideIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
.rule-detail-overlay {
|
||||
animation: fadeIn 0.15s ease-out;
|
||||
}
|
||||
|
||||
/* Shared Animations */
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
/* Line clamp utility for card descriptions */
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.skills-grid,
|
||||
.rules-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.skill-detail-panel,
|
||||
.rule-detail-panel {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Badge styles for skills and rules */
|
||||
.skill-card .badge,
|
||||
.rule-card .badge {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Code preview in rule cards */
|
||||
.rule-card pre {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Create modal styles (shared) */
|
||||
.skill-modal,
|
||||
.rule-modal {
|
||||
animation: fadeIn 0.15s ease-out;
|
||||
}
|
||||
|
||||
.skill-modal-backdrop,
|
||||
.rule-modal-backdrop {
|
||||
animation: fadeIn 0.15s ease-out;
|
||||
}
|
||||
|
||||
.skill-modal-content,
|
||||
.rule-modal-content {
|
||||
animation: slideUp 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.skill-modal.hidden,
|
||||
.rule-modal.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Form group styles */
|
||||
.skill-modal .form-group label,
|
||||
.rule-modal .form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.skill-modal input,
|
||||
.skill-modal textarea,
|
||||
.rule-modal input,
|
||||
.rule-modal textarea {
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.skill-modal input:focus,
|
||||
.skill-modal textarea:focus,
|
||||
.rule-modal input:focus,
|
||||
.rule-modal textarea:focus {
|
||||
border-color: hsl(var(--primary));
|
||||
box-shadow: 0 0 0 2px hsl(var(--primary) / 0.2);
|
||||
}
|
||||
@@ -108,6 +108,10 @@ function initNavigation() {
|
||||
renderMemoryView();
|
||||
} else if (currentView === 'prompt-history') {
|
||||
renderPromptHistoryView();
|
||||
} else if (currentView === 'skills-manager') {
|
||||
renderSkillsManager();
|
||||
} else if (currentView === 'rules-manager') {
|
||||
renderRulesManager();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -136,6 +140,10 @@ function updateContentTitle() {
|
||||
titleEl.textContent = t('title.memoryModule');
|
||||
} else if (currentView === 'prompt-history') {
|
||||
titleEl.textContent = t('title.promptHistory');
|
||||
} else if (currentView === 'skills-manager') {
|
||||
titleEl.textContent = t('title.skillsManager');
|
||||
} else if (currentView === 'rules-manager') {
|
||||
titleEl.textContent = t('title.rulesManager');
|
||||
} else if (currentView === 'liteTasks') {
|
||||
const names = { 'lite-plan': t('title.litePlanSessions'), 'lite-fix': t('title.liteFixSessions') };
|
||||
titleEl.textContent = names[currentLiteType] || t('title.liteTasks');
|
||||
|
||||
@@ -624,6 +624,62 @@ const i18n = {
|
||||
'memory.prompts': 'prompts',
|
||||
'memory.refreshInsights': 'Refresh',
|
||||
|
||||
// Skills
|
||||
'nav.skills': 'Skills',
|
||||
'title.skillsManager': 'Skills Manager',
|
||||
'skills.title': 'Skills Manager',
|
||||
'skills.description': 'Manage Claude Code skills and capabilities',
|
||||
'skills.create': 'Create Skill',
|
||||
'skills.projectSkills': 'Project Skills',
|
||||
'skills.userSkills': 'User Skills',
|
||||
'skills.skillsCount': 'skills',
|
||||
'skills.noProjectSkills': 'No project skills found',
|
||||
'skills.createHint': 'Create a skill in .claude/skills/ to add capabilities',
|
||||
'skills.noUserSkills': 'No user skills found',
|
||||
'skills.userSkillsHint': 'User skills apply to all your projects',
|
||||
'skills.noDescription': 'No description provided',
|
||||
'skills.tools': 'tools',
|
||||
'skills.files': 'files',
|
||||
'skills.descriptionLabel': 'Description',
|
||||
'skills.metadata': 'Metadata',
|
||||
'skills.location': 'Location',
|
||||
'skills.version': 'Version',
|
||||
'skills.allowedTools': 'Allowed Tools',
|
||||
'skills.supportingFiles': 'Supporting Files',
|
||||
'skills.path': 'Path',
|
||||
'skills.loadError': 'Failed to load skill details',
|
||||
'skills.deleteConfirm': 'Are you sure you want to delete the skill "{name}"?',
|
||||
'skills.deleted': 'Skill deleted successfully',
|
||||
'skills.deleteError': 'Failed to delete skill',
|
||||
'skills.editNotImplemented': 'Edit feature coming soon',
|
||||
'skills.createNotImplemented': 'Create feature coming soon',
|
||||
|
||||
// Rules
|
||||
'nav.rules': 'Rules',
|
||||
'title.rulesManager': 'Rules Manager',
|
||||
'rules.title': 'Rules Manager',
|
||||
'rules.description': 'Manage project and user rules for Claude Code',
|
||||
'rules.create': 'Create Rule',
|
||||
'rules.projectRules': 'Project Rules',
|
||||
'rules.userRules': 'User Rules',
|
||||
'rules.rulesCount': 'rules',
|
||||
'rules.noProjectRules': 'No project rules found',
|
||||
'rules.createHint': 'Create rules in .claude/rules/ for project-specific instructions',
|
||||
'rules.noUserRules': 'No user rules found',
|
||||
'rules.userRulesHint': 'User rules apply to all your projects',
|
||||
'rules.typeLabel': 'Type',
|
||||
'rules.conditional': 'Conditional',
|
||||
'rules.global': 'Global',
|
||||
'rules.pathConditions': 'Path Conditions',
|
||||
'rules.content': 'Content',
|
||||
'rules.filePath': 'File Path',
|
||||
'rules.loadError': 'Failed to load rule details',
|
||||
'rules.deleteConfirm': 'Are you sure you want to delete the rule "{name}"?',
|
||||
'rules.deleted': 'Rule deleted successfully',
|
||||
'rules.deleteError': 'Failed to delete rule',
|
||||
'rules.editNotImplemented': 'Edit feature coming soon',
|
||||
'rules.createNotImplemented': 'Create feature coming soon',
|
||||
|
||||
// Common
|
||||
'common.cancel': 'Cancel',
|
||||
'common.create': 'Create',
|
||||
@@ -1258,6 +1314,62 @@ const i18n = {
|
||||
'memory.prompts': '提示',
|
||||
'memory.refreshInsights': '刷新',
|
||||
|
||||
// Skills
|
||||
'nav.skills': '技能',
|
||||
'title.skillsManager': '技能管理',
|
||||
'skills.title': '技能管理',
|
||||
'skills.description': '管理 Claude Code 的技能和能力',
|
||||
'skills.create': '创建技能',
|
||||
'skills.projectSkills': '项目技能',
|
||||
'skills.userSkills': '用户技能',
|
||||
'skills.skillsCount': '个技能',
|
||||
'skills.noProjectSkills': '未找到项目技能',
|
||||
'skills.createHint': '在 .claude/skills/ 中创建技能以添加功能',
|
||||
'skills.noUserSkills': '未找到用户技能',
|
||||
'skills.userSkillsHint': '用户技能适用于所有项目',
|
||||
'skills.noDescription': '无描述',
|
||||
'skills.tools': '工具',
|
||||
'skills.files': '文件',
|
||||
'skills.descriptionLabel': '描述',
|
||||
'skills.metadata': '元数据',
|
||||
'skills.location': '位置',
|
||||
'skills.version': '版本',
|
||||
'skills.allowedTools': '允许的工具',
|
||||
'skills.supportingFiles': '支持文件',
|
||||
'skills.path': '路径',
|
||||
'skills.loadError': '加载技能详情失败',
|
||||
'skills.deleteConfirm': '确定要删除技能 "{name}" 吗?',
|
||||
'skills.deleted': '技能删除成功',
|
||||
'skills.deleteError': '删除技能失败',
|
||||
'skills.editNotImplemented': '编辑功能即将推出',
|
||||
'skills.createNotImplemented': '创建功能即将推出',
|
||||
|
||||
// Rules
|
||||
'nav.rules': '规则',
|
||||
'title.rulesManager': '规则管理',
|
||||
'rules.title': '规则管理',
|
||||
'rules.description': '管理 Claude Code 的项目和用户规则',
|
||||
'rules.create': '创建规则',
|
||||
'rules.projectRules': '项目规则',
|
||||
'rules.userRules': '用户规则',
|
||||
'rules.rulesCount': '条规则',
|
||||
'rules.noProjectRules': '未找到项目规则',
|
||||
'rules.createHint': '在 .claude/rules/ 中创建规则以设置项目特定指令',
|
||||
'rules.noUserRules': '未找到用户规则',
|
||||
'rules.userRulesHint': '用户规则适用于所有项目',
|
||||
'rules.typeLabel': '类型',
|
||||
'rules.conditional': '条件规则',
|
||||
'rules.global': '全局规则',
|
||||
'rules.pathConditions': '路径条件',
|
||||
'rules.content': '内容',
|
||||
'rules.filePath': '文件路径',
|
||||
'rules.loadError': '加载规则详情失败',
|
||||
'rules.deleteConfirm': '确定要删除规则 "{name}" 吗?',
|
||||
'rules.deleted': '规则删除成功',
|
||||
'rules.deleteError': '删除规则失败',
|
||||
'rules.editNotImplemented': '编辑功能即将推出',
|
||||
'rules.createNotImplemented': '创建功能即将推出',
|
||||
|
||||
// Common
|
||||
'common.cancel': '取消',
|
||||
'common.create': '创建',
|
||||
|
||||
@@ -58,14 +58,12 @@ async function renderMemoryView() {
|
||||
'<div class="memory-column center" id="memory-graph"></div>' +
|
||||
'<div class="memory-column right" id="memory-context"></div>' +
|
||||
'</div>' +
|
||||
'<div class="memory-insights-section" id="memory-insights"></div>' +
|
||||
'</div>';
|
||||
|
||||
// Render each column
|
||||
renderHotspotsColumn();
|
||||
renderGraphColumn();
|
||||
renderContextColumn();
|
||||
renderInsightsSection();
|
||||
|
||||
// Initialize Lucide icons
|
||||
if (window.lucide) lucide.createIcons();
|
||||
|
||||
@@ -8,6 +8,8 @@ var promptHistorySearch = '';
|
||||
var promptHistoryDateFilter = null;
|
||||
var promptHistoryProjectFilter = null;
|
||||
var selectedPromptId = null;
|
||||
var promptInsightsHistory = []; // Insights analysis history
|
||||
var selectedPromptInsight = null; // Currently selected insight for detail view
|
||||
|
||||
// ========== Data Loading ==========
|
||||
async function loadPromptHistory() {
|
||||
@@ -40,6 +42,20 @@ async function loadPromptInsights() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPromptInsightsHistory() {
|
||||
try {
|
||||
var response = await fetch('/api/memory/insights?limit=20');
|
||||
if (!response.ok) throw new Error('Failed to load insights history');
|
||||
var data = await response.json();
|
||||
promptInsightsHistory = data.insights || [];
|
||||
return promptInsightsHistory;
|
||||
} catch (err) {
|
||||
console.error('Failed to load insights history:', err);
|
||||
promptInsightsHistory = [];
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Rendering ==========
|
||||
async function renderPromptHistoryView() {
|
||||
var container = document.getElementById('mainContent');
|
||||
@@ -52,7 +68,7 @@ async function renderPromptHistoryView() {
|
||||
if (searchInput) searchInput.parentElement.style.display = 'none';
|
||||
|
||||
// Load data
|
||||
await Promise.all([loadPromptHistory(), loadPromptInsights()]);
|
||||
await Promise.all([loadPromptHistory(), loadPromptInsights(), loadPromptInsightsHistory()]);
|
||||
|
||||
// Calculate stats
|
||||
var totalPrompts = promptHistoryData.length;
|
||||
@@ -232,51 +248,207 @@ function renderInsightsPanel() {
|
||||
return html;
|
||||
}
|
||||
|
||||
if (!promptInsights || !promptInsights.patterns || promptInsights.patterns.length === 0) {
|
||||
html += '<div class="insights-empty-state">' +
|
||||
// Show insights history cards
|
||||
html += '<div class="insights-history-container">' +
|
||||
renderPromptInsightsHistory() +
|
||||
'</div>';
|
||||
|
||||
// Show detail panel if an insight is selected
|
||||
if (selectedPromptInsight) {
|
||||
html += '<div class="insight-detail-panel" id="promptInsightDetailPanel">' +
|
||||
renderPromptInsightDetail(selectedPromptInsight) +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderPromptInsightsHistory() {
|
||||
if (!promptInsightsHistory || promptInsightsHistory.length === 0) {
|
||||
return '<div class="insights-empty-state">' +
|
||||
'<i data-lucide="brain" class="w-10 h-10"></i>' +
|
||||
'<p>' + t('prompt.noInsights') + '</p>' +
|
||||
'<p class="insights-hint">' + t('prompt.noInsightsText') + '</p>' +
|
||||
'</div>';
|
||||
} else {
|
||||
html += '<div class="insights-list">';
|
||||
|
||||
// Render detected patterns
|
||||
if (promptInsights.patterns && promptInsights.patterns.length > 0) {
|
||||
html += '<div class="insights-section">' +
|
||||
'<h4><i data-lucide="alert-circle" class="w-4 h-4"></i> Detected Patterns</h4>';
|
||||
for (var i = 0; i < promptInsights.patterns.length; i++) {
|
||||
html += renderPatternCard(promptInsights.patterns[i]);
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// Render suggestions
|
||||
if (promptInsights.suggestions && promptInsights.suggestions.length > 0) {
|
||||
html += '<div class="insights-section">' +
|
||||
'<h4><i data-lucide="zap" class="w-4 h-4"></i> Optimization Suggestions</h4>';
|
||||
for (var j = 0; j < promptInsights.suggestions.length; j++) {
|
||||
html += renderSuggestionCard(promptInsights.suggestions[j]);
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// Render similar successful prompts
|
||||
if (promptInsights.similar_prompts && promptInsights.similar_prompts.length > 0) {
|
||||
html += '<div class="insights-section">' +
|
||||
'<h4><i data-lucide="stars" class="w-4 h-4"></i> Similar Successful Prompts</h4>';
|
||||
for (var k = 0; k < promptInsights.similar_prompts.length; k++) {
|
||||
html += renderSimilarPromptCard(promptInsights.similar_prompts[k]);
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
return '<div class="insights-history-cards">' +
|
||||
promptInsightsHistory.map(function(insight) {
|
||||
var patternCount = (insight.patterns || []).length;
|
||||
var suggestionCount = (insight.suggestions || []).length;
|
||||
var severity = getPromptInsightSeverity(insight.patterns);
|
||||
var timeAgo = formatPromptTimestamp(insight.created_at);
|
||||
|
||||
return '<div class="insight-history-card ' + severity + '" onclick="showPromptInsightDetail(\'' + insight.id + '\')">' +
|
||||
'<div class="insight-card-header">' +
|
||||
'<div class="insight-card-tool">' +
|
||||
'<i data-lucide="' + getPromptToolIcon(insight.tool) + '" class="w-4 h-4"></i>' +
|
||||
'<span>' + (insight.tool || 'CLI') + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="insight-card-time">' + timeAgo + '</div>' +
|
||||
'</div>' +
|
||||
'<div class="insight-card-stats">' +
|
||||
'<div class="insight-stat">' +
|
||||
'<span class="insight-stat-value">' + patternCount + '</span>' +
|
||||
'<span class="insight-stat-label">' + (isZh() ? '模式' : 'Patterns') + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="insight-stat">' +
|
||||
'<span class="insight-stat-value">' + suggestionCount + '</span>' +
|
||||
'<span class="insight-stat-label">' + (isZh() ? '建议' : 'Suggestions') + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="insight-stat">' +
|
||||
'<span class="insight-stat-value">' + (insight.prompt_count || 0) + '</span>' +
|
||||
'<span class="insight-stat-label">' + (isZh() ? '提示' : 'Prompts') + '</span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
(insight.patterns && insight.patterns.length > 0 ?
|
||||
'<div class="insight-card-preview">' +
|
||||
'<div class="pattern-preview ' + (insight.patterns[0].severity || 'low') + '">' +
|
||||
'<span class="pattern-type">' + escapeHtml(insight.patterns[0].type || 'pattern') + '</span>' +
|
||||
'<span class="pattern-desc">' + escapeHtml((insight.patterns[0].description || '').substring(0, 60)) + '...</span>' +
|
||||
'</div>' +
|
||||
'</div>' : '') +
|
||||
'</div>';
|
||||
}).join('') +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function getPromptInsightSeverity(patterns) {
|
||||
if (!patterns || patterns.length === 0) return 'low';
|
||||
var hasHigh = patterns.some(function(p) { return p.severity === 'high'; });
|
||||
var hasMedium = patterns.some(function(p) { return p.severity === 'medium'; });
|
||||
return hasHigh ? 'high' : (hasMedium ? 'medium' : 'low');
|
||||
}
|
||||
|
||||
function getPromptToolIcon(tool) {
|
||||
switch(tool) {
|
||||
case 'gemini': return 'sparkles';
|
||||
case 'qwen': return 'bot';
|
||||
case 'codex': return 'code-2';
|
||||
default: return 'cpu';
|
||||
}
|
||||
}
|
||||
|
||||
function formatPromptTimestamp(timestamp) {
|
||||
if (!timestamp) return '';
|
||||
var date = new Date(timestamp);
|
||||
var now = new Date();
|
||||
var diff = now - date;
|
||||
var minutes = Math.floor(diff / 60000);
|
||||
var hours = Math.floor(diff / 3600000);
|
||||
var days = Math.floor(diff / 86400000);
|
||||
|
||||
if (minutes < 1) return isZh() ? '刚刚' : 'Just now';
|
||||
if (minutes < 60) return minutes + (isZh() ? ' 分钟前' : 'm ago');
|
||||
if (hours < 24) return hours + (isZh() ? ' 小时前' : 'h ago');
|
||||
if (days < 7) return days + (isZh() ? ' 天前' : 'd ago');
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
|
||||
async function showPromptInsightDetail(insightId) {
|
||||
try {
|
||||
var response = await fetch('/api/memory/insights/' + insightId);
|
||||
if (!response.ok) throw new Error('Failed to load insight detail');
|
||||
var data = await response.json();
|
||||
selectedPromptInsight = data.insight;
|
||||
renderPromptHistoryView();
|
||||
} catch (err) {
|
||||
console.error('Failed to load insight detail:', err);
|
||||
if (window.showToast) {
|
||||
showToast(isZh() ? '加载洞察详情失败' : 'Failed to load insight detail', 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function closePromptInsightDetail() {
|
||||
selectedPromptInsight = null;
|
||||
renderPromptHistoryView();
|
||||
}
|
||||
|
||||
function renderPromptInsightDetail(insight) {
|
||||
if (!insight) return '';
|
||||
|
||||
var html = '<div class="insight-detail">' +
|
||||
'<div class="insight-detail-header">' +
|
||||
'<h4><i data-lucide="lightbulb" class="w-4 h-4"></i> ' + (isZh() ? '洞察详情' : 'Insight Detail') + '</h4>' +
|
||||
'<button class="btn-icon" onclick="closePromptInsightDetail()" title="' + t('common.close') + '">' +
|
||||
'<i data-lucide="x" class="w-4 h-4"></i>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'<div class="insight-detail-meta">' +
|
||||
'<span><i data-lucide="' + getPromptToolIcon(insight.tool) + '" class="w-3 h-3"></i> ' + (insight.tool || 'CLI') + '</span>' +
|
||||
'<span><i data-lucide="clock" class="w-3 h-3"></i> ' + formatPromptTimestamp(insight.created_at) + '</span>' +
|
||||
'<span><i data-lucide="file-text" class="w-3 h-3"></i> ' + (insight.prompt_count || 0) + ' ' + (isZh() ? '个提示已分析' : 'prompts analyzed') + '</span>' +
|
||||
'</div>';
|
||||
|
||||
// Patterns
|
||||
if (insight.patterns && insight.patterns.length > 0) {
|
||||
html += '<div class="insight-patterns">' +
|
||||
'<h5><i data-lucide="alert-triangle" class="w-3.5 h-3.5"></i> ' + (isZh() ? '发现的模式' : 'Patterns Found') + ' (' + insight.patterns.length + ')</h5>' +
|
||||
'<div class="patterns-list">' +
|
||||
insight.patterns.map(function(p) {
|
||||
return '<div class="pattern-item ' + (p.severity || 'low') + '">' +
|
||||
'<div class="pattern-header">' +
|
||||
'<span class="pattern-type-badge">' + escapeHtml(p.type || 'pattern') + '</span>' +
|
||||
'<span class="pattern-severity">' + (p.severity || 'low') + '</span>' +
|
||||
(p.occurrences ? '<span class="pattern-occurrences">' + p.occurrences + 'x</span>' : '') +
|
||||
'</div>' +
|
||||
'<div class="pattern-description">' + escapeHtml(p.description || '') + '</div>' +
|
||||
(p.suggestion ? '<div class="pattern-suggestion"><i data-lucide="arrow-right" class="w-3 h-3"></i> ' + escapeHtml(p.suggestion) + '</div>' : '') +
|
||||
'</div>';
|
||||
}).join('') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
// Suggestions
|
||||
if (insight.suggestions && insight.suggestions.length > 0) {
|
||||
html += '<div class="insight-suggestions">' +
|
||||
'<h5><i data-lucide="lightbulb" class="w-3.5 h-3.5"></i> ' + (isZh() ? '提供的建议' : 'Suggestions') + ' (' + insight.suggestions.length + ')</h5>' +
|
||||
'<div class="suggestions-list">' +
|
||||
insight.suggestions.map(function(s) {
|
||||
return '<div class="suggestion-item">' +
|
||||
'<div class="suggestion-title">' + escapeHtml(s.title || '') + '</div>' +
|
||||
'<div class="suggestion-description">' + escapeHtml(s.description || '') + '</div>' +
|
||||
(s.example ? '<div class="suggestion-example"><code>' + escapeHtml(s.example) + '</code></div>' : '') +
|
||||
'</div>';
|
||||
}).join('') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
html += '<div class="insight-detail-actions">' +
|
||||
'<button class="btn btn-sm btn-danger" onclick="deletePromptInsight(\'' + insight.id + '\')">' +
|
||||
'<i data-lucide="trash-2" class="w-3.5 h-3.5"></i> ' + t('common.delete') +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
async function deletePromptInsight(insightId) {
|
||||
if (!confirm(isZh() ? '确定要删除这条洞察记录吗?' : 'Are you sure you want to delete this insight?')) return;
|
||||
|
||||
try {
|
||||
var response = await fetch('/api/memory/insights/' + insightId, { method: 'DELETE' });
|
||||
if (!response.ok) throw new Error('Failed to delete insight');
|
||||
|
||||
selectedPromptInsight = null;
|
||||
await loadPromptInsightsHistory();
|
||||
renderPromptHistoryView();
|
||||
|
||||
if (window.showToast) {
|
||||
showToast(isZh() ? '洞察已删除' : 'Insight deleted', 'success');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to delete insight:', err);
|
||||
if (window.showToast) {
|
||||
showToast(isZh() ? '删除洞察失败' : 'Failed to delete insight', 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderPatternCard(pattern) {
|
||||
var iconMap = {
|
||||
'vague': 'help-circle',
|
||||
|
||||
343
ccw/src/templates/dashboard-js/views/rules-manager.js
Normal file
343
ccw/src/templates/dashboard-js/views/rules-manager.js
Normal file
@@ -0,0 +1,343 @@
|
||||
// Rules Manager View
|
||||
// Manages Claude Code rules (.claude/rules/)
|
||||
|
||||
// ========== Rules State ==========
|
||||
var rulesData = {
|
||||
projectRules: [],
|
||||
userRules: []
|
||||
};
|
||||
var selectedRule = null;
|
||||
var rulesLoading = false;
|
||||
|
||||
// ========== Main Render Function ==========
|
||||
async function renderRulesManager() {
|
||||
const container = document.getElementById('mainContent');
|
||||
if (!container) return;
|
||||
|
||||
// Hide stats grid and search
|
||||
const statsGrid = document.getElementById('statsGrid');
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
if (statsGrid) statsGrid.style.display = 'none';
|
||||
if (searchInput) searchInput.parentElement.style.display = 'none';
|
||||
|
||||
// Show loading state
|
||||
container.innerHTML = '<div class="rules-manager loading">' +
|
||||
'<div class="loading-spinner"><i data-lucide="loader-2" class="w-8 h-8 animate-spin"></i></div>' +
|
||||
'<p>' + t('common.loading') + '</p>' +
|
||||
'</div>';
|
||||
|
||||
// Load rules data
|
||||
await loadRulesData();
|
||||
|
||||
// Render the main view
|
||||
renderRulesView();
|
||||
}
|
||||
|
||||
async function loadRulesData() {
|
||||
rulesLoading = true;
|
||||
try {
|
||||
const response = await fetch('/api/rules?path=' + encodeURIComponent(projectPath));
|
||||
if (!response.ok) throw new Error('Failed to load rules');
|
||||
const data = await response.json();
|
||||
rulesData = {
|
||||
projectRules: data.projectRules || [],
|
||||
userRules: data.userRules || []
|
||||
};
|
||||
// Update badge
|
||||
updateRulesBadge();
|
||||
} catch (err) {
|
||||
console.error('Failed to load rules:', err);
|
||||
rulesData = { projectRules: [], userRules: [] };
|
||||
} finally {
|
||||
rulesLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function updateRulesBadge() {
|
||||
const badge = document.getElementById('badgeRules');
|
||||
if (badge) {
|
||||
const total = rulesData.projectRules.length + rulesData.userRules.length;
|
||||
badge.textContent = total;
|
||||
}
|
||||
}
|
||||
|
||||
function renderRulesView() {
|
||||
const container = document.getElementById('mainContent');
|
||||
if (!container) return;
|
||||
|
||||
const projectRules = rulesData.projectRules || [];
|
||||
const userRules = rulesData.userRules || [];
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="rules-manager">
|
||||
<!-- Header -->
|
||||
<div class="rules-header mb-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-success/10 rounded-lg flex items-center justify-center">
|
||||
<i data-lucide="book-open" class="w-5 h-5 text-success"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-foreground">${t('rules.title')}</h2>
|
||||
<p class="text-sm text-muted-foreground">${t('rules.description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-2"
|
||||
onclick="openRuleCreateModal()">
|
||||
<i data-lucide="plus" class="w-4 h-4"></i>
|
||||
${t('rules.create')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Project Rules Section -->
|
||||
<div class="rules-section mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="folder" class="w-5 h-5 text-success"></i>
|
||||
<h3 class="text-lg font-semibold text-foreground">${t('rules.projectRules')}</h3>
|
||||
<span class="text-xs px-2 py-0.5 bg-success/10 text-success rounded-full">.claude/rules/</span>
|
||||
</div>
|
||||
<span class="text-sm text-muted-foreground">${projectRules.length} ${t('rules.rulesCount')}</span>
|
||||
</div>
|
||||
|
||||
${projectRules.length === 0 ? `
|
||||
<div class="rules-empty-state bg-card border border-border rounded-lg p-6 text-center">
|
||||
<div class="text-muted-foreground mb-3"><i data-lucide="book-open" class="w-10 h-10 mx-auto"></i></div>
|
||||
<p class="text-muted-foreground">${t('rules.noProjectRules')}</p>
|
||||
<p class="text-sm text-muted-foreground mt-1">${t('rules.createHint')}</p>
|
||||
</div>
|
||||
` : `
|
||||
<div class="rules-grid grid gap-3">
|
||||
${projectRules.map(rule => renderRuleCard(rule, 'project')).join('')}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
|
||||
<!-- User Rules Section -->
|
||||
<div class="rules-section mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="user" class="w-5 h-5 text-orange"></i>
|
||||
<h3 class="text-lg font-semibold text-foreground">${t('rules.userRules')}</h3>
|
||||
<span class="text-xs px-2 py-0.5 bg-orange/10 text-orange rounded-full">~/.claude/rules/</span>
|
||||
</div>
|
||||
<span class="text-sm text-muted-foreground">${userRules.length} ${t('rules.rulesCount')}</span>
|
||||
</div>
|
||||
|
||||
${userRules.length === 0 ? `
|
||||
<div class="rules-empty-state bg-card border border-border rounded-lg p-6 text-center">
|
||||
<div class="text-muted-foreground mb-3"><i data-lucide="user" class="w-10 h-10 mx-auto"></i></div>
|
||||
<p class="text-muted-foreground">${t('rules.noUserRules')}</p>
|
||||
<p class="text-sm text-muted-foreground mt-1">${t('rules.userRulesHint')}</p>
|
||||
</div>
|
||||
` : `
|
||||
<div class="rules-grid grid gap-3">
|
||||
${userRules.map(rule => renderRuleCard(rule, 'user')).join('')}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
|
||||
<!-- Rule Detail Panel -->
|
||||
${selectedRule ? renderRuleDetailPanel(selectedRule) : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Initialize Lucide icons
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
|
||||
function renderRuleCard(rule, location) {
|
||||
const hasPathCondition = rule.paths && rule.paths.length > 0;
|
||||
const isGlobal = !hasPathCondition;
|
||||
const locationIcon = location === 'project' ? 'folder' : 'user';
|
||||
const locationClass = location === 'project' ? 'text-success' : 'text-orange';
|
||||
const locationBg = location === 'project' ? 'bg-success/10' : 'bg-orange/10';
|
||||
|
||||
// Get preview of content (first 100 chars)
|
||||
const contentPreview = rule.content ? rule.content.substring(0, 100).replace(/\n/g, ' ') + (rule.content.length > 100 ? '...' : '') : '';
|
||||
|
||||
return `
|
||||
<div class="rule-card bg-card border border-border rounded-lg p-4 hover:shadow-md transition-all cursor-pointer"
|
||||
onclick="showRuleDetail('${escapeHtml(rule.name)}', '${location}')">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 ${locationBg} rounded-lg flex items-center justify-center">
|
||||
<i data-lucide="file-text" class="w-5 h-5 ${locationClass}"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-foreground">${escapeHtml(rule.name)}</h4>
|
||||
${rule.subdirectory ? `<span class="text-xs text-muted-foreground">${escapeHtml(rule.subdirectory)}/</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
${isGlobal ? `
|
||||
<span class="inline-flex items-center px-2 py-0.5 text-xs font-medium rounded-full bg-primary/10 text-primary">
|
||||
<i data-lucide="globe" class="w-3 h-3 mr-1"></i>
|
||||
global
|
||||
</span>
|
||||
` : `
|
||||
<span class="inline-flex items-center px-2 py-0.5 text-xs font-medium rounded-full bg-warning/10 text-warning">
|
||||
<i data-lucide="filter" class="w-3 h-3 mr-1"></i>
|
||||
conditional
|
||||
</span>
|
||||
`}
|
||||
<span class="inline-flex items-center px-2 py-0.5 text-xs font-medium rounded-full ${locationBg} ${locationClass}">
|
||||
<i data-lucide="${locationIcon}" class="w-3 h-3 mr-1"></i>
|
||||
${location}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${contentPreview ? `
|
||||
<p class="text-sm text-muted-foreground mb-3 line-clamp-2 font-mono">${escapeHtml(contentPreview)}</p>
|
||||
` : ''}
|
||||
|
||||
${hasPathCondition ? `
|
||||
<div class="flex items-center gap-2 text-xs text-muted-foreground mt-2">
|
||||
<i data-lucide="filter" class="w-3 h-3"></i>
|
||||
<span class="font-mono">${escapeHtml(rule.paths.join(', '))}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderRuleDetailPanel(rule) {
|
||||
const hasPathCondition = rule.paths && rule.paths.length > 0;
|
||||
|
||||
return `
|
||||
<div class="rule-detail-panel fixed top-0 right-0 w-1/2 max-w-xl h-full bg-card border-l border-border shadow-lg z-50 flex flex-col">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-border">
|
||||
<h3 class="text-lg font-semibold text-foreground">${escapeHtml(rule.name)}</h3>
|
||||
<button class="w-8 h-8 flex items-center justify-center text-xl text-muted-foreground hover:text-foreground hover:bg-hover rounded"
|
||||
onclick="closeRuleDetail()">×</button>
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto p-5">
|
||||
<div class="space-y-6">
|
||||
<!-- Type -->
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-foreground mb-2">${t('rules.typeLabel')}</h4>
|
||||
<div class="flex items-center gap-2">
|
||||
${hasPathCondition ? `
|
||||
<span class="inline-flex items-center px-3 py-1 text-sm font-medium rounded-lg bg-warning/10 text-warning">
|
||||
<i data-lucide="filter" class="w-4 h-4 mr-2"></i>
|
||||
${t('rules.conditional')}
|
||||
</span>
|
||||
` : `
|
||||
<span class="inline-flex items-center px-3 py-1 text-sm font-medium rounded-lg bg-primary/10 text-primary">
|
||||
<i data-lucide="globe" class="w-4 h-4 mr-2"></i>
|
||||
${t('rules.global')}
|
||||
</span>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Path Conditions -->
|
||||
${hasPathCondition ? `
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-foreground mb-2">${t('rules.pathConditions')}</h4>
|
||||
<div class="space-y-2">
|
||||
${rule.paths.map(path => `
|
||||
<div class="flex items-center gap-2 p-2 bg-muted/50 rounded-lg">
|
||||
<i data-lucide="file-code" class="w-4 h-4 text-muted-foreground"></i>
|
||||
<code class="text-sm font-mono text-foreground">${escapeHtml(path)}</code>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Content -->
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-foreground mb-2">${t('rules.content')}</h4>
|
||||
<div class="bg-muted rounded-lg p-4 max-h-96 overflow-y-auto">
|
||||
<pre class="text-sm font-mono text-foreground whitespace-pre-wrap">${escapeHtml(rule.content || '')}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Path -->
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-foreground mb-2">${t('rules.filePath')}</h4>
|
||||
<code class="block p-3 bg-muted rounded-lg text-xs font-mono text-muted-foreground break-all">${escapeHtml(rule.path)}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="px-5 py-4 border-t border-border flex justify-between">
|
||||
<button class="px-4 py-2 text-sm text-destructive hover:bg-destructive/10 rounded-lg transition-colors flex items-center gap-2"
|
||||
onclick="deleteRule('${escapeHtml(rule.name)}', '${rule.location}')">
|
||||
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
||||
${t('common.delete')}
|
||||
</button>
|
||||
<button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-2"
|
||||
onclick="editRule('${escapeHtml(rule.name)}', '${rule.location}')">
|
||||
<i data-lucide="edit" class="w-4 h-4"></i>
|
||||
${t('common.edit')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rule-detail-overlay fixed inset-0 bg-black/50 z-40" onclick="closeRuleDetail()"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
async function showRuleDetail(ruleName, location) {
|
||||
try {
|
||||
const response = await fetch('/api/rules/' + encodeURIComponent(ruleName) + '?location=' + location + '&path=' + encodeURIComponent(projectPath));
|
||||
if (!response.ok) throw new Error('Failed to load rule detail');
|
||||
const data = await response.json();
|
||||
selectedRule = data.rule;
|
||||
renderRulesView();
|
||||
} catch (err) {
|
||||
console.error('Failed to load rule detail:', err);
|
||||
if (window.showToast) {
|
||||
showToast(t('rules.loadError'), 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function closeRuleDetail() {
|
||||
selectedRule = null;
|
||||
renderRulesView();
|
||||
}
|
||||
|
||||
async function deleteRule(ruleName, location) {
|
||||
if (!confirm(t('rules.deleteConfirm', { name: ruleName }))) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/rules/' + encodeURIComponent(ruleName), {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ location, projectPath })
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to delete rule');
|
||||
|
||||
selectedRule = null;
|
||||
await loadRulesData();
|
||||
renderRulesView();
|
||||
|
||||
if (window.showToast) {
|
||||
showToast(t('rules.deleted'), 'success');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to delete rule:', err);
|
||||
if (window.showToast) {
|
||||
showToast(t('rules.deleteError'), 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function editRule(ruleName, location) {
|
||||
// Open edit modal (to be implemented with modal)
|
||||
if (window.showToast) {
|
||||
showToast(t('rules.editNotImplemented'), 'info');
|
||||
}
|
||||
}
|
||||
|
||||
function openRuleCreateModal() {
|
||||
// Open create modal (to be implemented with modal)
|
||||
if (window.showToast) {
|
||||
showToast(t('rules.createNotImplemented'), 'info');
|
||||
}
|
||||
}
|
||||
345
ccw/src/templates/dashboard-js/views/skills-manager.js
Normal file
345
ccw/src/templates/dashboard-js/views/skills-manager.js
Normal file
@@ -0,0 +1,345 @@
|
||||
// Skills Manager View
|
||||
// Manages Claude Code skills (.claude/skills/)
|
||||
|
||||
// ========== Skills State ==========
|
||||
var skillsData = {
|
||||
projectSkills: [],
|
||||
userSkills: []
|
||||
};
|
||||
var selectedSkill = null;
|
||||
var skillsLoading = false;
|
||||
|
||||
// ========== Main Render Function ==========
|
||||
async function renderSkillsManager() {
|
||||
const container = document.getElementById('mainContent');
|
||||
if (!container) return;
|
||||
|
||||
// Hide stats grid and search
|
||||
const statsGrid = document.getElementById('statsGrid');
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
if (statsGrid) statsGrid.style.display = 'none';
|
||||
if (searchInput) searchInput.parentElement.style.display = 'none';
|
||||
|
||||
// Show loading state
|
||||
container.innerHTML = '<div class="skills-manager loading">' +
|
||||
'<div class="loading-spinner"><i data-lucide="loader-2" class="w-8 h-8 animate-spin"></i></div>' +
|
||||
'<p>' + t('common.loading') + '</p>' +
|
||||
'</div>';
|
||||
|
||||
// Load skills data
|
||||
await loadSkillsData();
|
||||
|
||||
// Render the main view
|
||||
renderSkillsView();
|
||||
}
|
||||
|
||||
async function loadSkillsData() {
|
||||
skillsLoading = true;
|
||||
try {
|
||||
const response = await fetch('/api/skills?path=' + encodeURIComponent(projectPath));
|
||||
if (!response.ok) throw new Error('Failed to load skills');
|
||||
const data = await response.json();
|
||||
skillsData = {
|
||||
projectSkills: data.projectSkills || [],
|
||||
userSkills: data.userSkills || []
|
||||
};
|
||||
// Update badge
|
||||
updateSkillsBadge();
|
||||
} catch (err) {
|
||||
console.error('Failed to load skills:', err);
|
||||
skillsData = { projectSkills: [], userSkills: [] };
|
||||
} finally {
|
||||
skillsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function updateSkillsBadge() {
|
||||
const badge = document.getElementById('badgeSkills');
|
||||
if (badge) {
|
||||
const total = skillsData.projectSkills.length + skillsData.userSkills.length;
|
||||
badge.textContent = total;
|
||||
}
|
||||
}
|
||||
|
||||
function renderSkillsView() {
|
||||
const container = document.getElementById('mainContent');
|
||||
if (!container) return;
|
||||
|
||||
const projectSkills = skillsData.projectSkills || [];
|
||||
const userSkills = skillsData.userSkills || [];
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="skills-manager">
|
||||
<!-- Header -->
|
||||
<div class="skills-header mb-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-primary/10 rounded-lg flex items-center justify-center">
|
||||
<i data-lucide="sparkles" class="w-5 h-5 text-primary"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-foreground">${t('skills.title')}</h2>
|
||||
<p class="text-sm text-muted-foreground">${t('skills.description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-2"
|
||||
onclick="openSkillCreateModal()">
|
||||
<i data-lucide="plus" class="w-4 h-4"></i>
|
||||
${t('skills.create')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Project Skills Section -->
|
||||
<div class="skills-section mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="folder" class="w-5 h-5 text-primary"></i>
|
||||
<h3 class="text-lg font-semibold text-foreground">${t('skills.projectSkills')}</h3>
|
||||
<span class="text-xs px-2 py-0.5 bg-primary/10 text-primary rounded-full">.claude/skills/</span>
|
||||
</div>
|
||||
<span class="text-sm text-muted-foreground">${projectSkills.length} ${t('skills.skillsCount')}</span>
|
||||
</div>
|
||||
|
||||
${projectSkills.length === 0 ? `
|
||||
<div class="skills-empty-state bg-card border border-border rounded-lg p-6 text-center">
|
||||
<div class="text-muted-foreground mb-3"><i data-lucide="sparkles" class="w-10 h-10 mx-auto"></i></div>
|
||||
<p class="text-muted-foreground">${t('skills.noProjectSkills')}</p>
|
||||
<p class="text-sm text-muted-foreground mt-1">${t('skills.createHint')}</p>
|
||||
</div>
|
||||
` : `
|
||||
<div class="skills-grid grid gap-3">
|
||||
${projectSkills.map(skill => renderSkillCard(skill, 'project')).join('')}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
|
||||
<!-- User Skills Section -->
|
||||
<div class="skills-section mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="user" class="w-5 h-5 text-indigo"></i>
|
||||
<h3 class="text-lg font-semibold text-foreground">${t('skills.userSkills')}</h3>
|
||||
<span class="text-xs px-2 py-0.5 bg-indigo/10 text-indigo rounded-full">~/.claude/skills/</span>
|
||||
</div>
|
||||
<span class="text-sm text-muted-foreground">${userSkills.length} ${t('skills.skillsCount')}</span>
|
||||
</div>
|
||||
|
||||
${userSkills.length === 0 ? `
|
||||
<div class="skills-empty-state bg-card border border-border rounded-lg p-6 text-center">
|
||||
<div class="text-muted-foreground mb-3"><i data-lucide="user" class="w-10 h-10 mx-auto"></i></div>
|
||||
<p class="text-muted-foreground">${t('skills.noUserSkills')}</p>
|
||||
<p class="text-sm text-muted-foreground mt-1">${t('skills.userSkillsHint')}</p>
|
||||
</div>
|
||||
` : `
|
||||
<div class="skills-grid grid gap-3">
|
||||
${userSkills.map(skill => renderSkillCard(skill, 'user')).join('')}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
|
||||
<!-- Skill Detail Panel -->
|
||||
${selectedSkill ? renderSkillDetailPanel(selectedSkill) : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Initialize Lucide icons
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
|
||||
function renderSkillCard(skill, location) {
|
||||
const hasAllowedTools = skill.allowedTools && skill.allowedTools.length > 0;
|
||||
const hasSupportingFiles = skill.supportingFiles && skill.supportingFiles.length > 0;
|
||||
const locationIcon = location === 'project' ? 'folder' : 'user';
|
||||
const locationClass = location === 'project' ? 'text-primary' : 'text-indigo';
|
||||
const locationBg = location === 'project' ? 'bg-primary/10' : 'bg-indigo/10';
|
||||
|
||||
return `
|
||||
<div class="skill-card bg-card border border-border rounded-lg p-4 hover:shadow-md transition-all cursor-pointer"
|
||||
onclick="showSkillDetail('${escapeHtml(skill.name)}', '${location}')">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 ${locationBg} rounded-lg flex items-center justify-center">
|
||||
<i data-lucide="sparkles" class="w-5 h-5 ${locationClass}"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-foreground">${escapeHtml(skill.name)}</h4>
|
||||
${skill.version ? `<span class="text-xs text-muted-foreground">v${escapeHtml(skill.version)}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="inline-flex items-center px-2 py-0.5 text-xs font-medium rounded-full ${locationBg} ${locationClass}">
|
||||
<i data-lucide="${locationIcon}" class="w-3 h-3 mr-1"></i>
|
||||
${location}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-muted-foreground mb-3 line-clamp-2">${escapeHtml(skill.description || t('skills.noDescription'))}</p>
|
||||
|
||||
<div class="flex items-center gap-3 text-xs text-muted-foreground">
|
||||
${hasAllowedTools ? `
|
||||
<span class="flex items-center gap-1">
|
||||
<i data-lucide="lock" class="w-3 h-3"></i>
|
||||
${skill.allowedTools.length} ${t('skills.tools')}
|
||||
</span>
|
||||
` : ''}
|
||||
${hasSupportingFiles ? `
|
||||
<span class="flex items-center gap-1">
|
||||
<i data-lucide="file-text" class="w-3 h-3"></i>
|
||||
${skill.supportingFiles.length} ${t('skills.files')}
|
||||
</span>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderSkillDetailPanel(skill) {
|
||||
const hasAllowedTools = skill.allowedTools && skill.allowedTools.length > 0;
|
||||
const hasSupportingFiles = skill.supportingFiles && skill.supportingFiles.length > 0;
|
||||
|
||||
return `
|
||||
<div class="skill-detail-panel fixed top-0 right-0 w-1/2 max-w-xl h-full bg-card border-l border-border shadow-lg z-50 flex flex-col">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-border">
|
||||
<h3 class="text-lg font-semibold text-foreground">${escapeHtml(skill.name)}</h3>
|
||||
<button class="w-8 h-8 flex items-center justify-center text-xl text-muted-foreground hover:text-foreground hover:bg-hover rounded"
|
||||
onclick="closeSkillDetail()">×</button>
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto p-5">
|
||||
<div class="space-y-6">
|
||||
<!-- Description -->
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-foreground mb-2">${t('skills.descriptionLabel')}</h4>
|
||||
<p class="text-sm text-muted-foreground">${escapeHtml(skill.description || t('skills.noDescription'))}</p>
|
||||
</div>
|
||||
|
||||
<!-- Metadata -->
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-foreground mb-2">${t('skills.metadata')}</h4>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="bg-muted/50 rounded-lg p-3">
|
||||
<span class="text-xs text-muted-foreground">${t('skills.location')}</span>
|
||||
<p class="text-sm font-medium text-foreground">${escapeHtml(skill.location)}</p>
|
||||
</div>
|
||||
${skill.version ? `
|
||||
<div class="bg-muted/50 rounded-lg p-3">
|
||||
<span class="text-xs text-muted-foreground">${t('skills.version')}</span>
|
||||
<p class="text-sm font-medium text-foreground">${escapeHtml(skill.version)}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Allowed Tools -->
|
||||
${hasAllowedTools ? `
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-foreground mb-2">${t('skills.allowedTools')}</h4>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
${skill.allowedTools.map(tool => `
|
||||
<span class="px-2 py-1 text-xs bg-muted rounded-lg font-mono">${escapeHtml(tool)}</span>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Supporting Files -->
|
||||
${hasSupportingFiles ? `
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-foreground mb-2">${t('skills.supportingFiles')}</h4>
|
||||
<div class="space-y-2">
|
||||
${skill.supportingFiles.map(file => `
|
||||
<div class="flex items-center gap-2 p-2 bg-muted/50 rounded-lg">
|
||||
<i data-lucide="file-text" class="w-4 h-4 text-muted-foreground"></i>
|
||||
<span class="text-sm font-mono text-foreground">${escapeHtml(file)}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Path -->
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-foreground mb-2">${t('skills.path')}</h4>
|
||||
<code class="block p-3 bg-muted rounded-lg text-xs font-mono text-muted-foreground break-all">${escapeHtml(skill.path)}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="px-5 py-4 border-t border-border flex justify-between">
|
||||
<button class="px-4 py-2 text-sm text-destructive hover:bg-destructive/10 rounded-lg transition-colors flex items-center gap-2"
|
||||
onclick="deleteSkill('${escapeHtml(skill.name)}', '${skill.location}')">
|
||||
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
||||
${t('common.delete')}
|
||||
</button>
|
||||
<button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-2"
|
||||
onclick="editSkill('${escapeHtml(skill.name)}', '${skill.location}')">
|
||||
<i data-lucide="edit" class="w-4 h-4"></i>
|
||||
${t('common.edit')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="skill-detail-overlay fixed inset-0 bg-black/50 z-40" onclick="closeSkillDetail()"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
async function showSkillDetail(skillName, location) {
|
||||
try {
|
||||
const response = await fetch('/api/skills/' + encodeURIComponent(skillName) + '?location=' + location + '&path=' + encodeURIComponent(projectPath));
|
||||
if (!response.ok) throw new Error('Failed to load skill detail');
|
||||
const data = await response.json();
|
||||
selectedSkill = data.skill;
|
||||
renderSkillsView();
|
||||
} catch (err) {
|
||||
console.error('Failed to load skill detail:', err);
|
||||
if (window.showToast) {
|
||||
showToast(t('skills.loadError'), 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function closeSkillDetail() {
|
||||
selectedSkill = null;
|
||||
renderSkillsView();
|
||||
}
|
||||
|
||||
async function deleteSkill(skillName, location) {
|
||||
if (!confirm(t('skills.deleteConfirm', { name: skillName }))) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/skills/' + encodeURIComponent(skillName), {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ location, projectPath })
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to delete skill');
|
||||
|
||||
selectedSkill = null;
|
||||
await loadSkillsData();
|
||||
renderSkillsView();
|
||||
|
||||
if (window.showToast) {
|
||||
showToast(t('skills.deleted'), 'success');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to delete skill:', err);
|
||||
if (window.showToast) {
|
||||
showToast(t('skills.deleteError'), 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function editSkill(skillName, location) {
|
||||
// Open edit modal (to be implemented with modal)
|
||||
if (window.showToast) {
|
||||
showToast(t('skills.editNotImplemented'), 'info');
|
||||
}
|
||||
}
|
||||
|
||||
function openSkillCreateModal() {
|
||||
// Open create modal (to be implemented with modal)
|
||||
if (window.showToast) {
|
||||
showToast(t('skills.createNotImplemented'), 'info');
|
||||
}
|
||||
}
|
||||
@@ -424,6 +424,16 @@
|
||||
<i data-lucide="message-square" class="nav-icon"></i>
|
||||
<span class="nav-text flex-1" data-i18n="nav.promptHistory">Prompts</span>
|
||||
</li>
|
||||
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-view="skills-manager" data-tooltip="Skills Management">
|
||||
<i data-lucide="sparkles" class="nav-icon"></i>
|
||||
<span class="nav-text flex-1" data-i18n="nav.skills">Skills</span>
|
||||
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-hover text-muted-foreground" id="badgeSkills">0</span>
|
||||
</li>
|
||||
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-view="rules-manager" data-tooltip="Rules Management">
|
||||
<i data-lucide="book-open" class="nav-icon"></i>
|
||||
<span class="nav-text flex-1" data-i18n="nav.rules">Rules</span>
|
||||
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-hover text-muted-foreground" id="badgeRules">0</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user