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,529 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { useNavigate } from 'react-router-dom'
|
|
3
|
+
import { ArrowRight, X, Flame, Zap, MessageSquare, Wrench, Share2, AlertTriangle, LayoutDashboard } from 'lucide-react'
|
|
4
|
+
import { Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement, PointElement, LineElement, Filler } from 'chart.js'
|
|
5
|
+
import { Doughnut, Bar, Line } from 'react-chartjs-2'
|
|
6
|
+
import KpiCard from '../components/KpiCard'
|
|
7
|
+
import ActivityHeatmap from '../components/ActivityHeatmap'
|
|
8
|
+
import DateRangePicker from '../components/DateRangePicker'
|
|
9
|
+
import { editorColor, editorLabel, formatNumber, formatCost, dateRangeToApiParams } from '../lib/constants'
|
|
10
|
+
import EditorIcon from '../components/EditorIcon'
|
|
11
|
+
import { fetchDailyActivity, fetchOverview as fetchOverviewApi, fetchDashboardStats, fetchChats, fetchCosts } from '../lib/api'
|
|
12
|
+
import ChatSidebar from '../components/ChatSidebar'
|
|
13
|
+
import AnimatedLoader from '../components/AnimatedLoader'
|
|
14
|
+
import ShareModal from '../components/ShareModal'
|
|
15
|
+
import { useTheme } from '../lib/theme'
|
|
16
|
+
import SectionTitle from '../components/SectionTitle'
|
|
17
|
+
import PageHeader from '../components/PageHeader'
|
|
18
|
+
|
|
19
|
+
ChartJS.register(ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement, PointElement, LineElement, Filler)
|
|
20
|
+
|
|
21
|
+
const MONO = 'JetBrains Mono, monospace'
|
|
22
|
+
const WEEKDAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
|
23
|
+
|
|
24
|
+
const MODE_COLORS = {
|
|
25
|
+
agent: '#a855f7', chat: '#3b82f6', cascade: '#06b6d4', edit: '#10b981',
|
|
26
|
+
copilot: '#f59e0b', thread: '#ec4899', opencode: '#f43f5e', claude: '#f97316',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default function Dashboard({ overview }) {
|
|
30
|
+
const navigate = useNavigate()
|
|
31
|
+
const [dailyData, setDailyData] = useState(null)
|
|
32
|
+
const [filteredData, setFilteredData] = useState(null)
|
|
33
|
+
const [stats, setStats] = useState(null)
|
|
34
|
+
const [selectedEditor, setSelectedEditor] = useState(null)
|
|
35
|
+
const [dateRange, setDateRange] = useState(null)
|
|
36
|
+
const { dark } = useTheme()
|
|
37
|
+
const [costs, setCosts] = useState(null)
|
|
38
|
+
const [shareOpen, setShareOpen] = useState(false)
|
|
39
|
+
const [largeContextChats, setLargeContextChats] = useState(null)
|
|
40
|
+
const [selectedChatId, setSelectedChatId] = useState(null)
|
|
41
|
+
const txtColor = dark ? '#888' : '#555'
|
|
42
|
+
const txtDim = dark ? '#555' : '#999'
|
|
43
|
+
const gridColor = dark ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.06)'
|
|
44
|
+
const legendColor = dark ? '#888' : '#555'
|
|
45
|
+
|
|
46
|
+
const chartOpts = {
|
|
47
|
+
responsive: true, maintainAspectRatio: false,
|
|
48
|
+
plugins: {
|
|
49
|
+
legend: { position: 'right', labels: { color: legendColor, font: { size: 10, family: MONO }, padding: 12, usePointStyle: true, pointStyle: 'circle' } },
|
|
50
|
+
tooltip: { bodyFont: { family: MONO, size: 11 }, titleFont: { family: MONO, size: 11 } },
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
const barScales = {
|
|
54
|
+
x: { grid: { display: false }, ticks: { color: txtDim, font: { size: 8, family: MONO } } },
|
|
55
|
+
y: { grid: { color: gridColor }, ticks: { color: txtDim, font: { size: 8, family: MONO } }, beginAtZero: true },
|
|
56
|
+
}
|
|
57
|
+
const noLegend = { legend: { display: false }, tooltip: { bodyFont: { family: MONO, size: 10 }, titleFont: { family: MONO, size: 10 } } }
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
const dateParams = dateRangeToApiParams(dateRange)
|
|
61
|
+
const chatParams = { limit: 500, named: false, ...dateParams }
|
|
62
|
+
if (selectedEditor) chatParams.editor = selectedEditor
|
|
63
|
+
|
|
64
|
+
fetchChats(chatParams).then(r => {
|
|
65
|
+
const big = (r.chats || []).filter(c => c.bubbleCount >= 100).sort((a, b) => b.bubbleCount - a.bubbleCount)
|
|
66
|
+
setLargeContextChats(big)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
if (!selectedEditor) {
|
|
70
|
+
// A date range without an editor must still refresh the overview widgets
|
|
71
|
+
// (KPIs, monthly trend, top projects) — falling back to the all-time
|
|
72
|
+
// `overview` prop would disagree with the date-scoped heatmap/stats/costs.
|
|
73
|
+
const hasDates = dateParams.dateFrom || dateParams.dateTo
|
|
74
|
+
if (hasDates) fetchOverviewApi(dateParams).then(setFilteredData)
|
|
75
|
+
else setFilteredData(null)
|
|
76
|
+
fetchDailyActivity(dateParams).then(setDailyData)
|
|
77
|
+
fetchDashboardStats(dateParams).then(setStats)
|
|
78
|
+
fetchCosts(dateParams).then(setCosts)
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
Promise.all([
|
|
82
|
+
fetchOverviewApi({ editor: selectedEditor, ...dateParams }),
|
|
83
|
+
fetchDailyActivity({ editor: selectedEditor, ...dateParams }),
|
|
84
|
+
fetchDashboardStats({ editor: selectedEditor, ...dateParams }),
|
|
85
|
+
fetchCosts({ editor: selectedEditor, ...dateParams }),
|
|
86
|
+
]).then(([ov, daily, st, c]) => {
|
|
87
|
+
setFilteredData(ov)
|
|
88
|
+
setDailyData(daily)
|
|
89
|
+
setStats(st)
|
|
90
|
+
setCosts(c)
|
|
91
|
+
})
|
|
92
|
+
}, [selectedEditor, dateRange])
|
|
93
|
+
|
|
94
|
+
if (!overview) return <AnimatedLoader label="Loading dashboard..." />
|
|
95
|
+
|
|
96
|
+
const d = filteredData || overview
|
|
97
|
+
const allEditors = overview.editors.sort((a, b) => b.count - a.count)
|
|
98
|
+
const daysSpan = d.oldestChat && d.newestChat ? Math.max(1, Math.round((d.newestChat - d.oldestChat) / 86400000)) : 0
|
|
99
|
+
const thisMonth = d.byMonth.length > 0 ? d.byMonth[d.byMonth.length - 1] : null
|
|
100
|
+
const modes = Object.entries(d.byMode).sort((a, b) => b[1] - a[1])
|
|
101
|
+
const sel = selectedEditor ? allEditors.find(e => e.id === selectedEditor) : null
|
|
102
|
+
|
|
103
|
+
const editorChartData = {
|
|
104
|
+
labels: allEditors.map(e => editorLabel(e.id)),
|
|
105
|
+
datasets: [{ data: allEditors.map(e => e.count), backgroundColor: allEditors.map(e => editorColor(e.id)), borderWidth: 0, spacing: 2 }],
|
|
106
|
+
}
|
|
107
|
+
const modeChartData = {
|
|
108
|
+
labels: modes.map(e => e[0]),
|
|
109
|
+
datasets: [{ data: modes.map(e => e[1]), backgroundColor: modes.map(e => MODE_COLORS[e[0]] || '#6b7280'), borderWidth: 0 }],
|
|
110
|
+
}
|
|
111
|
+
const maxProject = d.topProjects.length > 0 ? d.topProjects[0].count : 1
|
|
112
|
+
|
|
113
|
+
// ── Stats-derived charts ──
|
|
114
|
+
const mt = stats?.monthlyTrend
|
|
115
|
+
const monthlyTrendData = mt && mt.months.length > 0 ? {
|
|
116
|
+
labels: mt.months.map(m => m.substring(2)), // "25-01" etc
|
|
117
|
+
datasets: mt.sources.map(src => ({
|
|
118
|
+
label: editorLabel(src),
|
|
119
|
+
data: mt.months.map(m => mt.data[m]?.[src] || 0),
|
|
120
|
+
backgroundColor: editorColor(src) + 'CC',
|
|
121
|
+
borderRadius: 2,
|
|
122
|
+
})),
|
|
123
|
+
} : null
|
|
124
|
+
|
|
125
|
+
const hourlyData = stats ? {
|
|
126
|
+
labels: stats.hourly.map((_, i) => `${String(i).padStart(2, '0')}`),
|
|
127
|
+
datasets: [{
|
|
128
|
+
data: stats.hourly,
|
|
129
|
+
backgroundColor: stats.hourly.map((v, i) => {
|
|
130
|
+
const peak = Math.max(...stats.hourly)
|
|
131
|
+
const ratio = peak > 0 ? v / peak : 0
|
|
132
|
+
return ratio > 0.75 ? '#6366f1' : ratio > 0.5 ? '#818cf8' : ratio > 0.25 ? '#a5b4fc' : '#c7d2fe50'
|
|
133
|
+
}),
|
|
134
|
+
borderRadius: 2,
|
|
135
|
+
}],
|
|
136
|
+
} : null
|
|
137
|
+
|
|
138
|
+
const weekdayData = stats ? {
|
|
139
|
+
labels: WEEKDAY_LABELS,
|
|
140
|
+
datasets: [{
|
|
141
|
+
data: stats.weekdays,
|
|
142
|
+
backgroundColor: stats.weekdays.map((_, i) => i === 0 || i === 6 ? '#f59e0b80' : '#6366f1'),
|
|
143
|
+
borderRadius: 3,
|
|
144
|
+
}],
|
|
145
|
+
} : null
|
|
146
|
+
|
|
147
|
+
const depthData = stats ? {
|
|
148
|
+
labels: Object.keys(stats.depthBuckets),
|
|
149
|
+
datasets: [{
|
|
150
|
+
data: Object.values(stats.depthBuckets),
|
|
151
|
+
backgroundColor: ['#ef444460', '#f97316', '#f59e0b', '#10b981', '#06b6d4', '#6366f1', '#a855f7'],
|
|
152
|
+
borderRadius: 3,
|
|
153
|
+
}],
|
|
154
|
+
} : null
|
|
155
|
+
|
|
156
|
+
const velocityData = stats?.velocity?.length > 1 ? {
|
|
157
|
+
labels: stats.velocity.map(v => v.month.substring(2)),
|
|
158
|
+
datasets: [{
|
|
159
|
+
label: 'Avg msgs/session',
|
|
160
|
+
data: stats.velocity.map(v => v.avgMsgs),
|
|
161
|
+
borderColor: '#6366f1',
|
|
162
|
+
backgroundColor: '#6366f120',
|
|
163
|
+
borderWidth: 2,
|
|
164
|
+
tension: 0.3,
|
|
165
|
+
pointRadius: 2,
|
|
166
|
+
pointHoverRadius: 4,
|
|
167
|
+
fill: true,
|
|
168
|
+
}],
|
|
169
|
+
} : null
|
|
170
|
+
|
|
171
|
+
const tk = stats?.tokens
|
|
172
|
+
const totalInputAll = tk ? tk.input + tk.cacheRead + (tk.cacheWrite || 0) : 0
|
|
173
|
+
const cacheHitRate = totalInputAll > 0 ? ((tk.cacheRead / totalInputAll) * 100).toFixed(1) : 0
|
|
174
|
+
const outputInputRatio = totalInputAll > 0 ? (tk.output / totalInputAll).toFixed(3) : 0
|
|
175
|
+
const avgMsgsPerSession = tk && tk.sessions > 0 ? (depthData ? (Object.values(stats.depthBuckets).reduce((s, v, i) => {
|
|
176
|
+
const labels = Object.keys(stats.depthBuckets)
|
|
177
|
+
const midpoints = [1, 3.5, 8, 15.5, 35.5, 75.5, 150]
|
|
178
|
+
return s + v * midpoints[i]
|
|
179
|
+
}, 0) / tk.sessions).toFixed(1) : '—') : '—'
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<div className="fade-in space-y-3">
|
|
184
|
+
{/* Top bar */}
|
|
185
|
+
<PageHeader icon={LayoutDashboard} title="Dashboard">
|
|
186
|
+
<div className="ml-auto flex items-center gap-3">
|
|
187
|
+
<DateRangePicker value={dateRange} onChange={setDateRange} />
|
|
188
|
+
<button
|
|
189
|
+
onClick={() => setShareOpen(true)}
|
|
190
|
+
className="flex items-center gap-1.5 px-3 py-1 text-[12px] rounded-md transition hover:opacity-80"
|
|
191
|
+
style={{ background: '#6366f1', color: '#fff' }}
|
|
192
|
+
>
|
|
193
|
+
<Share2 size={12} />
|
|
194
|
+
Share Stats
|
|
195
|
+
</button>
|
|
196
|
+
</div>
|
|
197
|
+
</PageHeader>
|
|
198
|
+
|
|
199
|
+
{/* Editor breakdown - compact row */}
|
|
200
|
+
<div className="card p-3">
|
|
201
|
+
<div className="flex items-center flex-wrap gap-1.5">
|
|
202
|
+
{allEditors.map(e => {
|
|
203
|
+
const isSelected = selectedEditor === e.id
|
|
204
|
+
return (
|
|
205
|
+
<button
|
|
206
|
+
key={e.id}
|
|
207
|
+
className="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-[12px] cursor-pointer transition rounded-sm"
|
|
208
|
+
style={{
|
|
209
|
+
border: isSelected ? `1.5px solid ${editorColor(e.id)}` : '1px solid var(--c-border)',
|
|
210
|
+
background: isSelected ? editorColor(e.id) + '15' : 'transparent',
|
|
211
|
+
opacity: selectedEditor && !isSelected ? 0.4 : 1,
|
|
212
|
+
color: 'var(--c-text)',
|
|
213
|
+
}}
|
|
214
|
+
onClick={() => setSelectedEditor(isSelected ? null : e.id)}
|
|
215
|
+
>
|
|
216
|
+
<EditorIcon source={e.id} size={14} />
|
|
217
|
+
<span style={{ color: 'var(--c-text2)' }}>{editorLabel(e.id)}</span>
|
|
218
|
+
<span className="font-bold" style={{ color: 'var(--c-white)' }}>{e.count}</span>
|
|
219
|
+
</button>
|
|
220
|
+
)
|
|
221
|
+
})}
|
|
222
|
+
</div>
|
|
223
|
+
{selectedEditor && sel && (
|
|
224
|
+
<div className="mt-2 flex items-center gap-2">
|
|
225
|
+
<button onClick={() => navigate(`/sessions?editor=${selectedEditor}`)} className="flex items-center gap-1 text-[12px] px-2.5 py-1 transition" style={{ color: 'var(--c-accent)', border: '1px solid var(--c-border)' }}>
|
|
226
|
+
Show Sessions <ArrowRight size={11} />
|
|
227
|
+
</button>
|
|
228
|
+
<button onClick={() => setSelectedEditor(null)} className="flex items-center gap-1 text-[12px] px-2.5 py-1 transition" style={{ color: 'var(--c-text2)', border: '1px solid var(--c-border)' }}>
|
|
229
|
+
<X size={9} /> Clear
|
|
230
|
+
</button>
|
|
231
|
+
</div>
|
|
232
|
+
)}
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
{/* KPIs — compact single row */}
|
|
237
|
+
<div className="grid gap-2" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(90px, 1fr))' }}>
|
|
238
|
+
<KpiCard label="sessions" value={formatNumber(d.totalChats)} sub={sel ? editorLabel(sel.id) : `${allEditors.length} editors`} onClick={() => navigate(selectedEditor ? `/sessions?editor=${selectedEditor}` : '/sessions')} />
|
|
239
|
+
<KpiCard label="projects" value={d.topProjects.length} sub={`${daysSpan}d span`} onClick={() => navigate('/projects')} />
|
|
240
|
+
<KpiCard label="this month" value={thisMonth ? thisMonth.count : 0} sub={thisMonth ? thisMonth.month : ''} onClick={() => navigate('/sessions')} />
|
|
241
|
+
{stats && <>
|
|
242
|
+
<KpiCard label="avg depth" value={avgMsgsPerSession} sub={<span className="flex items-center gap-0.5"><MessageSquare size={8} /> msgs/session</span>} />
|
|
243
|
+
<KpiCard label="tool calls" value={formatNumber(stats.totalToolCalls)} sub={<span className="flex items-center gap-0.5"><Wrench size={8} /> total</span>} />
|
|
244
|
+
</>}
|
|
245
|
+
{tk && tk.input > 0 && <>
|
|
246
|
+
<KpiCard label="tokens in" value={formatNumber(tk.input)} sub="prompt" />
|
|
247
|
+
<KpiCard label="tokens out" value={formatNumber(tk.output)} sub={`${outputInputRatio}× ratio`} />
|
|
248
|
+
<KpiCard label="cache hit" value={`${cacheHitRate}%`} sub={formatNumber(tk.cacheRead)} />
|
|
249
|
+
<KpiCard label="you wrote" value={formatNumber(tk.userChars)} sub={`AI: ${formatNumber(tk.assistantChars)}`} />
|
|
250
|
+
</>}
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
{/* Token economy KPIs */}
|
|
254
|
+
{tk && tk.input > 0 && (
|
|
255
|
+
<div className="grid gap-2" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(90px, 1fr))' }}>
|
|
256
|
+
<KpiCard label="in tokens" value={formatNumber(tk.input)} sub="total prompt" />
|
|
257
|
+
<KpiCard label="out tokens" value={formatNumber(tk.output)} sub="total completion" />
|
|
258
|
+
<KpiCard label="cache read" value={formatNumber(tk.cacheRead)} sub={`${cacheHitRate}% hit rate`} />
|
|
259
|
+
<KpiCard label="cache write" value={formatNumber(tk.cacheWrite)} />
|
|
260
|
+
<KpiCard label="out/in ratio" value={`${outputInputRatio}×`} sub={<span className="flex items-center gap-0.5"><Zap size={8} /> efficiency</span>} />
|
|
261
|
+
<KpiCard label="you wrote" value={formatNumber(tk.userChars)} sub={`AI wrote ${formatNumber(tk.assistantChars)}`} />
|
|
262
|
+
<KpiCard label="est. cost" value={costs && costs.totalCost > 0 ? formatCost(costs.totalCost) : '—'} sub={costs && costs.byModel.length > 0 ? `${costs.byModel.length} model${costs.byModel.length !== 1 ? 's' : ''}` : undefined} />
|
|
263
|
+
</div>
|
|
264
|
+
)}
|
|
265
|
+
|
|
266
|
+
{/* Activity Heatmap | Col 1 | Col 2 | Col 3 */}
|
|
267
|
+
<div className="card p-3">
|
|
268
|
+
<SectionTitle>agentic coding activity</SectionTitle>
|
|
269
|
+
<div className="flex gap-4">
|
|
270
|
+
<div className="min-w-0 flex-shrink-0">
|
|
271
|
+
{dailyData ? <ActivityHeatmap dailyData={dailyData} /> : <div className="text-[11px]" style={{ color: 'var(--c-text3)' }}>loading...</div>}
|
|
272
|
+
</div>
|
|
273
|
+
{stats && dailyData && (() => {
|
|
274
|
+
const activeDays = dailyData.filter(d => d.total > 0)
|
|
275
|
+
const busiest = activeDays.length > 0 ? activeDays.reduce((a, b) => a.total > b.total ? a : b) : null
|
|
276
|
+
const totalSessions = activeDays.reduce((s, d) => s + d.total, 0)
|
|
277
|
+
const avgPerDay = activeDays.length > 0 ? (totalSessions / activeDays.length).toFixed(1) : 0
|
|
278
|
+
return (
|
|
279
|
+
<div className="flex-1 grid grid-cols-3 gap-3 text-[11px] min-w-0" style={{ borderLeft: '1px solid var(--c-border)', paddingLeft: 16 }}>
|
|
280
|
+
<div className="space-y-2 min-w-0">
|
|
281
|
+
<div>
|
|
282
|
+
<div style={{ color: 'var(--c-text3)' }} className="uppercase tracking-wider mb-1">streaks</div>
|
|
283
|
+
<div className="flex items-center gap-1">
|
|
284
|
+
<Flame size={10} className="text-orange-400 flex-shrink-0" />
|
|
285
|
+
<span style={{ color: 'var(--c-white)' }} className="font-bold">{stats.streaks.current}d</span>
|
|
286
|
+
<span style={{ color: 'var(--c-text3)' }}>now</span>
|
|
287
|
+
</div>
|
|
288
|
+
<div className="flex items-center gap-1 mt-0.5">
|
|
289
|
+
<Zap size={10} className="text-yellow-400 flex-shrink-0" />
|
|
290
|
+
<span style={{ color: 'var(--c-white)' }} className="font-bold">{stats.streaks.longest}d</span>
|
|
291
|
+
<span style={{ color: 'var(--c-text3)' }}>best</span>
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
<div>
|
|
295
|
+
<div style={{ color: 'var(--c-text3)' }} className="uppercase tracking-wider mb-1">active days</div>
|
|
296
|
+
<span style={{ color: 'var(--c-white)' }} className="font-bold">{stats.streaks.totalDays}</span>
|
|
297
|
+
<span className="ml-1" style={{ color: 'var(--c-text3)' }}>{avgPerDay}/day</span>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
<div className="space-y-2 min-w-0">
|
|
301
|
+
{busiest && (
|
|
302
|
+
<div>
|
|
303
|
+
<div style={{ color: 'var(--c-text3)' }} className="uppercase tracking-wider mb-1">busiest day</div>
|
|
304
|
+
<div style={{ color: 'var(--c-white)' }} className="font-bold">{busiest.day}</div>
|
|
305
|
+
<div style={{ color: 'var(--c-text3)' }}>{busiest.total} sessions</div>
|
|
306
|
+
</div>
|
|
307
|
+
)}
|
|
308
|
+
<div>
|
|
309
|
+
<div style={{ color: 'var(--c-text3)' }} className="uppercase tracking-wider mb-1">peak hour</div>
|
|
310
|
+
<div style={{ color: 'var(--c-white)' }} className="font-bold">{String(stats.hourly.indexOf(Math.max(...stats.hourly))).padStart(2, '0')}:00</div>
|
|
311
|
+
<div style={{ color: 'var(--c-text3)' }}>{Math.max(...stats.hourly)} sessions</div>
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
<div className="min-w-0">
|
|
315
|
+
<div style={{ color: 'var(--c-text3)' }} className="uppercase tracking-wider mb-1">top modes</div>
|
|
316
|
+
<div className="space-y-1">
|
|
317
|
+
{modes.slice(0, 5).map(([mode, count]) => (
|
|
318
|
+
<div key={mode} className="flex items-center gap-1">
|
|
319
|
+
<div className="w-1.5 h-1.5 rounded-full flex-shrink-0" style={{ background: MODE_COLORS[mode] || '#6b7280' }} />
|
|
320
|
+
<span className="truncate" style={{ color: 'var(--c-text)' }}>{mode}</span>
|
|
321
|
+
<span className="ml-auto font-bold flex-shrink-0" style={{ color: 'var(--c-white)' }}>{formatNumber(count)}</span>
|
|
322
|
+
</div>
|
|
323
|
+
))}
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
)
|
|
328
|
+
})()}
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
|
|
332
|
+
{/* Monthly trend (stacked bar by editor) */}
|
|
333
|
+
{monthlyTrendData && (
|
|
334
|
+
<div className="card p-3">
|
|
335
|
+
<SectionTitle>monthly trend <span style={{ color: 'var(--c-text3)' }}>(sessions by editor)</span></SectionTitle>
|
|
336
|
+
<div style={{ height: 200 }}>
|
|
337
|
+
<Bar data={monthlyTrendData} options={{
|
|
338
|
+
responsive: true, maintainAspectRatio: false,
|
|
339
|
+
scales: {
|
|
340
|
+
x: { stacked: true, grid: { display: false }, ticks: { color: txtDim, font: { size: 8, family: MONO }, maxRotation: 0 } },
|
|
341
|
+
y: { stacked: true, grid: { color: gridColor }, ticks: { color: txtDim, font: { size: 8, family: MONO } }, beginAtZero: true },
|
|
342
|
+
},
|
|
343
|
+
plugins: {
|
|
344
|
+
legend: { position: 'top', labels: { color: legendColor, font: { size: 9, family: MONO }, usePointStyle: true, pointStyle: 'circle', padding: 8 } },
|
|
345
|
+
tooltip: { mode: 'index', bodyFont: { family: MONO, size: 10 }, titleFont: { family: MONO, size: 10 } },
|
|
346
|
+
},
|
|
347
|
+
}} />
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
)}
|
|
351
|
+
|
|
352
|
+
{/* Behavior row: hourly, weekday, depth */}
|
|
353
|
+
{stats && (
|
|
354
|
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-2">
|
|
355
|
+
{/* Peak hours */}
|
|
356
|
+
<div className="card p-3">
|
|
357
|
+
<SectionTitle>peak hours <span style={{ color: 'var(--c-text3)' }}>(when you code with AI)</span></SectionTitle>
|
|
358
|
+
<div style={{ height: 160 }}>
|
|
359
|
+
{hourlyData && <Bar data={hourlyData} options={{
|
|
360
|
+
responsive: true, maintainAspectRatio: false,
|
|
361
|
+
scales: {
|
|
362
|
+
x: { grid: { display: false }, ticks: { color: txtDim, font: { size: 7, family: MONO }, maxRotation: 0, callback: (v, i) => i % 3 === 0 ? `${String(i).padStart(2, '0')}` : '' } },
|
|
363
|
+
y: { grid: { color: gridColor }, ticks: { color: txtDim, font: { size: 8, family: MONO } }, beginAtZero: true },
|
|
364
|
+
},
|
|
365
|
+
plugins: noLegend,
|
|
366
|
+
}} />}
|
|
367
|
+
</div>
|
|
368
|
+
</div>
|
|
369
|
+
|
|
370
|
+
{/* Weekday pattern */}
|
|
371
|
+
<div className="card p-3">
|
|
372
|
+
<SectionTitle>weekday pattern <span style={{ color: 'var(--c-text3)' }}>(weekends highlighted)</span></SectionTitle>
|
|
373
|
+
<div style={{ height: 160 }}>
|
|
374
|
+
{weekdayData && <Bar data={weekdayData} options={{
|
|
375
|
+
responsive: true, maintainAspectRatio: false,
|
|
376
|
+
scales: barScales,
|
|
377
|
+
plugins: noLegend,
|
|
378
|
+
}} />}
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
|
|
382
|
+
{/* Session depth */}
|
|
383
|
+
<div className="card p-3">
|
|
384
|
+
<SectionTitle>session depth <span style={{ color: 'var(--c-text3)' }}>(messages per session)</span></SectionTitle>
|
|
385
|
+
<div style={{ height: 160 }}>
|
|
386
|
+
{depthData && <Bar data={depthData} options={{
|
|
387
|
+
responsive: true, maintainAspectRatio: false,
|
|
388
|
+
scales: barScales,
|
|
389
|
+
plugins: noLegend,
|
|
390
|
+
}} />}
|
|
391
|
+
</div>
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
)}
|
|
395
|
+
|
|
396
|
+
{/* Velocity + Editors/Modes + Projects */}
|
|
397
|
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-2">
|
|
398
|
+
{/* Conversation velocity trend */}
|
|
399
|
+
{velocityData ? (
|
|
400
|
+
<div className="card p-3">
|
|
401
|
+
<SectionTitle>conversation velocity <span style={{ color: 'var(--c-text3)' }}>(avg msgs/session)</span></SectionTitle>
|
|
402
|
+
<div style={{ height: 180 }}>
|
|
403
|
+
<Line data={velocityData} options={{
|
|
404
|
+
responsive: true, maintainAspectRatio: false,
|
|
405
|
+
scales: {
|
|
406
|
+
x: { grid: { display: false }, ticks: { color: txtDim, font: { size: 8, family: MONO }, maxRotation: 0 } },
|
|
407
|
+
y: { grid: { color: gridColor }, ticks: { color: txtDim, font: { size: 8, family: MONO } }, beginAtZero: true },
|
|
408
|
+
},
|
|
409
|
+
plugins: noLegend,
|
|
410
|
+
}} />
|
|
411
|
+
</div>
|
|
412
|
+
</div>
|
|
413
|
+
) : (
|
|
414
|
+
<div className="card p-3">
|
|
415
|
+
<SectionTitle>editors</SectionTitle>
|
|
416
|
+
<div style={{ height: 180 }}>
|
|
417
|
+
<Doughnut data={editorChartData} options={{ ...chartOpts, cutout: '65%' }} />
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
)}
|
|
421
|
+
|
|
422
|
+
{/* Modes */}
|
|
423
|
+
<div className="card p-3">
|
|
424
|
+
<SectionTitle>modes</SectionTitle>
|
|
425
|
+
<div style={{ height: 180 }}>
|
|
426
|
+
<Doughnut data={modeChartData} options={{ ...chartOpts, cutout: '60%' }} />
|
|
427
|
+
</div>
|
|
428
|
+
</div>
|
|
429
|
+
|
|
430
|
+
{/* Top projects */}
|
|
431
|
+
<div className="card p-3">
|
|
432
|
+
<SectionTitle>top projects</SectionTitle>
|
|
433
|
+
<div className="space-y-1 max-h-[180px] overflow-y-auto scrollbar-thin">
|
|
434
|
+
{d.topProjects.slice(0, 12).map(p => (
|
|
435
|
+
<div key={p.name} className="flex items-center gap-1.5">
|
|
436
|
+
<div className="text-[10px] w-6 text-right" style={{ color: 'var(--c-text2)' }}>{p.count}</div>
|
|
437
|
+
<div className="flex-1 h-3 rounded-sm overflow-hidden" style={{ background: 'var(--c-code-bg)' }}>
|
|
438
|
+
<div className="h-full bg-accent/30 rounded-sm" style={{ width: `${(p.count / maxProject * 100).toFixed(1)}%` }} />
|
|
439
|
+
</div>
|
|
440
|
+
<div className="text-[10px] truncate max-w-[140px]" style={{ color: 'var(--c-text2)' }} title={p.fullPath}>{p.name}</div>
|
|
441
|
+
</div>
|
|
442
|
+
))}
|
|
443
|
+
</div>
|
|
444
|
+
</div>
|
|
445
|
+
</div>
|
|
446
|
+
|
|
447
|
+
{/* Bottom row: Top models + Top tools + Large context */}
|
|
448
|
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-2">
|
|
449
|
+
{stats && stats.topModels.length > 0 && (
|
|
450
|
+
<div className="card p-3">
|
|
451
|
+
<SectionTitle>top models {costs && costs.totalCost > 0 && <span style={{ color: 'var(--c-text3)' }}>({formatCost(costs.totalCost)} est.)</span>}</SectionTitle>
|
|
452
|
+
<div className="space-y-1">
|
|
453
|
+
{stats.topModels.map((m, i) => {
|
|
454
|
+
const maxM = stats.topModels[0].count
|
|
455
|
+
const modelCost = costs?.byModel?.find(c => c.model === m.name)
|
|
456
|
+
return (
|
|
457
|
+
<div key={m.name} className="flex items-center gap-2">
|
|
458
|
+
<span className="text-[10px] w-3 text-right" style={{ color: 'var(--c-text3)' }}>{i + 1}</span>
|
|
459
|
+
<span className="text-[9px] truncate w-28" style={{ color: 'var(--c-text2)' }} title={m.name}>{m.name}</span>
|
|
460
|
+
<div className="flex-1 h-4 rounded-sm overflow-hidden" style={{ background: 'var(--c-code-bg)' }}>
|
|
461
|
+
<div className="h-full rounded-sm" style={{ width: `${(m.count / maxM * 100).toFixed(1)}%`, background: i === 0 ? '#6366f1' : i === 1 ? '#818cf8' : '#a5b4fc40' }} />
|
|
462
|
+
</div>
|
|
463
|
+
{modelCost ? <span className="text-[9px] w-12 text-right" style={{ color: '#10b981' }}>{formatCost(modelCost.cost)}</span> : null}
|
|
464
|
+
<span className="text-[10px] w-8 text-right" style={{ color: 'var(--c-text3)' }}>{m.count}</span>
|
|
465
|
+
</div>
|
|
466
|
+
)
|
|
467
|
+
})}
|
|
468
|
+
</div>
|
|
469
|
+
</div>
|
|
470
|
+
)}
|
|
471
|
+
|
|
472
|
+
{stats && stats.topTools.length > 0 && (
|
|
473
|
+
<div className="card p-3">
|
|
474
|
+
<SectionTitle>top tools <span style={{ color: 'var(--c-text3)' }}>({formatNumber(stats.totalToolCalls)} total)</span></SectionTitle>
|
|
475
|
+
<div className="space-y-1">
|
|
476
|
+
{stats.topTools.map((t, i) => {
|
|
477
|
+
const maxT = stats.topTools[0].count
|
|
478
|
+
return (
|
|
479
|
+
<div key={t.name} className="flex items-center gap-2">
|
|
480
|
+
<span className="text-[10px] w-3 text-right" style={{ color: 'var(--c-text3)' }}>{i + 1}</span>
|
|
481
|
+
<div className="flex-1 h-4 rounded-sm overflow-hidden" style={{ background: 'var(--c-code-bg)' }}>
|
|
482
|
+
<div className="h-full rounded-sm flex items-center px-1.5" style={{ width: `${(t.count / maxT * 100).toFixed(1)}%`, background: i === 0 ? '#10b981' : i === 1 ? '#34d399' : '#6ee7b740' }}>
|
|
483
|
+
<span className="text-[8px] truncate font-mono" style={{ color: i < 2 ? '#fff' : 'var(--c-text2)' }}>{t.name}</span>
|
|
484
|
+
</div>
|
|
485
|
+
</div>
|
|
486
|
+
<span className="text-[10px] w-8 text-right" style={{ color: 'var(--c-text3)' }}>{formatNumber(t.count)}</span>
|
|
487
|
+
</div>
|
|
488
|
+
)
|
|
489
|
+
})}
|
|
490
|
+
</div>
|
|
491
|
+
</div>
|
|
492
|
+
)}
|
|
493
|
+
|
|
494
|
+
{largeContextChats && largeContextChats.length > 0 && (
|
|
495
|
+
<div className="card p-3">
|
|
496
|
+
<SectionTitle>
|
|
497
|
+
<span className="inline-flex items-center gap-1">
|
|
498
|
+
<AlertTriangle size={10} className="text-amber-400" />
|
|
499
|
+
large context
|
|
500
|
+
<span style={{ color: 'var(--c-text3)' }}>({largeContextChats.length})</span>
|
|
501
|
+
</span>
|
|
502
|
+
</SectionTitle>
|
|
503
|
+
<div className="space-y-1">
|
|
504
|
+
{largeContextChats.slice(0, 10).map(c => (
|
|
505
|
+
<div
|
|
506
|
+
key={c.id}
|
|
507
|
+
className="flex items-center gap-1.5 px-1.5 py-1 rounded-sm cursor-pointer transition hover:opacity-80"
|
|
508
|
+
style={{ background: c.bubbleCount >= 500 ? 'rgba(239,68,68,0.06)' : 'rgba(245,158,11,0.06)' }}
|
|
509
|
+
onClick={() => setSelectedChatId(c.id)}
|
|
510
|
+
>
|
|
511
|
+
<EditorIcon source={c.source} size={10} />
|
|
512
|
+
<span className="text-[10px] truncate flex-1" style={{ color: 'var(--c-text)' }}>{c.name || 'Untitled'}</span>
|
|
513
|
+
<span
|
|
514
|
+
className="text-[10px] font-bold flex-shrink-0"
|
|
515
|
+
style={{ color: c.bubbleCount >= 500 ? '#ef4444' : '#f59e0b' }}
|
|
516
|
+
>
|
|
517
|
+
{c.bubbleCount}
|
|
518
|
+
</span>
|
|
519
|
+
</div>
|
|
520
|
+
))}
|
|
521
|
+
</div>
|
|
522
|
+
</div>
|
|
523
|
+
)}
|
|
524
|
+
</div>
|
|
525
|
+
<ChatSidebar chatId={selectedChatId} onClose={() => setSelectedChatId(null)} />
|
|
526
|
+
<ShareModal open={shareOpen} onClose={() => setShareOpen(false)} />
|
|
527
|
+
</div>
|
|
528
|
+
)
|
|
529
|
+
}
|