cc-insight 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -0
- package/README.zh.md +106 -0
- package/bin/cc-insight.js +51 -0
- package/package.json +51 -0
- package/public/index.html +239 -0
- package/public/js/app.js +129 -0
- package/public/js/charts.js +0 -0
- package/public/js/heatmap.js +0 -0
- package/public/js/insights.js +326 -0
- package/public/js/mcp.js +143 -0
- package/public/js/overview.js +421 -0
- package/public/js/poster.js +1025 -0
- package/public/js/skills.js +637 -0
- package/public/js/theme.js +4 -0
- package/src/api.js +501 -0
- package/src/classifiers/topic-rules.js +100 -0
- package/src/config.js +28 -0
- package/src/db/db.js +39 -0
- package/src/db/queries.js +314 -0
- package/src/db/schema.js +46 -0
- package/src/indexer.js +158 -0
- package/src/parsers/jsonl.js +70 -0
- package/src/parsers/security.js +23 -0
- package/src/parsers/skill-md.js +38 -0
- package/src/poster.js +265 -0
- package/src/server.js +67 -0
- package/src/watcher.js +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# CC Insight
|
|
2
|
+
|
|
3
|
+
> A local analytics dashboard for [Claude Code](https://claude.ai/code) — understand how you actually use AI.
|
|
4
|
+
|
|
5
|
+
[中文说明](README.zh.md)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
Claude Code's built-in `/insights` command gives you a quick text summary. But it can't answer questions like:
|
|
12
|
+
|
|
13
|
+
- Which topics take the most back-and-forth before they're resolved?
|
|
14
|
+
- What time of day am I most productive?
|
|
15
|
+
- Which skills did I install and never use?
|
|
16
|
+
- How has my usage changed over the past month?
|
|
17
|
+
|
|
18
|
+
CC Insight indexes your local session history and presents it as an interactive dashboard — no cloud, no account, no data leaving your machine.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## vs `/insights`
|
|
23
|
+
|
|
24
|
+
| | `/insights` | CC Insight |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| Output | Static HTML file | Interactive dashboard |
|
|
27
|
+
| History | Current session only | All-time + time range filter |
|
|
28
|
+
| Trends | — | Time patterns, topic trends, efficiency metrics |
|
|
29
|
+
| Skills | — | Usage stats, idle detection, bulk cleanup |
|
|
30
|
+
| MCP Servers | — | Configured servers + tool list |
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
**Overview** — Session count, duration, peak hours, GitHub-style activity heatmap, 24H distribution, and smart habit insights.
|
|
37
|
+
|
|
38
|
+
**Efficiency** — Time-consuming topics, tool call density, high-round sessions, project distribution, and a time-of-day × topic heatmap.
|
|
39
|
+
|
|
40
|
+
**Skills** — Installed Skill / Agent / Plugin list with usage stats, idle detection, one-click bulk cleanup, and security scan.
|
|
41
|
+
|
|
42
|
+
**MCP Servers** — Configured servers and their tools, parsed from `settings.json` and `claude_desktop_config.json`.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Requirements
|
|
47
|
+
|
|
48
|
+
- Node.js ≥ 20
|
|
49
|
+
- Claude Code installed (`~/.claude/` directory must exist)
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
**npm (recommended)**
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm install -g cc-insight
|
|
59
|
+
cc-insight
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**From source**
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
git clone https://github.com/hopeee-lab/cc-insight.git
|
|
66
|
+
cd cc-insight
|
|
67
|
+
npm install
|
|
68
|
+
npm start
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The dashboard opens automatically at `http://127.0.0.1:3847`.
|
|
72
|
+
|
|
73
|
+
> macOS: if installation fails, install Xcode Command Line Tools first:
|
|
74
|
+
> `xcode-select --install`
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Usage
|
|
79
|
+
|
|
80
|
+
| Action | How |
|
|
81
|
+
|--------|-----|
|
|
82
|
+
| Switch time range | 7d / 30d / 90d / All — top of each view |
|
|
83
|
+
| Re-scan skills | Restart CC Insight (auto on every launch) |
|
|
84
|
+
| Re-index sessions | "重新检测" on the empty state screen |
|
|
85
|
+
| Bulk clean unused tools | "一键清理" in the Skills → Unused list |
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Data & Privacy
|
|
90
|
+
|
|
91
|
+
CC Insight reads only from `~/.claude/` and builds a local index at `~/.cc-insight/data.db`.
|
|
92
|
+
It runs a local web server at `127.0.0.1:3847` — accessible from your machine only.
|
|
93
|
+
**No data is ever sent anywhere.**
|
|
94
|
+
|
|
95
|
+
| Data | Source |
|
|
96
|
+
|------|--------|
|
|
97
|
+
| Session history | `~/.claude/projects/**/*.jsonl` |
|
|
98
|
+
| Skills & Agents | `~/.claude/skills/*/SKILL.md` |
|
|
99
|
+
| Plugins | `~/.claude/plugins/cache/` |
|
|
100
|
+
| MCP Servers | `~/.claude/settings.json`, `~/Library/Application Support/Claude/claude_desktop_config.json` |
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# CC Insight
|
|
2
|
+
|
|
3
|
+
> 本地 [Claude Code](https://claude.ai/code) 使用数据可视化工具 — 真正了解你在用 AI 做什么。
|
|
4
|
+
|
|
5
|
+
[English](README.md)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 为什么做这个
|
|
10
|
+
|
|
11
|
+
Claude Code 内置的 `/insights` 指令会生成一份静态 HTML 报告,但它回答不了这些问题:
|
|
12
|
+
|
|
13
|
+
- 哪类任务最耗时、需要最多轮对话才能解决?
|
|
14
|
+
- 我在一天中哪个时段最高效?
|
|
15
|
+
- 装了哪些 Skill 从来没用过?
|
|
16
|
+
- 这个月比上个月用得多了还是少了?
|
|
17
|
+
|
|
18
|
+
CC Insight 把你本地的对话历史建立索引,以交互式仪表盘的形式呈现——无需云端、无需注册、数据不离本机。
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 与 `/insights` 的区别
|
|
23
|
+
|
|
24
|
+
| | `/insights` | CC Insight |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| 输出形式 | 生成静态 HTML 文件 | 实时交互仪表盘 |
|
|
27
|
+
| 历史范围 | 当前 session | 全量历史 + 时间范围筛选 |
|
|
28
|
+
| 趋势分析 | — | 时段分布、话题趋势、效率指标 |
|
|
29
|
+
| Skill 管理 | — | 使用统计、闲置检测、一键清理 |
|
|
30
|
+
| MCP Server | — | 已配置服务器 + 工具列表 |
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 功能
|
|
35
|
+
|
|
36
|
+
**使用概览** — 对话次数、时长、活跃时段、GitHub 风格热力图、24H 分布图,以及使用习惯智能洞察。
|
|
37
|
+
|
|
38
|
+
**效率分析** — 耗时话题排名、工具调用密度、高轮次 Session 列表、项目分布、时段 × 话题热力图。
|
|
39
|
+
|
|
40
|
+
**Skill & Agent 管理** — 已安装工具列表(含使用统计)、闲置检测、一键批量清理、SKILL.md 安全扫描。
|
|
41
|
+
|
|
42
|
+
**MCP Server** — 自动读取配置文件,展示已配置的 MCP Server 及工具列表。
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 环境要求
|
|
47
|
+
|
|
48
|
+
- Node.js ≥ 20
|
|
49
|
+
- 已安装并使用过 Claude Code(`~/.claude/` 目录存在)
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 安装
|
|
54
|
+
|
|
55
|
+
**npm 安装(推荐)**
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm install -g cc-insight
|
|
59
|
+
cc-insight
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**从源码运行**
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
git clone https://github.com/hopeee-lab/cc-insight.git
|
|
66
|
+
cd cc-insight
|
|
67
|
+
npm install
|
|
68
|
+
npm start
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
启动后浏览器自动打开 `http://127.0.0.1:3847`。
|
|
72
|
+
|
|
73
|
+
> macOS 如果安装失败,先安装 Xcode 命令行工具:
|
|
74
|
+
> `xcode-select --install`
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 使用说明
|
|
79
|
+
|
|
80
|
+
| 操作 | 方式 |
|
|
81
|
+
|------|------|
|
|
82
|
+
| 切换时间范围 | 各视图顶部 7 天 / 30 天 / 90 天 / 全部按钮 |
|
|
83
|
+
| 重新扫描 Skill | 重启 CC Insight(每次启动自动执行) |
|
|
84
|
+
| 重建对话索引 | 空状态页点击「重新检测」 |
|
|
85
|
+
| 一键清理闲置工具 | Skill 页「从未使用」列表 → 「一键清理」 |
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 数据与隐私
|
|
90
|
+
|
|
91
|
+
CC Insight 只读取 `~/.claude/` 目录,在本地建立索引文件 `~/.cc-insight/data.db`。
|
|
92
|
+
Web 服务仅监听 `127.0.0.1:3847`,只有本机可访问。
|
|
93
|
+
**所有数据不会上传到任何服务器。**
|
|
94
|
+
|
|
95
|
+
| 数据类型 | 来源路径 |
|
|
96
|
+
|----------|----------|
|
|
97
|
+
| 对话记录 | `~/.claude/projects/**/*.jsonl` |
|
|
98
|
+
| Skill / Agent | `~/.claude/skills/*/SKILL.md` |
|
|
99
|
+
| Plugin | `~/.claude/plugins/cache/` |
|
|
100
|
+
| MCP Server | `~/.claude/settings.json`、`~/Library/Application Support/Claude/claude_desktop_config.json` |
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// bin/cc-insight.js
|
|
3
|
+
import { createAppServer } from '../src/server.js'
|
|
4
|
+
import { runFullIndex, syncToolsOnly } from '../src/indexer.js'
|
|
5
|
+
import { startWatcher } from '../src/watcher.js'
|
|
6
|
+
import { getMeta } from '../src/db/db.js'
|
|
7
|
+
import open from 'open'
|
|
8
|
+
|
|
9
|
+
const PORT = parseInt(process.env.CC_PORT ?? '3847')
|
|
10
|
+
|
|
11
|
+
async function main() {
|
|
12
|
+
const srv = createAppServer()
|
|
13
|
+
const port = await srv.listen(PORT)
|
|
14
|
+
const url = `http://127.0.0.1:${port}`
|
|
15
|
+
|
|
16
|
+
console.log(`\nCC Insight running → ${url}\n`)
|
|
17
|
+
|
|
18
|
+
const alreadyIndexed = getMeta('last_full_index')
|
|
19
|
+
|
|
20
|
+
// 每次启动都同步工具(检测新安装/删除的 skill & plugin)
|
|
21
|
+
syncToolsOnly()
|
|
22
|
+
|
|
23
|
+
// 先打开浏览器,等待 WS 连接建立后再推送进度
|
|
24
|
+
await open(url)
|
|
25
|
+
if (!alreadyIndexed) await new Promise(r => setTimeout(r, 1500))
|
|
26
|
+
|
|
27
|
+
if (!alreadyIndexed) {
|
|
28
|
+
console.log('首次启动,建立索引中...')
|
|
29
|
+
await runFullIndex((pct) => {
|
|
30
|
+
process.stdout.write(`\r 索引进度: ${pct}%`)
|
|
31
|
+
srv.sendProgress(pct)
|
|
32
|
+
})
|
|
33
|
+
console.log('\n索引完成。')
|
|
34
|
+
srv.sendProgress(100)
|
|
35
|
+
setTimeout(() => srv.sendRefresh(), 600)
|
|
36
|
+
} else {
|
|
37
|
+
srv.sendRefresh()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
startWatcher(() => srv.sendRefresh())
|
|
41
|
+
|
|
42
|
+
process.on('SIGINT', () => {
|
|
43
|
+
console.log('\nCC Insight stopped.')
|
|
44
|
+
process.exit(0)
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
main().catch(err => {
|
|
49
|
+
console.error('启动失败:', err.message)
|
|
50
|
+
process.exit(1)
|
|
51
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cc-insight",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local Claude Code usage dashboard — sessions, skills, efficiency, MCP servers, and shareable poster",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cc-insight": "bin/cc-insight.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"public",
|
|
13
|
+
"README.md",
|
|
14
|
+
"README.zh.md"
|
|
15
|
+
],
|
|
16
|
+
"keywords": [
|
|
17
|
+
"claude",
|
|
18
|
+
"claude-code",
|
|
19
|
+
"ai",
|
|
20
|
+
"dashboard",
|
|
21
|
+
"analytics",
|
|
22
|
+
"skill",
|
|
23
|
+
"mcp"
|
|
24
|
+
],
|
|
25
|
+
"homepage": "https://github.com/hopeee-lab/cc-insight",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/hopeee-lab/cc-insight.git"
|
|
29
|
+
},
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"scripts": {
|
|
32
|
+
"start": "node bin/cc-insight.js",
|
|
33
|
+
"test": "node --experimental-vm-modules node_modules/.bin/jest --testPathPattern=tests/"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"better-sqlite3": "^9.6.0",
|
|
37
|
+
"chokidar": "^3.6.0",
|
|
38
|
+
"express": "^4.19.0",
|
|
39
|
+
"open": "^10.1.0",
|
|
40
|
+
"ws": "^8.17.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"jest": "^29.7.0"
|
|
44
|
+
},
|
|
45
|
+
"jest": {
|
|
46
|
+
"transform": {}
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=20.0.0"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh" data-theme="dark">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>CC Insight</title>
|
|
7
|
+
<style>
|
|
8
|
+
/* ── Reset & Base ── */
|
|
9
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
10
|
+
|
|
11
|
+
:root {
|
|
12
|
+
--bg: #0d1117;
|
|
13
|
+
--bg2: #111827;
|
|
14
|
+
--bg3: #1f2937;
|
|
15
|
+
--border: #1f2937;
|
|
16
|
+
--text: #e5e7eb;
|
|
17
|
+
--muted: #6b7280;
|
|
18
|
+
--green: #4ade80;
|
|
19
|
+
--cyan: #22d3ee;
|
|
20
|
+
--amber: #f59e0b;
|
|
21
|
+
--red: #f87171;
|
|
22
|
+
--purple: #a78bfa;
|
|
23
|
+
--font: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
|
|
24
|
+
--radius: 6px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
[data-theme="light"] {
|
|
28
|
+
--bg: #ffffff;
|
|
29
|
+
--bg2: #f9fafb;
|
|
30
|
+
--bg3: #f3f4f6;
|
|
31
|
+
--border: #e5e7eb;
|
|
32
|
+
--text: #111827;
|
|
33
|
+
--muted: #6b7280;
|
|
34
|
+
--green: #16a34a;
|
|
35
|
+
--cyan: #0891b2;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
html, body { height: 100%; background: var(--bg); color: var(--text);
|
|
39
|
+
font-family: var(--font); font-size: 14px; line-height: 1.5; }
|
|
40
|
+
|
|
41
|
+
/* ── Layout ── */
|
|
42
|
+
.layout { display: flex; flex-direction: column; height: 100vh; }
|
|
43
|
+
|
|
44
|
+
.topbar {
|
|
45
|
+
display: flex; align-items: center; gap: 12px;
|
|
46
|
+
padding: 0 20px; height: 48px;
|
|
47
|
+
background: var(--bg2); border-bottom: 1px solid var(--border);
|
|
48
|
+
flex-shrink: 0;
|
|
49
|
+
}
|
|
50
|
+
.topbar-brand { color: var(--green); font-weight: bold; font-size: 15px; flex-shrink: 0; }
|
|
51
|
+
.topbar-spacer { flex: 1; }
|
|
52
|
+
.topbar-tool { color: var(--muted); font-size: 13px; flex-shrink: 0; }
|
|
53
|
+
|
|
54
|
+
/* ── 海报按钮 ── */
|
|
55
|
+
.poster-btn {
|
|
56
|
+
display: flex; align-items: center; gap: 6px;
|
|
57
|
+
padding: 5px 12px; border-radius: var(--radius);
|
|
58
|
+
border: 1px solid var(--border); background: transparent;
|
|
59
|
+
color: var(--muted); font-size: 13px; font-family: var(--font);
|
|
60
|
+
cursor: pointer; transition: all 0.15s; flex-shrink: 0;
|
|
61
|
+
}
|
|
62
|
+
.poster-btn:hover {
|
|
63
|
+
border-color: var(--green); color: var(--green);
|
|
64
|
+
background: color-mix(in srgb, var(--green) 8%, transparent);
|
|
65
|
+
}
|
|
66
|
+
.poster-btn svg { width: 14px; height: 14px; flex-shrink: 0; }
|
|
67
|
+
|
|
68
|
+
/* Segmented tab */
|
|
69
|
+
.tab-group {
|
|
70
|
+
display: flex; gap: 2px;
|
|
71
|
+
background: var(--bg3); border: 1px solid var(--border);
|
|
72
|
+
border-radius: 8px; padding: 3px;
|
|
73
|
+
}
|
|
74
|
+
.tab-btn {
|
|
75
|
+
padding: 5px 16px; border-radius: 6px; border: none;
|
|
76
|
+
background: transparent; color: var(--muted);
|
|
77
|
+
font-size: 14px; font-family: var(--font); cursor: pointer;
|
|
78
|
+
transition: all 0.15s; white-space: nowrap;
|
|
79
|
+
}
|
|
80
|
+
.tab-btn:hover { color: var(--text); }
|
|
81
|
+
.tab-btn.active { background: var(--bg2); color: var(--text);
|
|
82
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.3); }
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
.main { display: flex; flex: 1; overflow: hidden; }
|
|
86
|
+
|
|
87
|
+
/* ── Content ── */
|
|
88
|
+
.content { flex: 1; min-height: 0; overflow: hidden; padding: 20px; display: flex; flex-direction: column; }
|
|
89
|
+
|
|
90
|
+
/* ── Cards ── */
|
|
91
|
+
.card {
|
|
92
|
+
background: var(--bg2); border: 1px solid var(--border);
|
|
93
|
+
border-radius: var(--radius); padding: 16px;
|
|
94
|
+
}
|
|
95
|
+
.card-label { font-size: 11px; color: var(--muted);
|
|
96
|
+
letter-spacing: 1px; text-transform: uppercase; margin-bottom: 6px; }
|
|
97
|
+
.card-value { font-size: 26px; font-weight: bold; }
|
|
98
|
+
.card-sub { font-size: 14px; color: var(--muted); margin-top: 2px; }
|
|
99
|
+
|
|
100
|
+
.grid-4 { display: grid; grid-template-columns: repeat(4,1fr); gap: 10px; }
|
|
101
|
+
.grid-3 { display: grid; grid-template-columns: repeat(3,1fr); gap: 10px; }
|
|
102
|
+
|
|
103
|
+
/* ── Time range filter ── */
|
|
104
|
+
.range-filter { display: flex; gap: 6px; align-items: center;
|
|
105
|
+
margin-bottom: 16px; }
|
|
106
|
+
.range-filter span { font-size: 14px; color: var(--muted); }
|
|
107
|
+
.range-btn {
|
|
108
|
+
padding: 3px 10px; border-radius: var(--radius); font-size: 14px;
|
|
109
|
+
cursor: pointer; font-family: var(--font);
|
|
110
|
+
border: 1px solid var(--border); background: transparent; color: var(--muted);
|
|
111
|
+
}
|
|
112
|
+
.range-btn.active {
|
|
113
|
+
background: color-mix(in srgb, var(--green) 15%, transparent);
|
|
114
|
+
border-color: var(--green); color: var(--green);
|
|
115
|
+
}
|
|
116
|
+
.range-btn:hover:not(.active) { color: var(--text); }
|
|
117
|
+
|
|
118
|
+
/* ── Split layout ── */
|
|
119
|
+
.split { display: grid; grid-template-columns: 280px 1fr; gap: 12px; flex: 1; min-height: 0; overflow: hidden; }
|
|
120
|
+
.split-left { display: flex; flex-direction: column; gap: 10px; overflow-y: auto; min-height: 0; padding-bottom: 20px; }
|
|
121
|
+
.split-right { min-height: 0; overflow-y: auto; padding-bottom: 20px; }
|
|
122
|
+
|
|
123
|
+
/* ── Section header ── */
|
|
124
|
+
.section-header { display: flex; justify-content: space-between;
|
|
125
|
+
align-items: center; margin-bottom: 10px; }
|
|
126
|
+
.section-title { font-size: 12px; color: var(--muted);
|
|
127
|
+
letter-spacing: 1px; text-transform: uppercase; }
|
|
128
|
+
|
|
129
|
+
/* ── Progress screen ── */
|
|
130
|
+
#progress-screen {
|
|
131
|
+
position: fixed; inset: 0; background: var(--bg);
|
|
132
|
+
display: flex; flex-direction: column;
|
|
133
|
+
align-items: center; justify-content: center; gap: 20px; z-index: 100;
|
|
134
|
+
}
|
|
135
|
+
.progress-title { font-size: 16px; color: var(--green); }
|
|
136
|
+
.progress-bar-wrap { width: 320px; background: var(--bg3);
|
|
137
|
+
border-radius: 4px; height: 6px; }
|
|
138
|
+
.progress-bar { height: 6px; background: var(--green);
|
|
139
|
+
border-radius: 4px; width: 0%; transition: width 0.3s; }
|
|
140
|
+
.progress-pct { font-size: 14px; color: var(--muted); }
|
|
141
|
+
|
|
142
|
+
/* ── Scrollbar ── */
|
|
143
|
+
::-webkit-scrollbar { width: 6px; }
|
|
144
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
145
|
+
::-webkit-scrollbar-thumb { background: var(--bg3); border-radius: 3px; }
|
|
146
|
+
|
|
147
|
+
/* ── Util ── */
|
|
148
|
+
.green { color: var(--green); }
|
|
149
|
+
.cyan { color: var(--cyan); }
|
|
150
|
+
.amber { color: var(--amber); }
|
|
151
|
+
.red { color: var(--red); }
|
|
152
|
+
.purple { color: var(--purple); }
|
|
153
|
+
.muted { color: var(--muted); }
|
|
154
|
+
.hidden { display: none !important; }
|
|
155
|
+
</style>
|
|
156
|
+
</head>
|
|
157
|
+
<body>
|
|
158
|
+
|
|
159
|
+
<!-- 场景2:未检测到 Claude Code -->
|
|
160
|
+
<div id="empty-no-claude" style="display:none;position:fixed;inset:0;
|
|
161
|
+
display:none;align-items:center;justify-content:center;
|
|
162
|
+
background:var(--bg);flex-direction:column;gap:12px;text-align:center;padding:40px;">
|
|
163
|
+
<div style="font-size:20px;font-weight:700;color:var(--text);">未检测到 Claude Code</div>
|
|
164
|
+
<div style="font-size:13px;color:var(--muted);line-height:1.8;">
|
|
165
|
+
使用 Claude Code 后<br>即可生成你的使用画像
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
<!-- 场景1:有 Claude,但暂无数据 -->
|
|
170
|
+
<div id="empty-no-data" style="display:none;position:fixed;inset:0;
|
|
171
|
+
align-items:center;justify-content:center;
|
|
172
|
+
background:var(--bg);flex-direction:column;gap:12px;text-align:center;padding:40px;">
|
|
173
|
+
<div style="font-size:20px;font-weight:700;color:var(--text);">未检测到 Claude Code 使用记录</div>
|
|
174
|
+
<div style="font-size:13px;color:var(--muted);">请先使用一段时间再查看</div>
|
|
175
|
+
<div id="empty-scan-paths" style="font-size:12px;color:var(--muted);margin-top:4px;"></div>
|
|
176
|
+
<button id="empty-reindex-btn" style="margin-top:8px;background:transparent;
|
|
177
|
+
border:1px solid var(--border);color:var(--muted);border-radius:4px;
|
|
178
|
+
padding:6px 16px;font-size:13px;cursor:pointer;font-family:var(--font);">
|
|
179
|
+
重新检测
|
|
180
|
+
</button>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<!-- 首次建库进度屏 -->
|
|
184
|
+
<div id="progress-screen">
|
|
185
|
+
<div class="progress-title">CC Insight — 正在建立索引</div>
|
|
186
|
+
<div class="progress-bar-wrap">
|
|
187
|
+
<div class="progress-bar" id="progress-bar"></div>
|
|
188
|
+
</div>
|
|
189
|
+
<div class="progress-pct" id="progress-pct">0%</div>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<!-- 主布局 -->
|
|
193
|
+
<div class="layout" id="app">
|
|
194
|
+
<header class="topbar">
|
|
195
|
+
<span class="topbar-brand">CC Insight</span>
|
|
196
|
+
<div class="topbar-spacer"></div>
|
|
197
|
+
<nav class="tab-group" id="tab-group">
|
|
198
|
+
<button class="tab-btn active" data-view="overview">Overview</button>
|
|
199
|
+
<button class="tab-btn" data-view="insights">Insights</button>
|
|
200
|
+
<button class="tab-btn" data-view="skills">Skill & Agent</button>
|
|
201
|
+
<button class="tab-btn" data-view="mcp">MCP</button>
|
|
202
|
+
</nav>
|
|
203
|
+
<div class="topbar-spacer"></div>
|
|
204
|
+
<button class="poster-btn" id="poster-btn" title="生成分享海报">
|
|
205
|
+
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
206
|
+
<rect x="2" y="2" width="12" height="12" rx="1.5"/>
|
|
207
|
+
<path d="M8 5v6M5.5 7.5L8 5l2.5 2.5"/>
|
|
208
|
+
</svg>
|
|
209
|
+
生成海报
|
|
210
|
+
</button>
|
|
211
|
+
<span class="topbar-tool">Claude Code</span>
|
|
212
|
+
</header>
|
|
213
|
+
|
|
214
|
+
<div class="main">
|
|
215
|
+
<main class="content" id="content"></main>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<!-- 删除确认弹窗 -->
|
|
220
|
+
<div id="confirm-modal" style="display:none;position:fixed;inset:0;z-index:200;
|
|
221
|
+
background:rgba(0,0,0,0.6);align-items:center;justify-content:center;">
|
|
222
|
+
<div style="background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);
|
|
223
|
+
padding:24px;width:320px;font-family:var(--font);">
|
|
224
|
+
<div style="font-size:15px;color:var(--text);margin-bottom:8px;">确认删除</div>
|
|
225
|
+
<div id="confirm-msg" style="font-size:14px;color:var(--muted);margin-bottom:20px;"></div>
|
|
226
|
+
<div style="display:flex;gap:10px;justify-content:flex-end;">
|
|
227
|
+
<button id="confirm-cancel" style="background:transparent;border:1px solid var(--border);
|
|
228
|
+
color:var(--muted);border-radius:3px;padding:5px 16px;font-size:14px;
|
|
229
|
+
cursor:pointer;font-family:var(--font);">取消</button>
|
|
230
|
+
<button id="confirm-ok" style="background:color-mix(in srgb,var(--red) 15%,transparent);
|
|
231
|
+
border:1px solid var(--red);color:var(--red);border-radius:3px;
|
|
232
|
+
padding:5px 16px;font-size:14px;cursor:pointer;font-family:var(--font);">删除</button>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
<script type="module" src="js/app.js"></script>
|
|
238
|
+
</body>
|
|
239
|
+
</html>
|
package/public/js/app.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// public/js/app.js
|
|
2
|
+
import { initTheme } from './theme.js'
|
|
3
|
+
import { renderOverview } from './overview.js'
|
|
4
|
+
import { renderSkills, resetSkillsState } from './skills.js'
|
|
5
|
+
import { renderMcp } from './mcp.js'
|
|
6
|
+
import { renderInsightsPage } from './insights.js'
|
|
7
|
+
import { openPosterModal } from './poster.js'
|
|
8
|
+
|
|
9
|
+
// 当前时间范围,全局共享
|
|
10
|
+
export let currentRange = '7d'
|
|
11
|
+
|
|
12
|
+
// 当前视图
|
|
13
|
+
let currentView = 'overview'
|
|
14
|
+
|
|
15
|
+
// ── WebSocket ──
|
|
16
|
+
function connectWS() {
|
|
17
|
+
const ws = new WebSocket(`ws://${location.host}`)
|
|
18
|
+
|
|
19
|
+
ws.onmessage = (e) => {
|
|
20
|
+
const msg = JSON.parse(e.data)
|
|
21
|
+
|
|
22
|
+
if (msg.type === 'progress') {
|
|
23
|
+
const pct = msg.pct
|
|
24
|
+
document.getElementById('progress-bar').style.width = pct + '%'
|
|
25
|
+
document.getElementById('progress-pct').textContent = pct + '%'
|
|
26
|
+
if (pct >= 100) {
|
|
27
|
+
setTimeout(() => {
|
|
28
|
+
document.getElementById('progress-screen').classList.add('hidden')
|
|
29
|
+
document.getElementById('app').style.display = ''
|
|
30
|
+
renderView(currentView)
|
|
31
|
+
}, 500)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (msg.type === 'refresh') {
|
|
36
|
+
document.getElementById('progress-screen').classList.add('hidden')
|
|
37
|
+
// 重新检测按钮状态重置
|
|
38
|
+
const btn = document.getElementById('empty-reindex-btn')
|
|
39
|
+
if (btn) { btn.textContent = '重新检测'; btn.disabled = false }
|
|
40
|
+
checkStatusAndRender()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (msg.type === 'ready') {
|
|
44
|
+
document.getElementById('progress-screen').classList.add('hidden')
|
|
45
|
+
checkStatusAndRender()
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
ws.onclose = () => setTimeout(connectWS, 2000)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── 路由 ──
|
|
53
|
+
function renderView(view) {
|
|
54
|
+
const isTabSwitch = view !== currentView
|
|
55
|
+
if (isTabSwitch && view === 'skills') resetSkillsState()
|
|
56
|
+
currentView = view
|
|
57
|
+
const content = document.getElementById('content')
|
|
58
|
+
if (isTabSwitch) content.scrollTop = 0
|
|
59
|
+
|
|
60
|
+
document.querySelectorAll('#tab-group .tab-btn').forEach(btn => {
|
|
61
|
+
btn.classList.toggle('active', btn.dataset.view === view)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
if (view === 'overview') renderOverview(content, currentRange, !isTabSwitch)
|
|
65
|
+
if (view === 'insights') renderInsightsPage(content, currentRange)
|
|
66
|
+
if (view === 'skills') renderSkills(content, currentRange, !isTabSwitch)
|
|
67
|
+
if (view === 'mcp') renderMcp(content)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── 时间筛选 ──
|
|
71
|
+
export function setRange(range) {
|
|
72
|
+
currentRange = range
|
|
73
|
+
renderView(currentView)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── 状态检测 & 空视图 ──
|
|
77
|
+
async function checkStatusAndRender() {
|
|
78
|
+
const res = await fetch('/api/status')
|
|
79
|
+
const { hasClaude, hasData, scanPaths } = await res.json()
|
|
80
|
+
|
|
81
|
+
const elNoClaude = document.getElementById('empty-no-claude')
|
|
82
|
+
const elNoData = document.getElementById('empty-no-data')
|
|
83
|
+
const elApp = document.getElementById('app')
|
|
84
|
+
|
|
85
|
+
// 重置所有视图
|
|
86
|
+
elNoClaude.style.display = 'none'
|
|
87
|
+
elNoData.style.display = 'none'
|
|
88
|
+
elApp.style.display = 'none'
|
|
89
|
+
|
|
90
|
+
if (!hasClaude) {
|
|
91
|
+
elNoClaude.style.display = 'flex'
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!hasData) {
|
|
96
|
+
document.getElementById('empty-scan-paths').textContent =
|
|
97
|
+
'检测路径:' + scanPaths.join('、')
|
|
98
|
+
elNoData.style.display = 'flex'
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
elApp.style.display = ''
|
|
103
|
+
renderView(currentView)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── 初始化 ──
|
|
107
|
+
document.addEventListener('DOMContentLoaded', async () => {
|
|
108
|
+
await initTheme()
|
|
109
|
+
connectWS()
|
|
110
|
+
|
|
111
|
+
// 重新检测按钮
|
|
112
|
+
document.getElementById('empty-reindex-btn')?.addEventListener('click', async (e) => {
|
|
113
|
+
const btn = e.currentTarget
|
|
114
|
+
btn.textContent = '检测中…'
|
|
115
|
+
btn.disabled = true
|
|
116
|
+
await fetch('/api/reindex', { method: 'POST' })
|
|
117
|
+
// 结果通过 WS refresh 事件触发,不需要在这里轮询
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
// tab 导航
|
|
121
|
+
document.querySelectorAll('#tab-group .tab-btn').forEach(btn => {
|
|
122
|
+
btn.addEventListener('click', () => renderView(btn.dataset.view))
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
// 海报按钮
|
|
126
|
+
document.getElementById('poster-btn')?.addEventListener('click', () => {
|
|
127
|
+
openPosterModal(currentRange)
|
|
128
|
+
})
|
|
129
|
+
})
|
|
File without changes
|
|
File without changes
|