memhouse 0.0.1 → 0.3.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/LICENSE +22 -0
- package/README.md +72 -3
- package/bin/memhouse.js +559 -0
- package/cache.js +850 -0
- package/ch-ingest.js +329 -0
- package/ch-schema.js +152 -0
- package/clickhouse.js +87 -0
- package/editors/adapter-errors.js +52 -0
- package/editors/antigravity.js +1048 -0
- package/editors/base.js +321 -0
- package/editors/claude.js +386 -0
- package/editors/codebuff.js +338 -0
- package/editors/codex.js +537 -0
- package/editors/commandcode.js +166 -0
- package/editors/copilot-jetbrains.js +207 -0
- package/editors/copilot.js +260 -0
- package/editors/cursor-agent.js +202 -0
- package/editors/cursor.js +498 -0
- package/editors/gemini.js +231 -0
- package/editors/goose.js +386 -0
- package/editors/gsd.js +366 -0
- package/editors/index.js +274 -0
- package/editors/kiro.js +315 -0
- package/editors/opencode.js +432 -0
- package/editors/vscode.js +413 -0
- package/editors/windsurf.js +634 -0
- package/editors/zed.js +229 -0
- package/index.js +424 -2
- package/mcp-server.js +294 -0
- package/mem-house/DESIGN.md +119 -0
- package/mem-house/delivery/AGENT-INSTALL.md +87 -0
- package/mem-house/delivery/PROMPT.md +42 -0
- package/mem-house/delivery/install.sh +181 -0
- package/mem-house/delivery/kernel-install.md +83 -0
- package/mem-house/delivery/plugin/.claude-plugin/plugin.json +8 -0
- package/mem-house/delivery/skills/memhouse-search/SKILL.md +104 -0
- package/mem-house/delivery/skills/memhouse-sessions/SKILL.md +94 -0
- package/mem-house/delivery/skills/memhouse-sql/SKILL.md +115 -0
- package/mem-house/rls.sql +17 -0
- package/mem-house/schema.sql +145 -0
- package/mem-house/server/queries.js +878 -0
- package/mem-house/server/server.js +228 -0
- package/mem-house/shipper/ship.js +461 -0
- package/misc/syntax-check.js +38 -0
- package/package.json +59 -7
- package/pricing.js +88 -0
- package/pricing.json +992 -0
- package/public/assets/index-CCPh7a2Z.js +75 -0
- package/public/assets/index-DV6ONi_F.css +2 -0
- package/public/index.html +16 -0
- package/relay-client.js +306 -0
- package/relay-server.js +577 -0
- package/server.js +860 -0
- package/share-image.js +367 -0
- package/ui/eslint.config.js +29 -0
- package/ui/index.html +15 -0
- package/ui/package-lock.json +4229 -0
- package/ui/package.json +38 -0
- package/ui/src/App.jsx +364 -0
- package/ui/src/components/ActivityHeatmap.jsx +206 -0
- package/ui/src/components/AiAuditCard.jsx +254 -0
- package/ui/src/components/AnimatedLoader.jsx +14 -0
- package/ui/src/components/AnimatedLogo.jsx +96 -0
- package/ui/src/components/ChatSidebar.jsx +226 -0
- package/ui/src/components/DateRangePicker.jsx +104 -0
- package/ui/src/components/EditorDot.jsx +11 -0
- package/ui/src/components/EditorIcon.jsx +117 -0
- package/ui/src/components/KpiCard.jsx +9 -0
- package/ui/src/components/LiveFeed.jsx +128 -0
- package/ui/src/components/LoginScreen.jsx +81 -0
- package/ui/src/components/MessageRenderer.jsx +148 -0
- package/ui/src/components/PageHeader.jsx +11 -0
- package/ui/src/components/SectionTitle.jsx +3 -0
- package/ui/src/components/ShareModal.jsx +305 -0
- package/ui/src/components/TokenTimeline.jsx +258 -0
- package/ui/src/hooks/useLive.jsx +15 -0
- package/ui/src/index.css +83 -0
- package/ui/src/lib/api.js +343 -0
- package/ui/src/lib/constants.js +88 -0
- package/ui/src/lib/theme.jsx +23 -0
- package/ui/src/main.jsx +16 -0
- package/ui/src/pages/Artifacts.jsx +575 -0
- package/ui/src/pages/Compare.jsx +232 -0
- package/ui/src/pages/CostAnalysis.jsx +354 -0
- package/ui/src/pages/Dashboard.jsx +529 -0
- package/ui/src/pages/DeepAnalysis.jsx +495 -0
- package/ui/src/pages/GSD.jsx +726 -0
- package/ui/src/pages/MCPs.jsx +744 -0
- package/ui/src/pages/ProjectDetail.jsx +325 -0
- package/ui/src/pages/Projects.jsx +249 -0
- package/ui/src/pages/RelayDashboard.jsx +590 -0
- package/ui/src/pages/RelayUserDetail.jsx +466 -0
- package/ui/src/pages/Sessions.jsx +535 -0
- package/ui/src/pages/Settings.jsx +241 -0
- package/ui/src/pages/SqlViewer.jsx +370 -0
- package/ui/src/pages/Subscriptions.jsx +411 -0
- package/ui/vite.config.js +18 -0
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
import { useState, useEffect, useRef, useMemo } from 'react'
|
|
2
|
+
import { Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement } from 'chart.js'
|
|
3
|
+
import { Doughnut, Bar } from 'react-chartjs-2'
|
|
4
|
+
import { Loader2, X, Zap, MessageSquare, Wrench, Cpu, TrendingUp } from 'lucide-react'
|
|
5
|
+
import { fetchDeepAnalytics, fetchToolCalls, fetchCosts } from '../lib/api'
|
|
6
|
+
import { editorLabel, editorColor, formatNumber, formatCost, dateRangeToApiParams } from '../lib/constants'
|
|
7
|
+
import { useTheme } from '../lib/theme'
|
|
8
|
+
import KpiCard from '../components/KpiCard'
|
|
9
|
+
import EditorIcon from '../components/EditorIcon'
|
|
10
|
+
import SectionTitle from '../components/SectionTitle'
|
|
11
|
+
import DateRangePicker from '../components/DateRangePicker'
|
|
12
|
+
import PageHeader from '../components/PageHeader'
|
|
13
|
+
|
|
14
|
+
ChartJS.register(ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement)
|
|
15
|
+
|
|
16
|
+
const MODEL_COLORS = ['#6366f1', '#a78bfa', '#818cf8', '#c084fc', '#e879f9', '#f472b6', '#fb7185', '#f87171', '#fbbf24', '#34d399']
|
|
17
|
+
const TOOL_COLORS = ['#10b981', '#34d399', '#6ee7b7', '#a7f3d0', '#d1fae5', '#ecfdf5', '#b8f0d8', '#7ce0b8', '#4ade80', '#22c55e']
|
|
18
|
+
const MONO = 'JetBrains Mono, monospace'
|
|
19
|
+
|
|
20
|
+
// Categorize tools into groups
|
|
21
|
+
const TOOL_CATEGORIES = {
|
|
22
|
+
'File Operations': ['read_file', 'write_to_file', 'edit', 'multi_edit', 'Read', 'Write', 'EditFile', 'edit_file', 'create_file', 'read_notebook', 'edit_notebook'],
|
|
23
|
+
'Search': ['grep_search', 'find_by_name', 'code_search', 'search', 'list_dir', 'Grep', 'Find', 'SearchFiles', 'ListDir'],
|
|
24
|
+
'Terminal': ['run_command', 'command_status', 'RunCommand', 'execute_command', 'Bash', 'bash'],
|
|
25
|
+
'Browser': ['browser_preview', 'read_url_content', 'view_content_chunk'],
|
|
26
|
+
'AI Tools': ['mcp0_', 'mcp1_', 'mcp5_', 'mcp6_', 'skill', 'trajectory_search'],
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function categorizeTools(tools) {
|
|
30
|
+
const cats = {}
|
|
31
|
+
for (const t of tools) {
|
|
32
|
+
let found = false
|
|
33
|
+
for (const [cat, patterns] of Object.entries(TOOL_CATEGORIES)) {
|
|
34
|
+
if (patterns.some(p => t.name.startsWith(p) || t.name === p || t.name.toLowerCase().includes(p.toLowerCase()))) {
|
|
35
|
+
if (!cats[cat]) cats[cat] = { tools: [], total: 0 }
|
|
36
|
+
cats[cat].tools.push(t)
|
|
37
|
+
cats[cat].total += t.count
|
|
38
|
+
found = true
|
|
39
|
+
break
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (!found) {
|
|
43
|
+
if (!cats['Other']) cats['Other'] = { tools: [], total: 0 }
|
|
44
|
+
cats['Other'].tools.push(t)
|
|
45
|
+
cats['Other'].total += t.count
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return Object.entries(cats).sort((a, b) => b[1].total - a[1].total)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Summarize args for display
|
|
52
|
+
function summarizeArgs(toolName, args) {
|
|
53
|
+
if (!args || typeof args !== 'object') return null
|
|
54
|
+
if (args.CommandLine || args.command) return args.CommandLine || args.command
|
|
55
|
+
if (args.file_path) return args.file_path
|
|
56
|
+
if (args.TargetFile) return args.TargetFile
|
|
57
|
+
if (args.SearchPath || args.search_path) return `${args.Query || args.query || ''} in ${args.SearchPath || args.search_path || ''}`
|
|
58
|
+
if (args.Pattern) return `${args.Pattern} in ${args.SearchDirectory || ''}`
|
|
59
|
+
if (args.Url || args.url) return args.Url || args.url
|
|
60
|
+
if (args.query) return args.query
|
|
61
|
+
const vals = Object.values(args).filter(v => typeof v === 'string' && v.length > 0)
|
|
62
|
+
return vals.length > 0 ? vals[0].substring(0, 120) : null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getDiff(args) {
|
|
66
|
+
if (!args || typeof args !== 'object') return null
|
|
67
|
+
const old = args.old_string || args.old_text || args.oldText || args.search || null
|
|
68
|
+
const nw = args.new_string || args.new_text || args.newText || args.replace || null
|
|
69
|
+
const file = args.file_path || args.TargetFile || args.filePath || args.path || null
|
|
70
|
+
if (old != null || nw != null) return { old, new: nw, file }
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function DiffBlock({ diff }) {
|
|
75
|
+
const maxLines = 8
|
|
76
|
+
const oldLines = (diff.old || '').split('\n').slice(0, maxLines)
|
|
77
|
+
const newLines = (diff.new || '').split('\n').slice(0, maxLines)
|
|
78
|
+
return (
|
|
79
|
+
<div className="mt-1 text-[10px] font-mono overflow-x-auto" style={{ border: '1px solid var(--c-border)' }}>
|
|
80
|
+
{diff.file && (
|
|
81
|
+
<div className="px-2 py-0.5" style={{ background: 'var(--c-code-bg)', color: 'var(--c-text2)' }}>{diff.file}</div>
|
|
82
|
+
)}
|
|
83
|
+
{diff.old && oldLines.map((line, i) => (
|
|
84
|
+
<div key={'o' + i} className="px-2" style={{ background: 'rgba(248,113,113,0.07)', color: '#f87171' }}>
|
|
85
|
+
<span style={{ color: 'var(--c-text3)', userSelect: 'none' }}>- </span>{line}
|
|
86
|
+
</div>
|
|
87
|
+
))}
|
|
88
|
+
{diff.old && oldLines.length < (diff.old || '').split('\n').length && (
|
|
89
|
+
<div className="px-2" style={{ color: 'var(--c-text3)' }}> ... {(diff.old || '').split('\n').length - maxLines} more lines</div>
|
|
90
|
+
)}
|
|
91
|
+
{diff.new && newLines.map((line, i) => (
|
|
92
|
+
<div key={'n' + i} className="px-2" style={{ background: 'rgba(52,211,153,0.07)', color: '#34d399' }}>
|
|
93
|
+
<span style={{ color: 'var(--c-text3)', userSelect: 'none' }}>+ </span>{line}
|
|
94
|
+
</div>
|
|
95
|
+
))}
|
|
96
|
+
{diff.new && newLines.length < (diff.new || '').split('\n').length && (
|
|
97
|
+
<div className="px-2" style={{ color: 'var(--c-text3)' }}> ... {(diff.new || '').split('\n').length - maxLines} more lines</div>
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function ToolCallRow({ call, toolName, index }) {
|
|
104
|
+
const [expanded, setExpanded] = useState(false)
|
|
105
|
+
const summary = summarizeArgs(toolName, call.args)
|
|
106
|
+
const diff = getDiff(call.args)
|
|
107
|
+
const project = call.folder ? call.folder.split('/').pop() : null
|
|
108
|
+
const hasDetail = diff || (call.args && Object.keys(call.args).length > 0)
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div className="px-2 py-1 text-[11px]" style={{ background: index % 2 === 0 ? 'var(--c-code-bg)' : 'transparent' }}>
|
|
112
|
+
<div className="flex items-start gap-2 cursor-pointer" onClick={() => hasDetail && setExpanded(!expanded)}>
|
|
113
|
+
<EditorIcon source={call.source} size={10} />
|
|
114
|
+
<div className="flex-1 min-w-0">
|
|
115
|
+
{summary && (
|
|
116
|
+
<div className="font-mono truncate" style={{ color: 'var(--c-white)' }} title={summary}>{summary}</div>
|
|
117
|
+
)}
|
|
118
|
+
<div className="flex items-center gap-2" style={{ color: 'var(--c-text3)' }}>
|
|
119
|
+
<span>{editorLabel(call.source)}</span>
|
|
120
|
+
{project && <span>· {project}</span>}
|
|
121
|
+
{call.timestamp && <span>· {new Date(call.timestamp).toLocaleDateString()}</span>}
|
|
122
|
+
{hasDetail && <span>{expanded ? '▾' : '▸'}</span>}
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
{expanded && diff && <DiffBlock diff={diff} />}
|
|
127
|
+
{expanded && !diff && call.args && Object.keys(call.args).length > 0 && (
|
|
128
|
+
<pre className="mt-1 px-2 py-1 text-[10px] overflow-x-auto whitespace-pre-wrap break-all" style={{ background: 'var(--c-code-bg)', color: 'var(--c-text2)' }}>
|
|
129
|
+
{JSON.stringify(call.args, null, 2)}
|
|
130
|
+
</pre>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function ToolDrillDown({ toolName, folder, editor, dateRange, onClose }) {
|
|
137
|
+
const [calls, setCalls] = useState(null)
|
|
138
|
+
const [loading, setLoading] = useState(true)
|
|
139
|
+
const projectName = folder ? folder.split('/').pop() : null
|
|
140
|
+
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
setLoading(true)
|
|
143
|
+
// Same filters as the charts this drill-down was opened from — otherwise the
|
|
144
|
+
// call list can show other editors/dates than the clicked segment counted.
|
|
145
|
+
fetchToolCalls(toolName, {
|
|
146
|
+
folder: folder || undefined,
|
|
147
|
+
editor: editor || undefined,
|
|
148
|
+
...dateRangeToApiParams(dateRange),
|
|
149
|
+
}).then(data => { setCalls(data); setLoading(false) })
|
|
150
|
+
}, [toolName, folder, editor, dateRange])
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div className="card p-3 fade-in" style={{ borderColor: 'rgba(99,102,241,0.3)' }}>
|
|
154
|
+
<div className="flex items-center justify-between mb-2">
|
|
155
|
+
<div className="flex items-center gap-2">
|
|
156
|
+
<Wrench size={12} style={{ color: 'var(--c-accent)' }} />
|
|
157
|
+
<span className="text-xs font-bold" style={{ color: 'var(--c-white)' }}>{toolName}</span>
|
|
158
|
+
<span className="text-[11px]" style={{ color: 'var(--c-text2)' }}>
|
|
159
|
+
{calls ? `${calls.length} calls` : '...'}
|
|
160
|
+
{calls && projectName ? ` in ${projectName}` : ''}
|
|
161
|
+
</span>
|
|
162
|
+
</div>
|
|
163
|
+
<button onClick={onClose} className="p-0.5" style={{ color: 'var(--c-text2)' }}><X size={12} /></button>
|
|
164
|
+
</div>
|
|
165
|
+
{loading ? (
|
|
166
|
+
<div className="text-[11px] py-4 text-center" style={{ color: 'var(--c-text3)' }}>loading...</div>
|
|
167
|
+
) : calls && calls.length > 0 ? (
|
|
168
|
+
<div className="max-h-[500px] overflow-y-auto scrollbar-thin space-y-0.5">
|
|
169
|
+
{calls.map((c, i) => (
|
|
170
|
+
<ToolCallRow key={i} call={c} toolName={toolName} index={i} />
|
|
171
|
+
))}
|
|
172
|
+
</div>
|
|
173
|
+
) : (
|
|
174
|
+
<div className="text-[11px] py-4 text-center" style={{ color: 'var(--c-text3)' }}>no calls found</div>
|
|
175
|
+
)}
|
|
176
|
+
</div>
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Proportional bar component
|
|
181
|
+
function ProportionBar({ segments, height = 6 }) {
|
|
182
|
+
const total = segments.reduce((s, seg) => s + seg.value, 0)
|
|
183
|
+
if (total === 0) return null
|
|
184
|
+
return (
|
|
185
|
+
<div className="flex w-full rounded-full overflow-hidden" style={{ height }}>
|
|
186
|
+
{segments.filter(s => s.value > 0).map((seg, i) => (
|
|
187
|
+
<div
|
|
188
|
+
key={i}
|
|
189
|
+
title={`${seg.label}: ${formatNumber(seg.value)}`}
|
|
190
|
+
className="h-full transition-all"
|
|
191
|
+
style={{ width: `${(seg.value / total * 100).toFixed(1)}%`, background: seg.color }}
|
|
192
|
+
/>
|
|
193
|
+
))}
|
|
194
|
+
</div>
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export default function DeepAnalysis({ overview }) {
|
|
199
|
+
const [editor, setEditor] = useState('')
|
|
200
|
+
const [folder, setFolder] = useState('')
|
|
201
|
+
const [dateRange, setDateRange] = useState(null)
|
|
202
|
+
const [data, setData] = useState(null)
|
|
203
|
+
const [loading, setLoading] = useState(false)
|
|
204
|
+
const [selectedTool, setSelectedTool] = useState(null)
|
|
205
|
+
const [costs, setCosts] = useState(null)
|
|
206
|
+
const chartRef = useRef(null)
|
|
207
|
+
const { dark } = useTheme()
|
|
208
|
+
const txtColor = dark ? '#a0a0a0' : '#444'
|
|
209
|
+
const txtDim = dark ? '#555' : '#999'
|
|
210
|
+
const gridColor = dark ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.06)'
|
|
211
|
+
const legendColor = dark ? '#9ca3af' : '#555'
|
|
212
|
+
|
|
213
|
+
const editors = overview?.editors || []
|
|
214
|
+
const projects = overview?.topProjects || []
|
|
215
|
+
|
|
216
|
+
async function analyze() {
|
|
217
|
+
setLoading(true)
|
|
218
|
+
const dateParams = dateRangeToApiParams(dateRange)
|
|
219
|
+
const [result, costData] = await Promise.all([
|
|
220
|
+
fetchDeepAnalytics({ editor, folder: folder || undefined, limit: 500, ...dateParams }),
|
|
221
|
+
fetchCosts({ editor, folder: folder || undefined, ...dateParams }),
|
|
222
|
+
])
|
|
223
|
+
setData(result)
|
|
224
|
+
setCosts(costData)
|
|
225
|
+
setLoading(false)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
useEffect(() => { analyze() }, [editor, folder, dateRange])
|
|
229
|
+
|
|
230
|
+
const tools = data?.topTools?.slice(0, 15) || []
|
|
231
|
+
const models = data?.topModels?.slice(0, 10) || []
|
|
232
|
+
|
|
233
|
+
// Computed insights
|
|
234
|
+
const insights = useMemo(() => {
|
|
235
|
+
if (!data) return null
|
|
236
|
+
const totalTok = data.totalInputTokens + data.totalOutputTokens + data.totalCacheRead + data.totalCacheWrite
|
|
237
|
+
const msgsPerSession = data.analyzedChats > 0 ? (data.totalMessages / data.analyzedChats).toFixed(1) : 0
|
|
238
|
+
const toolsPerSession = data.analyzedChats > 0 ? (data.totalToolCalls / data.analyzedChats).toFixed(1) : 0
|
|
239
|
+
const tokPerMsg = data.totalMessages > 0 ? Math.round(totalTok / data.totalMessages) : 0
|
|
240
|
+
const totalInputAll = data.totalInputTokens + data.totalCacheRead + data.totalCacheWrite
|
|
241
|
+
const cacheHitRate = totalInputAll > 0 ? ((data.totalCacheRead / totalInputAll) * 100).toFixed(1) : 0
|
|
242
|
+
const outputRatio = totalInputAll > 0 ? (data.totalOutputTokens / totalInputAll).toFixed(3) : 0
|
|
243
|
+
const aiVsHuman = data.totalUserChars > 0 ? (data.totalAssistantChars / data.totalUserChars).toFixed(1) : 0
|
|
244
|
+
return { totalTok, msgsPerSession, toolsPerSession, tokPerMsg, cacheHitRate, outputRatio, aiVsHuman }
|
|
245
|
+
}, [data])
|
|
246
|
+
|
|
247
|
+
const toolCategories = useMemo(() => tools.length > 0 ? categorizeTools(tools) : [], [tools])
|
|
248
|
+
|
|
249
|
+
function handleToolClick(evt, elements) {
|
|
250
|
+
if (elements.length > 0) {
|
|
251
|
+
const idx = elements[0].index
|
|
252
|
+
const toolName = tools[idx]?.name
|
|
253
|
+
if (toolName) setSelectedTool(toolName)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<div className="fade-in space-y-3">
|
|
259
|
+
{/* Filters */}
|
|
260
|
+
<PageHeader icon={TrendingUp} title="Deep Analysis">
|
|
261
|
+
<select
|
|
262
|
+
value={editor}
|
|
263
|
+
onChange={e => setEditor(e.target.value)}
|
|
264
|
+
className="px-2 py-1 text-[12px] outline-none"
|
|
265
|
+
style={{ background: 'var(--c-bg3)', color: 'var(--c-text)', border: '1px solid var(--c-border)' }}
|
|
266
|
+
>
|
|
267
|
+
<option value="">All Editors</option>
|
|
268
|
+
{editors.map(e => (
|
|
269
|
+
<option key={e.id} value={e.id}>{editorLabel(e.id)}</option>
|
|
270
|
+
))}
|
|
271
|
+
</select>
|
|
272
|
+
<select
|
|
273
|
+
value={folder}
|
|
274
|
+
onChange={e => setFolder(e.target.value)}
|
|
275
|
+
className="px-2 py-1 text-[12px] outline-none max-w-[200px] truncate"
|
|
276
|
+
style={{ background: 'var(--c-bg3)', color: 'var(--c-text)', border: '1px solid var(--c-border)' }}
|
|
277
|
+
>
|
|
278
|
+
<option value="">All Projects</option>
|
|
279
|
+
{projects.map(p => (
|
|
280
|
+
<option key={p.fullPath || p.name} value={p.fullPath}>{p.name}</option>
|
|
281
|
+
))}
|
|
282
|
+
</select>
|
|
283
|
+
{loading && <Loader2 size={11} className="animate-spin" style={{ color: 'var(--c-text3)' }} />}
|
|
284
|
+
{data && <span className="text-[11px]" style={{ color: 'var(--c-text2)' }}>{data.analyzedChats} sessions analyzed</span>}
|
|
285
|
+
<div className="ml-auto"><DateRangePicker value={dateRange} onChange={setDateRange} /></div>
|
|
286
|
+
</PageHeader>
|
|
287
|
+
|
|
288
|
+
{data && insights && (
|
|
289
|
+
<>
|
|
290
|
+
{/* KPIs */}
|
|
291
|
+
<div className="grid gap-2" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(90px, 1fr))' }}>
|
|
292
|
+
<KpiCard label="sessions" value={data.analyzedChats} />
|
|
293
|
+
<KpiCard label="messages" value={formatNumber(data.totalMessages)} sub={`${insights.msgsPerSession}/session`} />
|
|
294
|
+
<KpiCard label="tool calls" value={formatNumber(data.totalToolCalls)} sub={`${insights.toolsPerSession}/session`} />
|
|
295
|
+
<KpiCard label="total tokens" value={formatNumber(insights.totalTok)} sub={`${formatNumber(insights.tokPerMsg)}/msg`} />
|
|
296
|
+
<KpiCard label="you wrote" value={formatNumber(data.totalUserChars)} sub={`AI: ${insights.aiVsHuman}\u00d7 more`} />
|
|
297
|
+
<KpiCard label="est. cost" value={costs && costs.totalCost > 0 ? formatCost(costs.totalCost) : '\u2014'} />
|
|
298
|
+
</div>
|
|
299
|
+
|
|
300
|
+
{/* Token flow + Insights row */}
|
|
301
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
|
|
302
|
+
{/* Token flow visualization */}
|
|
303
|
+
<div className="card p-3">
|
|
304
|
+
<SectionTitle>token flow</SectionTitle>
|
|
305
|
+
<div className="space-y-3 mt-2">
|
|
306
|
+
{/* Input tokens breakdown */}
|
|
307
|
+
<div>
|
|
308
|
+
<div className="flex items-center justify-between text-[11px] mb-1">
|
|
309
|
+
<span style={{ color: 'var(--c-text2)' }}>input tokens</span>
|
|
310
|
+
<span className="font-bold" style={{ color: 'var(--c-white)' }}>{formatNumber(data.totalInputTokens + data.totalCacheRead + data.totalCacheWrite)}</span>
|
|
311
|
+
</div>
|
|
312
|
+
<ProportionBar segments={[
|
|
313
|
+
{ label: 'Fresh input', value: data.totalInputTokens, color: '#6366f1' },
|
|
314
|
+
{ label: 'Cache write', value: data.totalCacheWrite, color: '#fbbf24' },
|
|
315
|
+
{ label: 'Cache read', value: data.totalCacheRead, color: '#34d399' },
|
|
316
|
+
]} />
|
|
317
|
+
<div className="flex items-center gap-3 mt-1 text-[10px]">
|
|
318
|
+
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#6366f1' }} /> fresh {formatNumber(data.totalInputTokens)}</span>
|
|
319
|
+
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#fbbf24' }} /> cache write {formatNumber(data.totalCacheWrite)}</span>
|
|
320
|
+
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#34d399' }} /> cache read {formatNumber(data.totalCacheRead)}</span>
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
{/* Output tokens */}
|
|
324
|
+
<div>
|
|
325
|
+
<div className="flex items-center justify-between text-[11px] mb-1">
|
|
326
|
+
<span style={{ color: 'var(--c-text2)' }}>output tokens</span>
|
|
327
|
+
<span className="font-bold" style={{ color: 'var(--c-white)' }}>{formatNumber(data.totalOutputTokens)}</span>
|
|
328
|
+
</div>
|
|
329
|
+
<ProportionBar segments={[
|
|
330
|
+
{ label: 'Output', value: data.totalOutputTokens, color: '#a78bfa' },
|
|
331
|
+
]} />
|
|
332
|
+
</div>
|
|
333
|
+
{/* Cache write */}
|
|
334
|
+
{data.totalCacheWrite > 0 && (
|
|
335
|
+
<div>
|
|
336
|
+
<div className="flex items-center justify-between text-[11px] mb-1">
|
|
337
|
+
<span style={{ color: 'var(--c-text2)' }}>cache write</span>
|
|
338
|
+
<span className="font-bold" style={{ color: 'var(--c-white)' }}>{formatNumber(data.totalCacheWrite)}</span>
|
|
339
|
+
</div>
|
|
340
|
+
<ProportionBar segments={[
|
|
341
|
+
{ label: 'Cache write', value: data.totalCacheWrite, color: '#fbbf24' },
|
|
342
|
+
]} />
|
|
343
|
+
</div>
|
|
344
|
+
)}
|
|
345
|
+
{/* Overall ratio bar */}
|
|
346
|
+
<div className="pt-2" style={{ borderTop: '1px solid var(--c-border)' }}>
|
|
347
|
+
<div className="text-[10px] mb-1" style={{ color: 'var(--c-text3)' }}>overall token distribution</div>
|
|
348
|
+
<ProportionBar height={10} segments={[
|
|
349
|
+
{ label: 'Input', value: data.totalInputTokens, color: '#6366f1' },
|
|
350
|
+
{ label: 'Output', value: data.totalOutputTokens, color: '#a78bfa' },
|
|
351
|
+
{ label: 'Cache Read', value: data.totalCacheRead, color: '#34d399' },
|
|
352
|
+
{ label: 'Cache Write', value: data.totalCacheWrite, color: '#fbbf24' },
|
|
353
|
+
]} />
|
|
354
|
+
<div className="flex items-center gap-3 mt-1 text-[10px]" style={{ color: 'var(--c-text3)' }}>
|
|
355
|
+
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#6366f1' }} /> in</span>
|
|
356
|
+
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#a78bfa' }} /> out</span>
|
|
357
|
+
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#34d399' }} /> cache read</span>
|
|
358
|
+
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#fbbf24' }} /> cache write</span>
|
|
359
|
+
</div>
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
|
|
364
|
+
{/* Efficiency insights */}
|
|
365
|
+
<div className="card p-3">
|
|
366
|
+
<SectionTitle>efficiency insights</SectionTitle>
|
|
367
|
+
<div className="space-y-3 mt-2">
|
|
368
|
+
{/* Human vs AI chars */}
|
|
369
|
+
<div>
|
|
370
|
+
<div className="text-[11px] mb-1" style={{ color: 'var(--c-text2)' }}>you vs AI (characters)</div>
|
|
371
|
+
<ProportionBar height={8} segments={[
|
|
372
|
+
{ label: 'You', value: data.totalUserChars, color: '#6366f1' },
|
|
373
|
+
{ label: 'AI', value: data.totalAssistantChars, color: '#34d399' },
|
|
374
|
+
]} />
|
|
375
|
+
<div className="flex items-center justify-between mt-1 text-[10px]">
|
|
376
|
+
<span style={{ color: '#6366f1' }}>You: {formatNumber(data.totalUserChars)}</span>
|
|
377
|
+
<span style={{ color: '#34d399' }}>AI: {formatNumber(data.totalAssistantChars)}</span>
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
|
|
381
|
+
{/* Metric cards */}
|
|
382
|
+
<div className="grid grid-cols-2 gap-2">
|
|
383
|
+
<div className="p-2 rounded-sm" style={{ background: 'var(--c-code-bg)' }}>
|
|
384
|
+
<div className="flex items-center gap-1 text-[10px]" style={{ color: 'var(--c-text3)' }}>
|
|
385
|
+
<TrendingUp size={9} /> output/input ratio
|
|
386
|
+
</div>
|
|
387
|
+
<div className="text-sm font-bold mt-0.5" style={{ color: 'var(--c-white)' }}>{insights.outputRatio}×</div>
|
|
388
|
+
</div>
|
|
389
|
+
<div className="p-2 rounded-sm" style={{ background: 'var(--c-code-bg)' }}>
|
|
390
|
+
<div className="flex items-center gap-1 text-[10px]" style={{ color: 'var(--c-text3)' }}>
|
|
391
|
+
<Zap size={9} /> cache hit rate
|
|
392
|
+
</div>
|
|
393
|
+
<div className="text-sm font-bold mt-0.5" style={{ color: parseFloat(insights.cacheHitRate) > 50 ? '#34d399' : parseFloat(insights.cacheHitRate) > 20 ? '#fbbf24' : 'var(--c-white)' }}>{insights.cacheHitRate}%</div>
|
|
394
|
+
</div>
|
|
395
|
+
<div className="p-2 rounded-sm" style={{ background: 'var(--c-code-bg)' }}>
|
|
396
|
+
<div className="flex items-center gap-1 text-[10px]" style={{ color: 'var(--c-text3)' }}>
|
|
397
|
+
<MessageSquare size={9} /> tokens per message
|
|
398
|
+
</div>
|
|
399
|
+
<div className="text-sm font-bold mt-0.5" style={{ color: 'var(--c-white)' }}>{formatNumber(insights.tokPerMsg)}</div>
|
|
400
|
+
</div>
|
|
401
|
+
<div className="p-2 rounded-sm" style={{ background: 'var(--c-code-bg)' }}>
|
|
402
|
+
<div className="flex items-center gap-1 text-[10px]" style={{ color: 'var(--c-text3)' }}>
|
|
403
|
+
<Wrench size={9} /> tools per session
|
|
404
|
+
</div>
|
|
405
|
+
<div className="text-sm font-bold mt-0.5" style={{ color: 'var(--c-white)' }}>{insights.toolsPerSession}</div>
|
|
406
|
+
</div>
|
|
407
|
+
</div>
|
|
408
|
+
|
|
409
|
+
{/* AI amplification */}
|
|
410
|
+
<div className="p-2 rounded-sm text-center" style={{ background: 'var(--c-code-bg)' }}>
|
|
411
|
+
<div className="text-[10px]" style={{ color: 'var(--c-text3)' }}>AI amplification factor</div>
|
|
412
|
+
<div className="text-lg font-bold" style={{ color: 'var(--c-accent)' }}>{insights.aiVsHuman}×</div>
|
|
413
|
+
<div className="text-[10px]" style={{ color: 'var(--c-text3)' }}>AI writes {insights.aiVsHuman}× more than you type</div>
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
418
|
+
|
|
419
|
+
{/* Charts row: Tools + Models + Tool Categories */}
|
|
420
|
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
|
|
421
|
+
{/* Tools bar */}
|
|
422
|
+
<div className="card p-3 lg:col-span-2">
|
|
423
|
+
<SectionTitle>most used tools <span style={{ color: 'var(--c-text3)' }}>(click to drill down)</span></SectionTitle>
|
|
424
|
+
<div style={{ height: Math.max(tools.length * 20 + 10, 120) }}>
|
|
425
|
+
{tools.length > 0 ? (
|
|
426
|
+
<Bar
|
|
427
|
+
ref={chartRef}
|
|
428
|
+
data={{
|
|
429
|
+
labels: tools.map(t => t.name),
|
|
430
|
+
datasets: [{ data: tools.map(t => t.count), backgroundColor: TOOL_COLORS.concat(MODEL_COLORS).slice(0, tools.length), borderRadius: 2 }],
|
|
431
|
+
}}
|
|
432
|
+
options={{
|
|
433
|
+
indexAxis: 'y',
|
|
434
|
+
responsive: true,
|
|
435
|
+
maintainAspectRatio: false,
|
|
436
|
+
onClick: handleToolClick,
|
|
437
|
+
scales: {
|
|
438
|
+
x: { grid: { color: gridColor }, ticks: { color: txtDim, font: { size: 8, family: MONO } } },
|
|
439
|
+
y: { grid: { display: false }, ticks: { color: txtColor, font: { size: 8, family: MONO } } },
|
|
440
|
+
},
|
|
441
|
+
plugins: { legend: { display: false }, tooltip: { bodyFont: { family: MONO, size: 10 }, titleFont: { family: MONO, size: 10 } } },
|
|
442
|
+
}}
|
|
443
|
+
/>
|
|
444
|
+
) : <div className="text-[11px] text-center py-8" style={{ color: 'var(--c-text3)' }}>no tool calls found</div>}
|
|
445
|
+
</div>
|
|
446
|
+
</div>
|
|
447
|
+
|
|
448
|
+
{/* Models doughnut */}
|
|
449
|
+
<div className="card p-3">
|
|
450
|
+
<SectionTitle>models</SectionTitle>
|
|
451
|
+
<div style={{ height: 160 }}>
|
|
452
|
+
{models.length > 0 ? (
|
|
453
|
+
<Doughnut
|
|
454
|
+
data={{
|
|
455
|
+
labels: models.map(m => m.name),
|
|
456
|
+
datasets: [{ data: models.map(m => m.count), backgroundColor: MODEL_COLORS, borderWidth: 0 }],
|
|
457
|
+
}}
|
|
458
|
+
options={{
|
|
459
|
+
responsive: true, maintainAspectRatio: false, cutout: '60%',
|
|
460
|
+
plugins: {
|
|
461
|
+
legend: { position: 'right', labels: { color: legendColor, font: { size: 8, family: MONO }, usePointStyle: true, pointStyle: 'circle', padding: 6 } },
|
|
462
|
+
tooltip: { bodyFont: { family: MONO, size: 10 }, titleFont: { family: MONO, size: 10 } },
|
|
463
|
+
},
|
|
464
|
+
}}
|
|
465
|
+
/>
|
|
466
|
+
) : <div className="text-[11px] text-center py-8" style={{ color: 'var(--c-text3)' }}>no model data</div>}
|
|
467
|
+
</div>
|
|
468
|
+
|
|
469
|
+
{/* Tool categories below models */}
|
|
470
|
+
{toolCategories.length > 0 && (
|
|
471
|
+
<div className="mt-3 pt-3" style={{ borderTop: '1px solid var(--c-border)' }}>
|
|
472
|
+
<div className="text-[10px] uppercase tracking-wider mb-2" style={{ color: 'var(--c-text3)' }}>tool categories</div>
|
|
473
|
+
<div className="space-y-1.5">
|
|
474
|
+
{toolCategories.map(([cat, { tools: catTools, total }]) => (
|
|
475
|
+
<div key={cat} className="flex items-center gap-2 text-[11px]">
|
|
476
|
+
<span className="truncate flex-1" style={{ color: 'var(--c-text2)' }}>{cat}</span>
|
|
477
|
+
<span className="font-bold" style={{ color: 'var(--c-white)' }}>{total}</span>
|
|
478
|
+
<span style={{ color: 'var(--c-text3)' }}>({catTools.length})</span>
|
|
479
|
+
</div>
|
|
480
|
+
))}
|
|
481
|
+
</div>
|
|
482
|
+
</div>
|
|
483
|
+
)}
|
|
484
|
+
</div>
|
|
485
|
+
</div>
|
|
486
|
+
|
|
487
|
+
{/* Tool drill-down */}
|
|
488
|
+
{selectedTool && (
|
|
489
|
+
<ToolDrillDown toolName={selectedTool} folder={folder} editor={editor} dateRange={dateRange} onClose={() => setSelectedTool(null)} />
|
|
490
|
+
)}
|
|
491
|
+
</>
|
|
492
|
+
)}
|
|
493
|
+
</div>
|
|
494
|
+
)
|
|
495
|
+
}
|