claude-session-insights 0.4.0 → 0.4.1
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 +14 -8
- package/package.json +1 -1
- package/public/index.html +408 -76
- package/src/ai-analyze.js +222 -0
- package/src/server.js +24 -1
package/README.md
CHANGED
|
@@ -10,8 +10,8 @@ Think "Spotify Wrapped" for your Claude Code usage — scores, summaries, badges
|
|
|
10
10
|
- **Overall Summary** — natural-language assessment of your prompting habits with specific recommendations
|
|
11
11
|
- **Per-Session Summary** — each session gets a plain-English breakdown of what happened, what went well, and what could improve
|
|
12
12
|
- **Session Drill-down** — click any session to see the full conversation timeline with per-turn token counts, costs, tool calls, and prompt previews
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
13
|
+
- **Session Intelligence** — unified card combining a rule-based overview (patterns, model choices, token habits) with on-demand **AI Analyze** powered by Claude, with model picker (Sonnet, Opus, Haiku) and streaming output
|
|
14
|
+
- **Workflow Optimizer** — analyzes session patterns and recommends Claude Code setup improvements: skills, CLAUDE.md files, agents, and plugins. Pattern-based suggestions load instantly; **AI Optimize** generates AI-enhanced suggestions (defaults to Opus). Optional AI artifact content generation produces ready-to-copy CLAUDE.md files, skill prompts, and agent configs
|
|
15
15
|
- **Heaviest Sessions** — top sessions ranked by cost for quick identification of expensive outliers
|
|
16
16
|
- **Daily Score Chart** — trend visualization of your efficiency score, session count, tokens, and cost over time
|
|
17
17
|
- **Badges** — positive achievements (Surgical Prompter, Cache Whisperer, etc.) and negative anti-patterns (Opus Addict, Token Furnace, etc.)
|
|
@@ -79,27 +79,33 @@ The dashboard generates rule-based summaries at two levels:
|
|
|
79
79
|
|
|
80
80
|
## Workflow Optimizer
|
|
81
81
|
|
|
82
|
-
Closes the loop from "how am I doing?" to "here's what to build to do better."
|
|
82
|
+
Closes the loop from "how am I doing?" to "here's what to build to do better."
|
|
83
83
|
|
|
84
|
-
**
|
|
84
|
+
**Pattern-based suggestions (instant):** Detects patterns from your badges, tool usage, session titles, and project history to recommend:
|
|
85
85
|
|
|
86
86
|
- **Skills** — slash commands to create in `~/.claude/skills/`. Example: if you have the Vague Commander badge, it suggests a `/spec` skill to help you write thorough task specs before Claude starts working
|
|
87
87
|
- **CLAUDE.md** — project-scoped or global config files to reduce repeated explanations. Triggered by projects with 5+ sessions, the Context Hoarder badge, Opus overuse, etc.
|
|
88
88
|
- **Agents** — subagent configurations for offloading exploration or repetitive task types
|
|
89
89
|
- **Plugins** — MCP servers only where CLI tools have a genuine capability gap (e.g. Playwright for browser tasks, not tools with good CLI equivalents that would just add context overhead)
|
|
90
90
|
|
|
91
|
-
**
|
|
91
|
+
**AI Optimize (optional):** Click "AI Optimize" to stream AI-generated suggestions that either replace or supplement the pattern-based ones — defaults to Opus. AI suggestions appear at the top of the card when generated and persist across refreshes until cleared.
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
**Artifact content generation (optional):** Click "Generate artifact content with AI" on any suggestion card to stream ready-to-copy content — the actual CLAUDE.md file, the real skill prompt body, agent configuration — not just a description of what to build.
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
## Session Intelligence
|
|
96
|
+
|
|
97
|
+
The Session Intelligence card combines a rule-based overview of your prompting habits with on-demand AI analysis.
|
|
98
|
+
|
|
99
|
+
The static overview surfaces patterns across your sessions: cache efficiency, model selection, prompt specificity, and cost distribution.
|
|
100
|
+
|
|
101
|
+
Click **AI Analyze** to run a deeper analysis using the Claude CLI. This streams a response via SSE that covers:
|
|
96
102
|
|
|
97
103
|
- **Key Patterns** — non-obvious trends the static rules miss (time-of-day patterns, project-specific habits, cost trajectories)
|
|
98
104
|
- **Biggest Opportunities** — specific workflow changes with quantified potential savings
|
|
99
105
|
- **What's Working Well** — habits worth keeping
|
|
100
106
|
- **Standout Session** — the most interesting session and what can be learned from it
|
|
101
107
|
|
|
102
|
-
You can pick which model to use (Sonnet, Opus, or Haiku) from the model picker.
|
|
108
|
+
AI analysis appears at the top of the card when generated and persists across refreshes until cleared. You can pick which model to use (Sonnet, Opus, or Haiku) from the model picker. Requires the `claude` CLI to be installed and in your PATH.
|
|
103
109
|
|
|
104
110
|
## Badges
|
|
105
111
|
|
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -163,13 +163,13 @@
|
|
|
163
163
|
background: var(--surface3); color: var(--text); border-color: var(--border-hover);
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
/* Hero:
|
|
167
|
-
.hero {
|
|
168
|
-
.hero-left { display: flex; flex-direction:
|
|
169
|
-
.hero-
|
|
166
|
+
/* Hero: single row — score card fixed-width, badges fill the rest */
|
|
167
|
+
.hero { margin-bottom: 28px; }
|
|
168
|
+
.hero-left { display: flex; flex-direction: row; gap: 14px; align-items: stretch; }
|
|
169
|
+
.hero-left .score-card { width: 230px; flex-shrink: 0; }
|
|
170
170
|
@media (max-width: 720px) {
|
|
171
|
-
.hero { flex-direction: column; }
|
|
172
|
-
.hero-left { width: auto;
|
|
171
|
+
.hero-left { flex-direction: column; }
|
|
172
|
+
.hero-left .score-card { width: auto; }
|
|
173
173
|
}
|
|
174
174
|
.score-card {
|
|
175
175
|
background: var(--surface); border: 1px solid var(--border);
|
|
@@ -320,7 +320,7 @@
|
|
|
320
320
|
|
|
321
321
|
/* Score breakdown */
|
|
322
322
|
.score-info-toggle {
|
|
323
|
-
background: none; border: none; color: var(--text3); cursor: pointer;
|
|
323
|
+
display: block; background: none; border: none; color: var(--text3); cursor: pointer;
|
|
324
324
|
font-size: 11px; margin-top: 10px; padding: 0; transition: color 0.15s;
|
|
325
325
|
}
|
|
326
326
|
.score-info-toggle:hover { color: var(--accent); }
|
|
@@ -526,8 +526,27 @@
|
|
|
526
526
|
.ai-insights-card h3 {
|
|
527
527
|
font-size: 11px; text-transform: uppercase; letter-spacing: 0.8px;
|
|
528
528
|
color: var(--purple); margin-bottom: 14px; font-weight: 600;
|
|
529
|
-
display: flex; align-items: center; gap: 8px;
|
|
529
|
+
display: flex; align-items: center; gap: 8px; justify-content: space-between;
|
|
530
|
+
}
|
|
531
|
+
.card-header-actions {
|
|
532
|
+
display: flex; align-items: center; gap: 8px; flex-shrink: 0;
|
|
533
|
+
}
|
|
534
|
+
.ai-generate-btn-sm {
|
|
535
|
+
background: var(--purple-dim); border: 1px solid rgba(210,168,255,0.2);
|
|
536
|
+
color: var(--purple); padding: 5px 12px; border-radius: 7px;
|
|
537
|
+
cursor: pointer; font-size: 11px; font-weight: 600; transition: all 0.2s;
|
|
538
|
+
text-transform: none; letter-spacing: 0;
|
|
539
|
+
}
|
|
540
|
+
.ai-generate-btn-sm:hover { background: rgba(210,168,255,0.2); border-color: rgba(210,168,255,0.3); }
|
|
541
|
+
.ai-generate-btn-sm:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
542
|
+
.optimizer-generate-btn-sm {
|
|
543
|
+
background: var(--accent-dim); border: 1px solid rgba(37,99,235,0.2);
|
|
544
|
+
color: var(--accent); padding: 5px 12px; border-radius: 7px;
|
|
545
|
+
cursor: pointer; font-size: 11px; font-weight: 600; transition: all 0.2s;
|
|
546
|
+
text-transform: none; letter-spacing: 0;
|
|
530
547
|
}
|
|
548
|
+
.optimizer-generate-btn-sm:hover { background: rgba(37,99,235,0.15); border-color: rgba(37,99,235,0.3); }
|
|
549
|
+
.optimizer-generate-btn-sm:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
531
550
|
.ai-insights-content { font-size: 13px; line-height: 1.75; color: var(--text); }
|
|
532
551
|
.ai-insights-content h2 {
|
|
533
552
|
font-size: 13px; font-weight: 700; color: var(--text); margin: 16px 0 8px 0;
|
|
@@ -541,6 +560,21 @@
|
|
|
541
560
|
font-family: var(--mono); font-size: 11px; background: var(--surface2);
|
|
542
561
|
padding: 1px 5px; border-radius: 3px;
|
|
543
562
|
}
|
|
563
|
+
/* Session Intelligence — merged static + AI card */
|
|
564
|
+
.session-section-label {
|
|
565
|
+
font-size: 10px; text-transform: uppercase; letter-spacing: 0.7px;
|
|
566
|
+
color: var(--text3); font-weight: 600; margin-bottom: 12px;
|
|
567
|
+
}
|
|
568
|
+
.ai-divider {
|
|
569
|
+
display: flex; align-items: center; gap: 10px;
|
|
570
|
+
margin: 22px 0 18px; color: var(--purple);
|
|
571
|
+
font-size: 10px; text-transform: uppercase; letter-spacing: 0.8px; font-weight: 600;
|
|
572
|
+
}
|
|
573
|
+
.ai-divider::before, .ai-divider::after {
|
|
574
|
+
content: ''; flex: 1; height: 1px;
|
|
575
|
+
background: linear-gradient(90deg, transparent, rgba(210,168,255,0.35), transparent);
|
|
576
|
+
}
|
|
577
|
+
.ai-divider span { white-space: nowrap; }
|
|
544
578
|
.ai-generate-btn {
|
|
545
579
|
background: var(--purple-dim); border: 1px solid rgba(210,168,255,0.2);
|
|
546
580
|
color: var(--purple); padding: 10px 20px; border-radius: 7px;
|
|
@@ -569,7 +603,7 @@
|
|
|
569
603
|
animation: ai-spin 0.8s linear infinite;
|
|
570
604
|
}
|
|
571
605
|
@keyframes ai-spin { to { transform: rotate(360deg); } }
|
|
572
|
-
.ai-controls { display: flex; align-items: center; gap: 10px; }
|
|
606
|
+
.ai-controls { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
|
573
607
|
.ai-model-picker { position: relative; display: inline-block; }
|
|
574
608
|
.ai-model-pill {
|
|
575
609
|
display: inline-flex; align-items: center; gap: 5px;
|
|
@@ -607,12 +641,22 @@
|
|
|
607
641
|
.ai-model-option:hover { background: var(--surface2); }
|
|
608
642
|
.ai-model-option.active { color: var(--text); font-weight: 600; }
|
|
609
643
|
.ai-model-option .check { width: 14px; color: var(--purple); font-size: 13px; }
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
644
|
+
[data-warn] { position: relative; }
|
|
645
|
+
[data-warn]:hover::before {
|
|
646
|
+
content: attr(data-warn);
|
|
647
|
+
position: absolute; bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%);
|
|
648
|
+
background: var(--surface); border: 1px solid rgba(255,200,80,0.3);
|
|
649
|
+
color: var(--text2); font-size: 11px; font-weight: 400;
|
|
650
|
+
padding: 7px 10px; border-radius: 6px; pointer-events: none; z-index: 300;
|
|
651
|
+
white-space: normal; max-width: 260px; width: max-content; line-height: 1.5;
|
|
652
|
+
box-shadow: 0 4px 14px rgba(0,0,0,0.15);
|
|
653
|
+
}
|
|
654
|
+
[data-warn]:hover::after {
|
|
655
|
+
content: '';
|
|
656
|
+
position: absolute; bottom: calc(100% + 3px); left: 50%; transform: translateX(-50%);
|
|
657
|
+
border: 5px solid transparent; border-top-color: rgba(255,200,80,0.3);
|
|
658
|
+
pointer-events: none; z-index: 300;
|
|
614
659
|
}
|
|
615
|
-
.ai-token-warning .warn-icon { color: #f5c842; flex-shrink: 0; margin-top: 1px; }
|
|
616
660
|
.ai-model-label {
|
|
617
661
|
font-size: 10px; color: var(--text3); font-family: var(--mono);
|
|
618
662
|
display: flex; align-items: center; gap: 6px;
|
|
@@ -645,6 +689,71 @@
|
|
|
645
689
|
}
|
|
646
690
|
.ai-prompt-close:hover { color: var(--text); }
|
|
647
691
|
|
|
692
|
+
/* Workflow Optimizer — mode toggle (Static / AI Suggestions) */
|
|
693
|
+
.optimizer-mode-tabs {
|
|
694
|
+
display: flex; gap: 4px; margin-bottom: 16px;
|
|
695
|
+
}
|
|
696
|
+
.optimizer-mode-tab {
|
|
697
|
+
padding: 6px 14px; border-radius: 7px; border: 1px solid var(--border);
|
|
698
|
+
background: var(--surface2); color: var(--text2); font-size: 12px;
|
|
699
|
+
font-weight: 500; cursor: pointer; transition: all 0.15s;
|
|
700
|
+
}
|
|
701
|
+
.optimizer-mode-tab:hover { background: var(--surface3); color: var(--text); }
|
|
702
|
+
.optimizer-mode-tab.active {
|
|
703
|
+
background: var(--accent-dim); border-color: rgba(37,99,235,0.3);
|
|
704
|
+
color: var(--accent); font-weight: 600;
|
|
705
|
+
}
|
|
706
|
+
.optimizer-mode-panel { display: none; }
|
|
707
|
+
.optimizer-mode-panel.active { display: block; }
|
|
708
|
+
.optimizer-ai-panel-header {
|
|
709
|
+
display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 16px;
|
|
710
|
+
}
|
|
711
|
+
.optimizer-ai-suggestions-output { margin-top: 4px; }
|
|
712
|
+
.ai-optimizer-model-picker { position: relative; display: inline-block; }
|
|
713
|
+
.ai-optimizer-model-pill {
|
|
714
|
+
display: inline-flex; align-items: center; gap: 5px;
|
|
715
|
+
background: var(--surface2); border: 1px solid var(--border); color: var(--text3);
|
|
716
|
+
padding: 5px 10px; border-radius: 20px; font-size: 11px; font-family: var(--mono);
|
|
717
|
+
cursor: pointer; transition: all 0.2s; user-select: none; position: relative;
|
|
718
|
+
}
|
|
719
|
+
.ai-optimizer-model-pill:hover { border-color: var(--border-hover); color: var(--text2); }
|
|
720
|
+
.ai-optimizer-model-pill[data-tip]:hover::after {
|
|
721
|
+
content: attr(data-tip);
|
|
722
|
+
position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%);
|
|
723
|
+
background: var(--text); color: var(--bg); font-size: 10px; white-space: nowrap;
|
|
724
|
+
padding: 4px 8px; border-radius: 4px; pointer-events: none; z-index: 200;
|
|
725
|
+
}
|
|
726
|
+
.ai-optimizer-model-picker.open .pill-chevron { transform: rotate(180deg); }
|
|
727
|
+
.ai-optimizer-model-menu {
|
|
728
|
+
position: absolute; top: calc(100% + 4px); left: 0; z-index: 200;
|
|
729
|
+
background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
|
|
730
|
+
padding: 4px; min-width: 140px; box-shadow: var(--shadow-tooltip);
|
|
731
|
+
opacity: 0; transform: translateY(-4px); pointer-events: none;
|
|
732
|
+
transition: opacity 0.15s, transform 0.15s;
|
|
733
|
+
}
|
|
734
|
+
.ai-optimizer-model-picker.open .ai-optimizer-model-menu {
|
|
735
|
+
opacity: 1; transform: translateY(0); pointer-events: auto;
|
|
736
|
+
}
|
|
737
|
+
.ai-optimizer-model-option {
|
|
738
|
+
display: flex; align-items: center; gap: 8px;
|
|
739
|
+
padding: 6px 10px; border-radius: 5px; font-size: 11px; font-family: var(--mono);
|
|
740
|
+
color: var(--text2); cursor: pointer; transition: background 0.1s;
|
|
741
|
+
}
|
|
742
|
+
.ai-optimizer-model-option:hover { background: var(--surface2); }
|
|
743
|
+
.ai-optimizer-model-option.active { color: var(--text); font-weight: 600; }
|
|
744
|
+
.ai-optimizer-model-option .check { width: 14px; color: var(--purple); font-size: 13px; }
|
|
745
|
+
.ai-suggestions-type-group { margin-bottom: 18px; }
|
|
746
|
+
.ai-suggestions-type-group:last-child { margin-bottom: 0; }
|
|
747
|
+
.ai-suggestions-type-label {
|
|
748
|
+
font-size: 10px; text-transform: uppercase; letter-spacing: 0.7px;
|
|
749
|
+
color: var(--text3); font-weight: 600; margin-bottom: 8px;
|
|
750
|
+
}
|
|
751
|
+
.ai-suggestion-badge {
|
|
752
|
+
display: inline-flex; align-items: center; gap: 4px; font-size: 9px;
|
|
753
|
+
font-weight: 600; padding: 1px 6px; border-radius: 4px; margin-left: 6px;
|
|
754
|
+
background: rgba(37,99,235,0.1); color: var(--accent); vertical-align: middle;
|
|
755
|
+
}
|
|
756
|
+
|
|
648
757
|
/* Workflow Optimizer */
|
|
649
758
|
.optimizer-card {
|
|
650
759
|
background: var(--surface); border: 1px solid var(--border);
|
|
@@ -659,7 +768,7 @@
|
|
|
659
768
|
.optimizer-card h3 {
|
|
660
769
|
font-size: 11px; text-transform: uppercase; letter-spacing: 0.8px;
|
|
661
770
|
color: var(--accent); margin-bottom: 14px; font-weight: 600;
|
|
662
|
-
display: flex; align-items: center; gap: 8px;
|
|
771
|
+
display: flex; align-items: center; gap: 8px; justify-content: space-between;
|
|
663
772
|
}
|
|
664
773
|
.optimizer-tabs {
|
|
665
774
|
display: flex; gap: 4px; margin-bottom: 16px; flex-wrap: wrap;
|
|
@@ -1228,12 +1337,19 @@ function renderDashboard(data) {
|
|
|
1228
1337
|
: '<span style="color: var(--text3); font-size: 13px">Keep going -- badges unlock after 5+ sessions</span>'}
|
|
1229
1338
|
</div>
|
|
1230
1339
|
</div>
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1340
|
+
</div>
|
|
1341
|
+
<section id="session-insights-section">
|
|
1342
|
+
<div class="ai-insights-card">
|
|
1343
|
+
<h3>Session Intelligence
|
|
1344
|
+
<span class="card-header-actions">
|
|
1345
|
+
<span class="ai-model-picker" id="ai-model-picker"><span class="ai-model-pill" onclick="toggleModelMenu(event)" data-tip="Click to switch model"></span><div class="ai-model-menu"></div></span>
|
|
1346
|
+
<button class="ai-generate-btn-sm" id="ai-gen-header-btn" onclick="generateAIInsights()" data-warn="⚠ This runs a real Claude session and will show up in your own usage stats. Each generation costs roughly the same as a short coding task.">AI Analyze</button>
|
|
1347
|
+
</span>
|
|
1348
|
+
</h3>
|
|
1349
|
+
<div id="ai-insights-body"></div>
|
|
1350
|
+
${data.overallSummary ? `
|
|
1351
|
+
<div class="ai-divider"><span>Overview</span></div>
|
|
1352
|
+
<div class="session-static-section">
|
|
1237
1353
|
<div class="summary-paragraphs">
|
|
1238
1354
|
${data.overallSummary.paragraphs.map(p => `<p>${escHtml(p)}</p>`).join('')}
|
|
1239
1355
|
</div>
|
|
@@ -1242,28 +1358,12 @@ function renderDashboard(data) {
|
|
|
1242
1358
|
<h4>Recommendations</h4>
|
|
1243
1359
|
<ul>${data.overallSummary.recommendations.map(r => `<li>${escHtml(r)}</li>`).join('')}</ul>
|
|
1244
1360
|
</div>` : ''}
|
|
1245
|
-
</div
|
|
1246
|
-
</div>` : ''}
|
|
1247
|
-
</div>
|
|
1248
|
-
<section id="ai-insights-section">
|
|
1249
|
-
<div class="ai-insights-card">
|
|
1250
|
-
<h3>AI Insights</h3>
|
|
1251
|
-
<div id="ai-insights-body">
|
|
1252
|
-
<div class="ai-controls">
|
|
1253
|
-
<button class="ai-generate-btn" onclick="generateAIInsights()">Generate AI Insights</button>
|
|
1254
|
-
<span class="ai-model-picker" id="ai-model-picker"><span class="ai-model-pill" onclick="toggleModelMenu(event)" data-tip="Click to switch model"></span><div class="ai-model-menu"></div></span>
|
|
1255
|
-
</div>
|
|
1256
|
-
<button class="score-info-toggle" onclick="togglePromptView()">What prompt is it sending to Claude?</button>
|
|
1257
|
-
<div class="ai-token-warning">
|
|
1258
|
-
<span class="warn-icon">⚠</span>
|
|
1259
|
-
<span>This runs a real Claude session and will show up in your own usage stats. Each generation costs roughly the same as a short coding task.</span>
|
|
1260
|
-
</div>
|
|
1261
|
-
</div>
|
|
1361
|
+
</div>` : ''}
|
|
1262
1362
|
</div>
|
|
1263
1363
|
</section>
|
|
1264
1364
|
<section id="optimizer-section">
|
|
1265
1365
|
<div class="optimizer-card">
|
|
1266
|
-
<h3>Workflow Optimizer
|
|
1366
|
+
<h3>Workflow Optimizer<span id="optimizer-header-controls"></span></h3>
|
|
1267
1367
|
<div id="optimizer-body">
|
|
1268
1368
|
<div style="color:var(--text3); font-size:13px; padding: 4px 0 12px;">Loading suggestions<span id="optimizer-dots"></span></div>
|
|
1269
1369
|
</div>
|
|
@@ -1638,6 +1738,7 @@ function selectModel(modelId) {
|
|
|
1638
1738
|
|
|
1639
1739
|
document.addEventListener('click', () => {
|
|
1640
1740
|
document.querySelectorAll('.ai-model-picker.open').forEach(p => p.classList.remove('open'));
|
|
1741
|
+
document.querySelectorAll('.ai-optimizer-model-picker.open').forEach(p => p.classList.remove('open'));
|
|
1641
1742
|
});
|
|
1642
1743
|
|
|
1643
1744
|
function applyAIState() {
|
|
@@ -1646,23 +1747,25 @@ function applyAIState() {
|
|
|
1646
1747
|
} else {
|
|
1647
1748
|
renderModelPicker();
|
|
1648
1749
|
}
|
|
1750
|
+
if (optimizerSuggestions) renderOptimizerTabs();
|
|
1649
1751
|
}
|
|
1650
1752
|
|
|
1651
1753
|
function renderAIComplete(content, generatedAt, model) {
|
|
1652
1754
|
cachedAIRender = { content, generatedAt, model };
|
|
1653
1755
|
const body = document.getElementById('ai-insights-body');
|
|
1654
1756
|
if (!body) return;
|
|
1757
|
+
const headerBtn = document.getElementById('ai-gen-header-btn');
|
|
1758
|
+
if (headerBtn) { headerBtn.textContent = 'AI Analyze'; headerBtn.disabled = false; }
|
|
1655
1759
|
body.innerHTML = `
|
|
1760
|
+
<div class="ai-divider" style="margin-top:0"><span>AI Analysis</span></div>
|
|
1656
1761
|
<div class="ai-insights-content">${renderMarkdown(escHtml(content))}</div>
|
|
1657
1762
|
<div class="ai-timestamp">
|
|
1658
1763
|
Generated ${new Date(generatedAt).toLocaleString()}
|
|
1659
1764
|
${model ? ` · ${modelLabel(model)}` : ''}
|
|
1660
|
-
<div style="margin-top:
|
|
1661
|
-
<button class="ai-generate-btn" style="padding:
|
|
1662
|
-
<button class="ai-generate-btn" style="padding: 5px 14px; font-size: 11px; background: var(--surface2); color: var(--text2);" onclick="clearAIInsights()">Clear</button>
|
|
1663
|
-
<span class="ai-model-picker" id="ai-model-picker"><span class="ai-model-pill" onclick="toggleModelMenu(event)" data-tip="Click to switch model"></span><div class="ai-model-menu"></div></span>
|
|
1765
|
+
<div style="margin-top:8px" class="ai-controls">
|
|
1766
|
+
<button class="ai-generate-btn" style="padding:5px 14px;font-size:11px;background:var(--surface2);border-color:var(--border);color:var(--text2)" onclick="clearAIInsights()">Clear</button>
|
|
1664
1767
|
</div>
|
|
1665
|
-
<button class="score-info-toggle" onclick="togglePromptView()">What prompt is it sending to Claude?</button>
|
|
1768
|
+
<div><button class="score-info-toggle" onclick="togglePromptView()">What prompt is it sending to Claude?</button></div>
|
|
1666
1769
|
</div>`;
|
|
1667
1770
|
renderModelPicker();
|
|
1668
1771
|
}
|
|
@@ -1672,16 +1775,9 @@ function clearAIInsights() {
|
|
|
1672
1775
|
fetch('/api/ai-analyze', { method: 'DELETE' }).catch(() => {});
|
|
1673
1776
|
const body = document.getElementById('ai-insights-body');
|
|
1674
1777
|
if (!body) return;
|
|
1675
|
-
body.innerHTML =
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
<span class="ai-model-picker" id="ai-model-picker"><span class="ai-model-pill" onclick="toggleModelMenu(event)" data-tip="Click to switch model"></span><div class="ai-model-menu"></div></span>
|
|
1679
|
-
</div>
|
|
1680
|
-
<button class="score-info-toggle" onclick="togglePromptView()">What prompt is it sending to Claude?</button>
|
|
1681
|
-
<div class="ai-token-warning">
|
|
1682
|
-
<span class="warn-icon">⚠</span>
|
|
1683
|
-
<span>This runs a real Claude session and will show up in your own usage stats. Each generation costs roughly the same as a short coding task.</span>
|
|
1684
|
-
</div>`;
|
|
1778
|
+
body.innerHTML = '';
|
|
1779
|
+
const headerBtn = document.getElementById('ai-gen-header-btn');
|
|
1780
|
+
if (headerBtn) { headerBtn.textContent = 'AI Analyze'; headerBtn.disabled = false; }
|
|
1685
1781
|
renderModelPicker();
|
|
1686
1782
|
}
|
|
1687
1783
|
|
|
@@ -1707,6 +1803,9 @@ function generateAIInsights() {
|
|
|
1707
1803
|
const body = document.getElementById('ai-insights-body');
|
|
1708
1804
|
if (!body) return;
|
|
1709
1805
|
|
|
1806
|
+
const headerBtn = document.getElementById('ai-gen-header-btn');
|
|
1807
|
+
if (headerBtn) { headerBtn.textContent = 'Generating…'; headerBtn.disabled = true; }
|
|
1808
|
+
|
|
1710
1809
|
const wasActive = autoRefreshActive;
|
|
1711
1810
|
if (autoRefreshActive) pauseAutoRefresh();
|
|
1712
1811
|
|
|
@@ -1714,11 +1813,11 @@ function generateAIInsights() {
|
|
|
1714
1813
|
const modelParam = modelId ? `?model=${encodeURIComponent(modelId)}` : '';
|
|
1715
1814
|
|
|
1716
1815
|
body.innerHTML = `
|
|
1816
|
+
<div class="ai-divider" style="margin-top:0"><span>AI Analysis</span></div>
|
|
1717
1817
|
<div class="ai-loading">
|
|
1718
1818
|
<div class="ai-spinner"></div>
|
|
1719
1819
|
<div>
|
|
1720
1820
|
<div>Generating with ${modelId ? modelLabel(modelId) : 'default model'}, please wait...</div>
|
|
1721
|
-
${wasActive ? '<div style="font-size:11px; color:var(--yellow); margin-top:4px">Auto-refresh paused while generating</div>' : ''}
|
|
1722
1821
|
</div>
|
|
1723
1822
|
</div>
|
|
1724
1823
|
<div class="ai-insights-content" id="ai-stream-content"></div>`;
|
|
@@ -1766,12 +1865,9 @@ async function streamAIFetch(modelParam, body) {
|
|
|
1766
1865
|
renderAIComplete(fullContent, generatedAt, usedModel);
|
|
1767
1866
|
} else if (currentEvent === 'error') {
|
|
1768
1867
|
const { message } = JSON.parse(data);
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
<span class="ai-model-picker" id="ai-model-picker"><span class="ai-model-pill" onclick="toggleModelMenu(event)" data-tip="Click to switch model"></span><div class="ai-model-menu"></div></span>
|
|
1773
|
-
</div>`;
|
|
1774
|
-
renderModelPicker();
|
|
1868
|
+
const headerBtn = document.getElementById('ai-gen-header-btn');
|
|
1869
|
+
if (headerBtn) { headerBtn.textContent = 'AI Analyze'; headerBtn.disabled = false; }
|
|
1870
|
+
body.innerHTML = `<div class="ai-error">${escHtml(message)}</div>`;
|
|
1775
1871
|
resumeAutoRefreshIfPaused();
|
|
1776
1872
|
}
|
|
1777
1873
|
currentEvent = '';
|
|
@@ -1780,12 +1876,9 @@ async function streamAIFetch(modelParam, body) {
|
|
|
1780
1876
|
}
|
|
1781
1877
|
resumeAutoRefreshIfPaused();
|
|
1782
1878
|
} catch (err) {
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
<span class="ai-model-picker" id="ai-model-picker"><span class="ai-model-pill" onclick="toggleModelMenu(event)" data-tip="Click to switch model"></span><div class="ai-model-menu"></div></span>
|
|
1787
|
-
</div>`;
|
|
1788
|
-
renderModelPicker();
|
|
1879
|
+
const headerBtnCatch = document.getElementById('ai-gen-header-btn');
|
|
1880
|
+
if (headerBtnCatch) { headerBtnCatch.textContent = 'AI Analyze'; headerBtnCatch.disabled = false; }
|
|
1881
|
+
body.innerHTML = `<div class="ai-error">Failed to connect to server</div>`;
|
|
1789
1882
|
resumeAutoRefreshIfPaused();
|
|
1790
1883
|
}
|
|
1791
1884
|
}
|
|
@@ -1812,6 +1905,9 @@ async function loadCachedAIInsights() {
|
|
|
1812
1905
|
let optimizerSuggestions = null;
|
|
1813
1906
|
let activeOptimizerTab = 'all';
|
|
1814
1907
|
let cachedOptimizerAI = null;
|
|
1908
|
+
let optimizerMode = 'static'; // 'static' | 'ai'
|
|
1909
|
+
let aiOptimizerModelId = 'claude-opus-4-6'; // default Opus for AI suggestions
|
|
1910
|
+
let cachedAIOptimizerSuggestions = null; // { suggestions, generatedAt, model }
|
|
1815
1911
|
|
|
1816
1912
|
const OPTIMIZER_TABS = [
|
|
1817
1913
|
{ id: 'all', label: 'All' },
|
|
@@ -1830,8 +1926,15 @@ async function loadSuggestions() {
|
|
|
1830
1926
|
}, 400);
|
|
1831
1927
|
|
|
1832
1928
|
try {
|
|
1833
|
-
const
|
|
1834
|
-
|
|
1929
|
+
const [suggestionsRes, aiCacheRes] = await Promise.all([
|
|
1930
|
+
fetch('/api/suggestions'),
|
|
1931
|
+
fetch('/api/ai-optimizer'),
|
|
1932
|
+
]);
|
|
1933
|
+
optimizerSuggestions = await suggestionsRes.json();
|
|
1934
|
+
const aiCache = await aiCacheRes.json();
|
|
1935
|
+
if (aiCache.suggestions) {
|
|
1936
|
+
cachedAIOptimizerSuggestions = aiCache;
|
|
1937
|
+
}
|
|
1835
1938
|
clearInterval(dotTimer);
|
|
1836
1939
|
renderOptimizerTabs();
|
|
1837
1940
|
} catch {
|
|
@@ -1875,7 +1978,7 @@ function renderOptimizerTabs() {
|
|
|
1875
1978
|
const aiSection = `
|
|
1876
1979
|
<div class="optimizer-ai-section">
|
|
1877
1980
|
<div class="optimizer-ai-controls">
|
|
1878
|
-
<button class="optimizer-generate-btn" onclick="generateOptimizerContent()" id="optimizer-gen-btn">
|
|
1981
|
+
<button class="optimizer-generate-btn" onclick="generateOptimizerContent()" id="optimizer-gen-btn" data-warn="⚠ This runs a real Claude session and will show up in your own usage stats. Each generation costs roughly the same as a short coding task.">
|
|
1879
1982
|
Generate artifact content with AI
|
|
1880
1983
|
</button>
|
|
1881
1984
|
<span class="ai-model-picker" id="optimizer-model-picker">
|
|
@@ -1886,15 +1989,42 @@ function renderOptimizerTabs() {
|
|
|
1886
1989
|
<div id="optimizer-ai-output"></div>
|
|
1887
1990
|
</div>`;
|
|
1888
1991
|
|
|
1992
|
+
// Build AI Advisor model picker for card header
|
|
1993
|
+
const opusModel = aiModels.find(m => m.id === 'claude-opus-4-6') || aiModels[0];
|
|
1994
|
+
const currentAIModel = aiModels.find(m => m.id === aiOptimizerModelId) || opusModel || aiModels[0];
|
|
1995
|
+
const aiModelMenuHtml = aiModels.map(m =>
|
|
1996
|
+
`<div class="ai-optimizer-model-option${m.id === aiOptimizerModelId ? ' active' : ''}" onclick="selectAIOptimizerModel('${m.id}')">` +
|
|
1997
|
+
`<span class="check">${m.id === aiOptimizerModelId ? '●' : ''}</span>${m.label}</div>`
|
|
1998
|
+
).join('');
|
|
1999
|
+
|
|
2000
|
+
// Populate card header controls
|
|
2001
|
+
const headerControls = document.getElementById('optimizer-header-controls');
|
|
2002
|
+
if (headerControls) {
|
|
2003
|
+
headerControls.innerHTML = `
|
|
2004
|
+
<span class="card-header-actions">
|
|
2005
|
+
<div class="ai-optimizer-model-picker" id="ai-optimizer-model-picker" onclick="event.stopPropagation()">
|
|
2006
|
+
<div class="ai-optimizer-model-pill" onclick="toggleAIOptimizerModelMenu()" data-tip="Click to switch model">${currentAIModel ? currentAIModel.label : 'Opus 4.6'} <span class="pill-chevron">▾</span></div>
|
|
2007
|
+
<div class="ai-optimizer-model-menu">${aiModelMenuHtml}</div>
|
|
2008
|
+
</div>
|
|
2009
|
+
<button class="optimizer-generate-btn-sm" id="ai-optimizer-gen-btn" onclick="generateAIOptimizerSuggestions()" data-warn="⚠ This runs a real Claude session and will show up in your own usage stats. Each generation costs roughly the same as a short coding task.">AI Optimize</button>
|
|
2010
|
+
</span>`;
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
// Body: AI section at top (hidden until generated), then patterns
|
|
1889
2014
|
body.innerHTML = `
|
|
2015
|
+
<div id="ai-optimizer-top-section" style="display:none">
|
|
2016
|
+
<div class="ai-divider" style="margin-top:0"><span>AI Optimize</span></div>
|
|
2017
|
+
<div id="ai-optimizer-suggestions-output"></div>
|
|
2018
|
+
<div class="ai-divider"><span>Patterns</span></div>
|
|
2019
|
+
</div>
|
|
1890
2020
|
<p style="font-size:12px; color:var(--text3); margin-bottom:12px;">
|
|
1891
|
-
Based on ${
|
|
2021
|
+
Based on ${total} patterns found in your sessions.
|
|
1892
2022
|
</p>
|
|
1893
2023
|
<div class="optimizer-tabs">${tabsHtml}</div>
|
|
1894
2024
|
${panelsHtml}
|
|
1895
2025
|
${aiSection}`;
|
|
1896
2026
|
|
|
1897
|
-
// Sync model picker
|
|
2027
|
+
// Sync artifact content model picker
|
|
1898
2028
|
const optimizerPicker = document.getElementById('optimizer-model-picker');
|
|
1899
2029
|
if (optimizerPicker && aiModels.length > 0) {
|
|
1900
2030
|
const current = aiModels.find(m => m.id === selectedModelId) ||
|
|
@@ -1911,9 +2041,13 @@ function renderOptimizerTabs() {
|
|
|
1911
2041
|
if (cachedOptimizerAI) {
|
|
1912
2042
|
renderOptimizerAIComplete(cachedOptimizerAI.content, cachedOptimizerAI.generatedAt, cachedOptimizerAI.model);
|
|
1913
2043
|
}
|
|
2044
|
+
|
|
2045
|
+
if (cachedAIOptimizerSuggestions) {
|
|
2046
|
+
renderAIOptimizerSuggestionsComplete(cachedAIOptimizerSuggestions.suggestions, cachedAIOptimizerSuggestions.generatedAt, cachedAIOptimizerSuggestions.model);
|
|
2047
|
+
}
|
|
1914
2048
|
}
|
|
1915
2049
|
|
|
1916
|
-
function renderSuggestionList(type, items) {
|
|
2050
|
+
function renderSuggestionList(type, items, isAI = false) {
|
|
1917
2051
|
if (!items.length) return '<div class="suggestion-empty">No suggestions for this category.</div>';
|
|
1918
2052
|
return items.map(item => {
|
|
1919
2053
|
const priorityClass = `priority-${item.priority || 'medium'}`;
|
|
@@ -1923,10 +2057,12 @@ function renderSuggestionList(type, items) {
|
|
|
1923
2057
|
if (type === 'claudeMd') meta = `<span>scope: <strong>${escHtml(item.scope)}${item.projectName ? ' · ' + escHtml(item.projectName) : ''}</strong></span>`;
|
|
1924
2058
|
if (type === 'plugins' && item.installCmd) meta = `<span style="font-family:var(--mono)">${escHtml(item.installCmd)}</span>`;
|
|
1925
2059
|
if (type === 'agents' && item.use_case) meta = `<span>${escHtml(item.use_case)}</span>`;
|
|
2060
|
+
const aiNewBadge = isAI && item.aiNew ? '<span class="ai-suggestion-badge">AI new</span>' : '';
|
|
2061
|
+
const aiEnhancedBadge = isAI && item.aiEnhanced ? '<span class="ai-suggestion-badge">AI enhanced</span>' : '';
|
|
1926
2062
|
return `
|
|
1927
2063
|
<div class="suggestion-card">
|
|
1928
2064
|
<div class="suggestion-header">
|
|
1929
|
-
<div class="suggestion-title">${escHtml(item.title || item.name || item.use_case || '')}</div>
|
|
2065
|
+
<div class="suggestion-title">${escHtml(item.title || item.name || item.use_case || '')}${aiNewBadge}${aiEnhancedBadge}</div>
|
|
1930
2066
|
<span class="suggestion-priority ${priorityClass}">${priorityLabel}</span>
|
|
1931
2067
|
</div>
|
|
1932
2068
|
<div class="suggestion-rationale">${escHtml(item.rationale)}</div>
|
|
@@ -1947,6 +2083,202 @@ function switchOptimizerTab(tabId) {
|
|
|
1947
2083
|
});
|
|
1948
2084
|
}
|
|
1949
2085
|
|
|
2086
|
+
function switchOptimizerMode(mode) {
|
|
2087
|
+
optimizerMode = mode;
|
|
2088
|
+
document.querySelectorAll('.optimizer-mode-panel').forEach(p => {
|
|
2089
|
+
p.classList.toggle('active', p.id === `optimizer-${mode === 'static' ? 'static' : 'ai-suggestions'}-panel`);
|
|
2090
|
+
});
|
|
2091
|
+
document.querySelectorAll('.optimizer-mode-tab').forEach(t => {
|
|
2092
|
+
const onclick = t.getAttribute('onclick') || '';
|
|
2093
|
+
const match = onclick.match(/switchOptimizerMode\('([^']+)'\)/);
|
|
2094
|
+
if (match) t.classList.toggle('active', match[1] === mode);
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
function toggleAIOptimizerModelMenu() {
|
|
2099
|
+
const picker = document.getElementById('ai-optimizer-model-picker');
|
|
2100
|
+
if (!picker) return;
|
|
2101
|
+
const wasOpen = picker.classList.contains('open');
|
|
2102
|
+
document.querySelectorAll('.ai-optimizer-model-picker.open').forEach(p => p.classList.remove('open'));
|
|
2103
|
+
if (!wasOpen) picker.classList.add('open');
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
function selectAIOptimizerModel(modelId) {
|
|
2107
|
+
aiOptimizerModelId = modelId;
|
|
2108
|
+
document.querySelectorAll('.ai-optimizer-model-picker.open').forEach(p => p.classList.remove('open'));
|
|
2109
|
+
// Update pill label
|
|
2110
|
+
const pill = document.querySelector('#ai-optimizer-model-picker .ai-optimizer-model-pill');
|
|
2111
|
+
const menu = document.querySelector('#ai-optimizer-model-picker .ai-optimizer-model-menu');
|
|
2112
|
+
const model = aiModels.find(m => m.id === modelId);
|
|
2113
|
+
if (pill && model) { pill.innerHTML = `${model.label} <span class="pill-chevron">▾</span>`; pill.setAttribute('data-tip', 'Click to switch model'); }
|
|
2114
|
+
if (menu) menu.innerHTML = aiModels.map(m =>
|
|
2115
|
+
`<div class="ai-optimizer-model-option${m.id === modelId ? ' active' : ''}" onclick="selectAIOptimizerModel('${m.id}')">` +
|
|
2116
|
+
`<span class="check">${m.id === modelId ? '●' : ''}</span>${m.label}</div>`
|
|
2117
|
+
).join('');
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
function generateAIOptimizerSuggestions() {
|
|
2121
|
+
const btn = document.getElementById('ai-optimizer-gen-btn');
|
|
2122
|
+
const output = document.getElementById('ai-optimizer-suggestions-output');
|
|
2123
|
+
const topSection = document.getElementById('ai-optimizer-top-section');
|
|
2124
|
+
if (!btn || !output) return;
|
|
2125
|
+
|
|
2126
|
+
const wasActive = autoRefreshActive || autoRefreshPaused;
|
|
2127
|
+
if (autoRefreshActive) pauseAutoRefresh();
|
|
2128
|
+
|
|
2129
|
+
const modelParam = aiOptimizerModelId ? `?model=${encodeURIComponent(aiOptimizerModelId)}` : '';
|
|
2130
|
+
const modelName = modelLabel(aiOptimizerModelId) || aiOptimizerModelId;
|
|
2131
|
+
|
|
2132
|
+
btn.disabled = true;
|
|
2133
|
+
if (topSection) topSection.style.display = '';
|
|
2134
|
+
output.innerHTML = `
|
|
2135
|
+
<div class="ai-loading">
|
|
2136
|
+
<div class="ai-spinner"></div>
|
|
2137
|
+
<div>
|
|
2138
|
+
<div>Optimizing with ${modelName}, please wait...</div>
|
|
2139
|
+
</div>
|
|
2140
|
+
</div>
|
|
2141
|
+
<div id="ai-optimizer-cards-streaming" style="margin-top:12px"></div>`;
|
|
2142
|
+
|
|
2143
|
+
streamAIOptimizerFetch(modelParam, output, wasActive);
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
async function streamAIOptimizerFetch(modelParam, output, wasActive) {
|
|
2147
|
+
const accumulatedSuggestions = [];
|
|
2148
|
+
|
|
2149
|
+
try {
|
|
2150
|
+
const res = await fetch('/api/ai-optimizer' + modelParam, { method: 'POST' });
|
|
2151
|
+
const reader = res.body.getReader();
|
|
2152
|
+
const decoder = new TextDecoder();
|
|
2153
|
+
let buffer = '';
|
|
2154
|
+
|
|
2155
|
+
while (true) {
|
|
2156
|
+
const { done, value } = await reader.read();
|
|
2157
|
+
if (done) break;
|
|
2158
|
+
|
|
2159
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2160
|
+
const lines = buffer.split('\n');
|
|
2161
|
+
buffer = lines.pop();
|
|
2162
|
+
|
|
2163
|
+
let currentEvent = '';
|
|
2164
|
+
for (const line of lines) {
|
|
2165
|
+
if (line.startsWith('event: ')) {
|
|
2166
|
+
currentEvent = line.slice(7);
|
|
2167
|
+
} else if (line.startsWith('data: ')) {
|
|
2168
|
+
const data = line.slice(6);
|
|
2169
|
+
if (currentEvent === 'model') {
|
|
2170
|
+
const usedModel = JSON.parse(data);
|
|
2171
|
+
const loading = output.querySelector('.ai-loading div:last-child');
|
|
2172
|
+
if (loading) loading.textContent = `Optimizing with ${modelLabel(usedModel)}, please wait...`;
|
|
2173
|
+
} else if (currentEvent === 'suggestion') {
|
|
2174
|
+
const suggestion = JSON.parse(data);
|
|
2175
|
+
accumulatedSuggestions.push(suggestion);
|
|
2176
|
+
// Render cards progressively
|
|
2177
|
+
const streamEl = document.getElementById('ai-optimizer-cards-streaming');
|
|
2178
|
+
if (streamEl) streamEl.innerHTML = renderAIOptimizerCards(accumulatedSuggestions);
|
|
2179
|
+
} else if (currentEvent === 'done') {
|
|
2180
|
+
const { generatedAt, model } = JSON.parse(data);
|
|
2181
|
+
const loading = output.querySelector('.ai-loading');
|
|
2182
|
+
if (loading) loading.remove();
|
|
2183
|
+
// Merge AI suggestions with static and cache
|
|
2184
|
+
const merged = optimizerSuggestions
|
|
2185
|
+
? mergeAISuggestionsWithStatic(optimizerSuggestions, accumulatedSuggestions)
|
|
2186
|
+
: null;
|
|
2187
|
+
cachedAIOptimizerSuggestions = { suggestions: accumulatedSuggestions, mergedSuggestions: merged, generatedAt, model };
|
|
2188
|
+
renderAIOptimizerSuggestionsComplete(accumulatedSuggestions, generatedAt, model);
|
|
2189
|
+
if (wasActive) resumeAutoRefreshIfPaused();
|
|
2190
|
+
} else if (currentEvent === 'error') {
|
|
2191
|
+
const { message } = JSON.parse(data);
|
|
2192
|
+
const btn = document.getElementById('ai-optimizer-gen-btn');
|
|
2193
|
+
if (btn) { btn.disabled = false; btn.textContent = 'AI Optimize'; }
|
|
2194
|
+
output.innerHTML = `<div class="ai-error">${escHtml(message)}</div>`;
|
|
2195
|
+
if (wasActive) resumeAutoRefreshIfPaused();
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
} catch (err) {
|
|
2201
|
+
const btn = document.getElementById('ai-optimizer-gen-btn');
|
|
2202
|
+
if (btn) { btn.disabled = false; btn.textContent = 'AI Optimize'; }
|
|
2203
|
+
output.innerHTML = `<div class="ai-error">Failed to connect to server</div>`;
|
|
2204
|
+
if (wasActive) resumeAutoRefreshIfPaused();
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
function mergeAISuggestionsWithStatic(staticSuggestions, aiSuggestions) {
|
|
2209
|
+
const normalize = (s) => (s || '').toLowerCase().replace(/[^a-z0-9 ]/g, '').trim();
|
|
2210
|
+
const priorityOrder = { high: 0, medium: 1, low: 2 };
|
|
2211
|
+
|
|
2212
|
+
const result = {
|
|
2213
|
+
skills: [...staticSuggestions.skills],
|
|
2214
|
+
claudeMd: [...staticSuggestions.claudeMd],
|
|
2215
|
+
agents: [...staticSuggestions.agents],
|
|
2216
|
+
plugins: [...staticSuggestions.plugins],
|
|
2217
|
+
};
|
|
2218
|
+
|
|
2219
|
+
for (const ai of aiSuggestions) {
|
|
2220
|
+
const type = ai.type;
|
|
2221
|
+
if (!result[type]) continue;
|
|
2222
|
+
const aiTitle = normalize(ai.title);
|
|
2223
|
+
const idx = result[type].findIndex(s => {
|
|
2224
|
+
const st = normalize(s.title || s.name || s.use_case || '');
|
|
2225
|
+
return st === aiTitle || st.includes(aiTitle) || aiTitle.includes(st);
|
|
2226
|
+
});
|
|
2227
|
+
if (idx >= 0) {
|
|
2228
|
+
result[type][idx] = { ...result[type][idx], ...ai, aiEnhanced: true };
|
|
2229
|
+
} else {
|
|
2230
|
+
result[type].push({ ...ai, aiNew: true });
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
for (const type of Object.keys(result)) {
|
|
2235
|
+
result[type].sort((a, b) => (priorityOrder[a.priority] ?? 1) - (priorityOrder[b.priority] ?? 1));
|
|
2236
|
+
}
|
|
2237
|
+
return result;
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
function renderAIOptimizerCards(suggestions) {
|
|
2241
|
+
if (!suggestions.length) return '';
|
|
2242
|
+
const groups = { skills: [], claudeMd: [], agents: [], plugins: [] };
|
|
2243
|
+
for (const s of suggestions) {
|
|
2244
|
+
if (groups[s.type]) groups[s.type].push(s);
|
|
2245
|
+
}
|
|
2246
|
+
const typeLabels = { skills: 'Skills', claudeMd: 'CLAUDE.md', agents: 'Agents', plugins: 'Plugins' };
|
|
2247
|
+
return Object.entries(groups).filter(([, items]) => items.length > 0).map(([type, items]) => `
|
|
2248
|
+
<div class="ai-suggestions-type-group">
|
|
2249
|
+
<div class="ai-suggestions-type-label">${typeLabels[type] || type}</div>
|
|
2250
|
+
${renderSuggestionList(type, items, true)}
|
|
2251
|
+
</div>`).join('');
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
function renderAIOptimizerSuggestionsComplete(suggestions, generatedAt, model) {
|
|
2255
|
+
const output = document.getElementById('ai-optimizer-suggestions-output');
|
|
2256
|
+
const btn = document.getElementById('ai-optimizer-gen-btn');
|
|
2257
|
+
const topSection = document.getElementById('ai-optimizer-top-section');
|
|
2258
|
+
if (!output) return;
|
|
2259
|
+
if (btn) { btn.disabled = false; btn.textContent = 'AI Optimize'; }
|
|
2260
|
+
if (topSection) topSection.style.display = '';
|
|
2261
|
+
output.innerHTML = `
|
|
2262
|
+
<div>${renderAIOptimizerCards(suggestions)}</div>
|
|
2263
|
+
<div class="optimizer-timestamp">
|
|
2264
|
+
Generated ${new Date(generatedAt).toLocaleString()}${model ? ' · ' + modelLabel(model) : ''}
|
|
2265
|
+
<div style="margin-top:8px">
|
|
2266
|
+
<button class="optimizer-generate-btn" style="padding:4px 12px;font-size:11px;background:var(--surface2);border-color:var(--border);color:var(--text2)" onclick="clearAIOptimizerSuggestions()">Clear</button>
|
|
2267
|
+
</div>
|
|
2268
|
+
</div>`;
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
function clearAIOptimizerSuggestions() {
|
|
2272
|
+
cachedAIOptimizerSuggestions = null;
|
|
2273
|
+
fetch('/api/ai-optimizer', { method: 'DELETE' }).catch(() => {});
|
|
2274
|
+
const output = document.getElementById('ai-optimizer-suggestions-output');
|
|
2275
|
+
const btn = document.getElementById('ai-optimizer-gen-btn');
|
|
2276
|
+
const topSection = document.getElementById('ai-optimizer-top-section');
|
|
2277
|
+
if (output) output.innerHTML = '';
|
|
2278
|
+
if (topSection) topSection.style.display = 'none';
|
|
2279
|
+
if (btn) { btn.disabled = false; btn.textContent = 'AI Optimize'; }
|
|
2280
|
+
}
|
|
2281
|
+
|
|
1950
2282
|
function generateOptimizerContent() {
|
|
1951
2283
|
const btn = document.getElementById('optimizer-gen-btn');
|
|
1952
2284
|
const output = document.getElementById('optimizer-ai-output');
|
|
@@ -2071,7 +2403,7 @@ function renderOptimizerAIComplete(content, generatedAt, model) {
|
|
|
2071
2403
|
<div class="optimizer-ai-content">${renderOptimizerMarkdown(escHtml(content))}</div>
|
|
2072
2404
|
<div class="optimizer-timestamp">
|
|
2073
2405
|
Generated ${new Date(generatedAt).toLocaleString()}${model ? ' · ' + modelLabel(model) : ''}
|
|
2074
|
-
<button class="optimizer-generate-btn" style="padding:4px 12px;font-size:11px;margin-left:8px" onclick="generateOptimizerContent()">Regenerate</button>
|
|
2406
|
+
<button class="optimizer-generate-btn" style="padding:4px 12px;font-size:11px;margin-left:8px" onclick="generateOptimizerContent()" data-warn="⚠ This runs a real Claude session and will show up in your own usage stats. Each generation costs roughly the same as a short coding task.">Regenerate</button>
|
|
2075
2407
|
<button class="optimizer-generate-btn" style="padding:4px 12px;font-size:11px;background:var(--surface2);color:var(--text2)" onclick="clearOptimizerAI()">Clear</button>
|
|
2076
2408
|
</div>`;
|
|
2077
2409
|
}
|
|
@@ -2566,7 +2898,7 @@ sse.onmessage = async (e) => {
|
|
|
2566
2898
|
location.reload();
|
|
2567
2899
|
} else {
|
|
2568
2900
|
currentData = await fetchData();
|
|
2569
|
-
if (currentView === 'dashboard') renderDashboard(currentData);
|
|
2901
|
+
if (currentView === 'dashboard') { renderDashboard(currentData); applyAIState(); }
|
|
2570
2902
|
}
|
|
2571
2903
|
};
|
|
2572
2904
|
</script>
|
package/src/ai-analyze.js
CHANGED
|
@@ -458,3 +458,225 @@ export function getCachedSuggestions() {
|
|
|
458
458
|
export function clearCachedSuggestions() {
|
|
459
459
|
cachedSuggestionResult = null;
|
|
460
460
|
}
|
|
461
|
+
|
|
462
|
+
// --- AI Optimizer Suggestions (structured suggestion objects) ---
|
|
463
|
+
|
|
464
|
+
function buildOptimizerSuggestionsPrompt(scoredData, staticSuggestions) {
|
|
465
|
+
const { sessions, badges, overallScore } = scoredData;
|
|
466
|
+
const totalCost = sessions.reduce((s, x) => s + x.totals.estimatedCost, 0);
|
|
467
|
+
const projectNames = [...new Set(sessions.map((s) => s.project).filter(Boolean))].slice(0, 5);
|
|
468
|
+
const badgeNames = badges.map((b) => `${b.negative ? "⚠ " : "✓ "}${b.name}`).join(", ");
|
|
469
|
+
|
|
470
|
+
const staticSummary = [
|
|
471
|
+
...staticSuggestions.skills.map(s => `skills: "${s.title}" (${s.priority})`),
|
|
472
|
+
...staticSuggestions.claudeMd.map(s => `claudeMd: "${s.title}" (${s.priority})`),
|
|
473
|
+
...staticSuggestions.agents.map(s => `agents: "${s.title || s.name}" (${s.priority})`),
|
|
474
|
+
...staticSuggestions.plugins.map(s => `plugins: "${s.title || s.name}" (${s.priority})`),
|
|
475
|
+
].join("\n");
|
|
476
|
+
|
|
477
|
+
return `You are analyzing a developer's Claude Code session data to generate workflow improvement suggestions.
|
|
478
|
+
|
|
479
|
+
## Their Usage Context
|
|
480
|
+
- ${sessions.length} total sessions across ${projectNames.length} project(s): ${projectNames.join(", ")}
|
|
481
|
+
- Overall efficiency score: ${overallScore}/100
|
|
482
|
+
- Total spend: $${totalCost.toFixed(2)}
|
|
483
|
+
- Badges: ${badgeNames || "none"}
|
|
484
|
+
|
|
485
|
+
## Existing rule-based suggestions (already shown to user)
|
|
486
|
+
${staticSummary || "none"}
|
|
487
|
+
|
|
488
|
+
## Your task
|
|
489
|
+
Generate 4-8 workflow suggestions based on a deeper analysis of their patterns. You may enhance existing rule-based suggestions with better rationale, or add entirely new suggestions the rules missed.
|
|
490
|
+
|
|
491
|
+
Output ONLY a series of JSON objects, one per line, with NO other text, markdown, or explanation. Each line must be valid JSON in exactly one of these formats:
|
|
492
|
+
|
|
493
|
+
For skills: {"type":"skills","title":"Short action title","rationale":"Why this helps, referencing their specific data","priority":"high","trigger":"/slash-command"}
|
|
494
|
+
For CLAUDE.md: {"type":"claudeMd","title":"Short title","rationale":"Why this helps","priority":"medium","scope":"global","projectName":"optional project name"}
|
|
495
|
+
For agents: {"type":"agents","title":"Agent name","rationale":"Why this helps","priority":"low","use_case":"One-line description"}
|
|
496
|
+
For plugins: {"type":"plugins","title":"Plugin name","rationale":"Why this helps","priority":"high","installCmd":"mcp install command"}
|
|
497
|
+
|
|
498
|
+
Rules:
|
|
499
|
+
- Output 4-8 total suggestions
|
|
500
|
+
- Reference specific patterns from their data (costs, badges, session counts)
|
|
501
|
+
- Prioritize suggestions NOT already in the rule-based list, or meaningfully improve on them with richer rationale
|
|
502
|
+
- Output ONLY JSON lines, nothing else`;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
let cachedOptimizerSuggestionsResult = null;
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Stream AI-generated structured suggestion objects.
|
|
509
|
+
* Emits: { event: "model", data: modelId }
|
|
510
|
+
* { event: "suggestion", data: { type, title, rationale, priority, ... } }
|
|
511
|
+
* { event: "done", data: { generatedAt, model } }
|
|
512
|
+
* { event: "error", data: { message } }
|
|
513
|
+
*/
|
|
514
|
+
export function streamAIOptimizerSuggestions(scoredData, staticSuggestions, res, modelId) {
|
|
515
|
+
const prompt = buildOptimizerSuggestionsPrompt(scoredData, staticSuggestions);
|
|
516
|
+
|
|
517
|
+
const args = ["-p", "-", "--output-format", "stream-json", "--verbose"];
|
|
518
|
+
if (modelId) args.push("--model", modelId);
|
|
519
|
+
|
|
520
|
+
const resolvedModel = modelId || "default";
|
|
521
|
+
console.log(`[ai-optimizer] Starting (model: ${resolvedModel})`);
|
|
522
|
+
res.write(`event: model\ndata: ${JSON.stringify(resolvedModel)}\n\n`);
|
|
523
|
+
|
|
524
|
+
const child = spawn("claude", args, {
|
|
525
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
526
|
+
env: cleanEnv,
|
|
527
|
+
});
|
|
528
|
+
activeChildren.add(child);
|
|
529
|
+
|
|
530
|
+
child.stdin.write(prompt);
|
|
531
|
+
child.stdin.end();
|
|
532
|
+
|
|
533
|
+
const aiSuggestions = [];
|
|
534
|
+
let errOutput = "";
|
|
535
|
+
let detectedModel = modelId || detectedDefaultModel || null;
|
|
536
|
+
let lineBuf = "";
|
|
537
|
+
let textBuf = "";
|
|
538
|
+
|
|
539
|
+
child.stdout.on("data", (buf) => {
|
|
540
|
+
lineBuf += buf.toString();
|
|
541
|
+
const lines = lineBuf.split("\n");
|
|
542
|
+
lineBuf = lines.pop();
|
|
543
|
+
for (const line of lines) {
|
|
544
|
+
if (!line.trim()) continue;
|
|
545
|
+
try {
|
|
546
|
+
const obj = JSON.parse(line);
|
|
547
|
+
if (obj.type === "system" || obj.type === "rate_limit_event") continue;
|
|
548
|
+
if (obj.type === "result") {
|
|
549
|
+
const models = Object.keys(obj.modelUsage || {});
|
|
550
|
+
if (models.length > 0) {
|
|
551
|
+
detectedModel = models[0];
|
|
552
|
+
if (!modelId) detectedDefaultModel = models[0];
|
|
553
|
+
}
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
if (obj.type === "assistant" && obj.message?.content) {
|
|
557
|
+
for (const block of obj.message.content) {
|
|
558
|
+
if (block.type === "text" && block.text) {
|
|
559
|
+
textBuf += block.text;
|
|
560
|
+
// Try to parse complete JSON lines from the text buffer
|
|
561
|
+
const textLines = textBuf.split("\n");
|
|
562
|
+
textBuf = textLines.pop(); // keep last (possibly incomplete) line
|
|
563
|
+
for (const tl of textLines) {
|
|
564
|
+
const trimmed = tl.trim();
|
|
565
|
+
if (!trimmed) continue;
|
|
566
|
+
try {
|
|
567
|
+
const suggestion = JSON.parse(trimmed);
|
|
568
|
+
if (suggestion.type && suggestion.title) {
|
|
569
|
+
aiSuggestions.push(suggestion);
|
|
570
|
+
res.write(`event: suggestion\ndata: ${JSON.stringify(suggestion)}\n\n`);
|
|
571
|
+
}
|
|
572
|
+
} catch {
|
|
573
|
+
// not a valid JSON suggestion line, skip
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (obj.message.model && !detectedModel) {
|
|
579
|
+
detectedModel = obj.message.model;
|
|
580
|
+
if (!modelId) detectedDefaultModel = obj.message.model;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
} catch {
|
|
584
|
+
// not stream-json, treat as raw text
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
child.stderr.on("data", (buf) => {
|
|
590
|
+
errOutput += buf.toString();
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
child.on("error", (err) => {
|
|
594
|
+
const message =
|
|
595
|
+
err.code === "ENOENT"
|
|
596
|
+
? "Claude CLI not found. Make sure `claude` is installed and in your PATH."
|
|
597
|
+
: `Claude CLI error: ${err.message}`;
|
|
598
|
+
res.write(`event: error\ndata: ${JSON.stringify({ message })}\n\n`);
|
|
599
|
+
res.end();
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
child.on("close", (code) => {
|
|
603
|
+
activeChildren.delete(child);
|
|
604
|
+
// Flush any remaining text buffer
|
|
605
|
+
if (textBuf.trim()) {
|
|
606
|
+
try {
|
|
607
|
+
const suggestion = JSON.parse(textBuf.trim());
|
|
608
|
+
if (suggestion.type && suggestion.title) {
|
|
609
|
+
aiSuggestions.push(suggestion);
|
|
610
|
+
res.write(`event: suggestion\ndata: ${JSON.stringify(suggestion)}\n\n`);
|
|
611
|
+
}
|
|
612
|
+
} catch { /* ignore */ }
|
|
613
|
+
}
|
|
614
|
+
if (code !== 0 && aiSuggestions.length === 0) {
|
|
615
|
+
const message = errOutput.trim() || `Claude CLI exited with code ${code}`;
|
|
616
|
+
console.error(`[ai-optimizer] Failed (exit ${code}): ${message}`);
|
|
617
|
+
res.write(`event: error\ndata: ${JSON.stringify({ message })}\n\n`);
|
|
618
|
+
} else {
|
|
619
|
+
const finalModel = detectedModel || resolvedModel;
|
|
620
|
+
console.log(`[ai-optimizer] Done (model: ${finalModel}, ${aiSuggestions.length} suggestions)`);
|
|
621
|
+
cachedOptimizerSuggestionsResult = {
|
|
622
|
+
suggestions: aiSuggestions,
|
|
623
|
+
generatedAt: new Date().toISOString(),
|
|
624
|
+
model: finalModel,
|
|
625
|
+
};
|
|
626
|
+
res.write(`event: done\ndata: ${JSON.stringify({ generatedAt: cachedOptimizerSuggestionsResult.generatedAt, model: finalModel })}\n\n`);
|
|
627
|
+
}
|
|
628
|
+
res.end();
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
res.on("close", () => {
|
|
632
|
+
if (child.exitCode === null) child.kill();
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
export function getCachedOptimizerSuggestions() {
|
|
637
|
+
return cachedOptimizerSuggestionsResult;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
export function clearCachedOptimizerSuggestions() {
|
|
641
|
+
cachedOptimizerSuggestionsResult = null;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Merge AI-generated suggestions with static rule-based ones.
|
|
646
|
+
* AI suggestions matching an existing one (by type + title similarity) replace it;
|
|
647
|
+
* novel AI suggestions are appended.
|
|
648
|
+
*/
|
|
649
|
+
export function mergeAISuggestionsWithStatic(staticSuggestions, aiSuggestions) {
|
|
650
|
+
const normalize = (s) => (s || "").toLowerCase().replace(/[^a-z0-9 ]/g, "").trim();
|
|
651
|
+
const priorityOrder = { high: 0, medium: 1, low: 2 };
|
|
652
|
+
|
|
653
|
+
const result = {
|
|
654
|
+
skills: [...staticSuggestions.skills],
|
|
655
|
+
claudeMd: [...staticSuggestions.claudeMd],
|
|
656
|
+
agents: [...staticSuggestions.agents],
|
|
657
|
+
plugins: [...staticSuggestions.plugins],
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
for (const ai of aiSuggestions) {
|
|
661
|
+
const type = ai.type;
|
|
662
|
+
if (!result[type]) continue;
|
|
663
|
+
|
|
664
|
+
const aiTitle = normalize(ai.title);
|
|
665
|
+
const idx = result[type].findIndex((s) => {
|
|
666
|
+
const st = normalize(s.title || s.name || s.use_case || "");
|
|
667
|
+
return st === aiTitle || st.includes(aiTitle) || aiTitle.includes(st);
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
if (idx >= 0) {
|
|
671
|
+
result[type][idx] = { ...result[type][idx], ...ai };
|
|
672
|
+
} else {
|
|
673
|
+
result[type].push(ai);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
for (const type of Object.keys(result)) {
|
|
678
|
+
result[type].sort((a, b) => (priorityOrder[a.priority] ?? 1) - (priorityOrder[b.priority] ?? 1));
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return result;
|
|
682
|
+
}
|
package/src/server.js
CHANGED
|
@@ -7,7 +7,7 @@ import { execFile } from "node:child_process";
|
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
8
|
import { parseAllSessions } from "./parser.js";
|
|
9
9
|
import { scoreAllSessions } from "./scorer.js";
|
|
10
|
-
import { streamAIAnalysis, getCachedAnalysis, clearCachedAnalysis, getAvailableModels, killActiveProcesses, detectDefaultModel, buildPrompt, buildDataSnapshot, streamAISuggestions, getCachedSuggestions, clearCachedSuggestions } from "./ai-analyze.js";
|
|
10
|
+
import { streamAIAnalysis, getCachedAnalysis, clearCachedAnalysis, getAvailableModels, killActiveProcesses, detectDefaultModel, buildPrompt, buildDataSnapshot, streamAISuggestions, getCachedSuggestions, clearCachedSuggestions, streamAIOptimizerSuggestions, getCachedOptimizerSuggestions, clearCachedOptimizerSuggestions } from "./ai-analyze.js";
|
|
11
11
|
import { generateSuggestions } from "./suggestions.js";
|
|
12
12
|
|
|
13
13
|
const execFileAsync = promisify(execFile);
|
|
@@ -207,6 +207,29 @@ export function startServer(port = 6543) {
|
|
|
207
207
|
return json(res, cached || { content: null });
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
if (url.pathname === "/api/ai-optimizer" && req.method === "POST") {
|
|
211
|
+
const data = await getData();
|
|
212
|
+
const suggestions = generateSuggestions(data);
|
|
213
|
+
const modelId = url.searchParams.get("model") || "";
|
|
214
|
+
res.writeHead(200, {
|
|
215
|
+
"Content-Type": "text/event-stream",
|
|
216
|
+
"Cache-Control": "no-cache",
|
|
217
|
+
Connection: "keep-alive",
|
|
218
|
+
});
|
|
219
|
+
streamAIOptimizerSuggestions(data, suggestions, res, modelId || undefined);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (url.pathname === "/api/ai-optimizer" && req.method === "DELETE") {
|
|
224
|
+
clearCachedOptimizerSuggestions();
|
|
225
|
+
return json(res, { ok: true });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (url.pathname === "/api/ai-optimizer" && req.method === "GET") {
|
|
229
|
+
const cached = getCachedOptimizerSuggestions();
|
|
230
|
+
return json(res, cached || { suggestions: null });
|
|
231
|
+
}
|
|
232
|
+
|
|
210
233
|
if (url.pathname === "/api/refresh") {
|
|
211
234
|
const data = await getData(true);
|
|
212
235
|
return json(res, { sessions: data.sessions.length, overallScore: data.overallScore });
|