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,466 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useMemo } from 'react'
|
|
2
|
+
import { useParams, useNavigate } from 'react-router-dom'
|
|
3
|
+
import { ArrowLeft, MessageSquare, FolderOpen, ChevronDown, ChevronRight, Hash } from 'lucide-react'
|
|
4
|
+
import { Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement } from 'chart.js'
|
|
5
|
+
import { Doughnut, Bar } from 'react-chartjs-2'
|
|
6
|
+
import KpiCard from '../components/KpiCard'
|
|
7
|
+
import EditorIcon from '../components/EditorIcon'
|
|
8
|
+
import SectionTitle from '../components/SectionTitle'
|
|
9
|
+
import ChatSidebar from '../components/ChatSidebar'
|
|
10
|
+
import LiveFeed from '../components/LiveFeed'
|
|
11
|
+
import { editorColor, editorLabel, formatNumber, formatDate } from '../lib/constants'
|
|
12
|
+
import { fetchRelayUserActivity, fetchRelaySession } from '../lib/api'
|
|
13
|
+
import { useTheme } from '../lib/theme'
|
|
14
|
+
|
|
15
|
+
ChartJS.register(ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement)
|
|
16
|
+
|
|
17
|
+
const MONO = 'JetBrains Mono, monospace'
|
|
18
|
+
const MODEL_COLORS = ['#6366f1', '#a78bfa', '#818cf8', '#c084fc', '#e879f9', '#f472b6', '#fb7185', '#f87171', '#fbbf24', '#34d399']
|
|
19
|
+
|
|
20
|
+
function ProportionBar({ segments, height = 6 }) {
|
|
21
|
+
const total = segments.reduce((s, seg) => s + seg.value, 0)
|
|
22
|
+
if (total === 0) return null
|
|
23
|
+
return (
|
|
24
|
+
<div className="flex w-full rounded-full overflow-hidden" style={{ height }}>
|
|
25
|
+
{segments.filter(s => s.value > 0).map((seg, i) => (
|
|
26
|
+
<div
|
|
27
|
+
key={i}
|
|
28
|
+
title={`${seg.label}: ${formatNumber(seg.value)}`}
|
|
29
|
+
className="h-full transition-all"
|
|
30
|
+
style={{ width: `${(seg.value / total * 100).toFixed(1)}%`, background: seg.color }}
|
|
31
|
+
/>
|
|
32
|
+
))}
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Left sidebar: sessions grouped by project (folder-tree)
|
|
38
|
+
function SessionSidebar({ sessions, projects, selectedChat, onSelectChat }) {
|
|
39
|
+
const [collapsed, setCollapsed] = useState(new Set())
|
|
40
|
+
|
|
41
|
+
const toggle = (key) => {
|
|
42
|
+
setCollapsed(prev => {
|
|
43
|
+
const next = new Set(prev)
|
|
44
|
+
next.has(key) ? next.delete(key) : next.add(key)
|
|
45
|
+
return next
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Group sessions by project
|
|
50
|
+
const grouped = useMemo(() => {
|
|
51
|
+
const map = {}
|
|
52
|
+
const noProject = []
|
|
53
|
+
for (const s of sessions) {
|
|
54
|
+
if (s.folder) {
|
|
55
|
+
if (!map[s.folder]) map[s.folder] = []
|
|
56
|
+
map[s.folder].push(s)
|
|
57
|
+
} else {
|
|
58
|
+
noProject.push(s)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const sorted = Object.entries(map).sort((a, b) => b[1].length - a[1].length)
|
|
62
|
+
return { sorted, noProject }
|
|
63
|
+
}, [sessions])
|
|
64
|
+
|
|
65
|
+
const SessionItem = ({ s }) => (
|
|
66
|
+
<div
|
|
67
|
+
className="flex items-center gap-1.5 px-2 py-1.5 cursor-pointer transition rounded-sm"
|
|
68
|
+
style={{ background: selectedChat === s.id ? 'rgba(99,102,241,0.12)' : 'transparent' }}
|
|
69
|
+
onMouseEnter={e => { if (selectedChat !== s.id) e.currentTarget.style.background = 'var(--c-bg3)' }}
|
|
70
|
+
onMouseLeave={e => { if (selectedChat !== s.id) e.currentTarget.style.background = 'transparent' }}
|
|
71
|
+
onClick={() => onSelectChat(s.id)}
|
|
72
|
+
>
|
|
73
|
+
<EditorIcon source={s.source} size={10} />
|
|
74
|
+
<div className="flex-1 min-w-0">
|
|
75
|
+
<div className="text-[11px] font-medium truncate" style={{ color: 'var(--c-white)' }}>{s.name || 'Untitled'}</div>
|
|
76
|
+
<div className="flex items-center gap-1 text-[8px]" style={{ color: 'var(--c-text3)' }}>
|
|
77
|
+
{s.totalMessages > 0 && <span>{s.totalMessages}m</span>}
|
|
78
|
+
{s.totalMessages > 0 && s.lastUpdatedAt && <span>·</span>}
|
|
79
|
+
{s.lastUpdatedAt && <span>{formatDate(s.lastUpdatedAt)}</span>}
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div className="flex flex-col h-full">
|
|
87
|
+
<div className="flex items-center gap-2 px-3 py-2.5 shrink-0" style={{ borderBottom: '1px solid var(--c-border)' }}>
|
|
88
|
+
<Hash size={12} style={{ color: 'var(--c-accent)' }} />
|
|
89
|
+
<span className="text-[12px] font-medium uppercase tracking-wider" style={{ color: 'var(--c-text2)' }}>Sessions</span>
|
|
90
|
+
<span className="text-[10px] ml-auto" style={{ color: 'var(--c-text3)' }}>{sessions.length}</span>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div className="flex-1 overflow-y-auto scrollbar-thin py-1">
|
|
94
|
+
{grouped.sorted.map(([folder, list]) => {
|
|
95
|
+
const isCollapsed = collapsed.has(folder)
|
|
96
|
+
const folderName = folder.split('/').pop()
|
|
97
|
+
return (
|
|
98
|
+
<div key={folder}>
|
|
99
|
+
<div
|
|
100
|
+
className="flex items-center gap-1.5 px-2 py-1.5 cursor-pointer transition"
|
|
101
|
+
onMouseEnter={e => e.currentTarget.style.background = 'var(--c-bg3)'}
|
|
102
|
+
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
|
|
103
|
+
onClick={() => toggle(folder)}
|
|
104
|
+
>
|
|
105
|
+
{isCollapsed ? <ChevronRight size={10} style={{ color: 'var(--c-text3)' }} /> : <ChevronDown size={10} style={{ color: 'var(--c-text3)' }} />}
|
|
106
|
+
<FolderOpen size={10} style={{ color: '#818cf8' }} />
|
|
107
|
+
<span className="text-[11px] font-medium truncate flex-1" style={{ color: 'var(--c-text2)' }} title={folder}>{folderName}</span>
|
|
108
|
+
<span className="text-[8px]" style={{ color: 'var(--c-text3)' }}>{list.length}</span>
|
|
109
|
+
</div>
|
|
110
|
+
{!isCollapsed && (
|
|
111
|
+
<div className="pl-3">
|
|
112
|
+
{list.map(s => <SessionItem key={s.id} s={s} />)}
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
)
|
|
117
|
+
})}
|
|
118
|
+
|
|
119
|
+
{grouped.noProject.length > 0 && (
|
|
120
|
+
<div>
|
|
121
|
+
{grouped.sorted.length > 0 && (
|
|
122
|
+
<div className="px-2 py-1.5 text-[10px] uppercase tracking-wider" style={{ color: 'var(--c-text3)' }}>no project</div>
|
|
123
|
+
)}
|
|
124
|
+
<div className={grouped.sorted.length > 0 ? 'pl-1' : ''}>
|
|
125
|
+
{grouped.noProject.map(s => <SessionItem key={s.id} s={s} />)}
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export default function RelayUserDetail() {
|
|
135
|
+
const { username } = useParams()
|
|
136
|
+
const navigate = useNavigate()
|
|
137
|
+
const [sessions, setSessions] = useState(null)
|
|
138
|
+
const [selectedChat, setSelectedChat] = useState(null)
|
|
139
|
+
const { dark } = useTheme()
|
|
140
|
+
|
|
141
|
+
const legendColor = dark ? '#888' : '#555'
|
|
142
|
+
const gridColor = dark ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.06)'
|
|
143
|
+
const txtDim = dark ? '#555' : '#999'
|
|
144
|
+
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
if (username) {
|
|
147
|
+
fetchRelayUserActivity(username, { limit: 200 }).then(setSessions)
|
|
148
|
+
}
|
|
149
|
+
}, [username])
|
|
150
|
+
|
|
151
|
+
const fetchFn = useCallback(
|
|
152
|
+
(id) => fetchRelaySession(id, username),
|
|
153
|
+
[username]
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
if (!sessions) return <div className="text-sm py-12 text-center" style={{ color: 'var(--c-text2)' }}>loading...</div>
|
|
157
|
+
|
|
158
|
+
// Aggregate stats
|
|
159
|
+
const totalSessions = sessions.length
|
|
160
|
+
const totalMessages = sessions.reduce((s, c) => s + (c.totalMessages || 0), 0)
|
|
161
|
+
const totalInputTokens = sessions.reduce((s, c) => s + (c.totalInputTokens || 0), 0)
|
|
162
|
+
const totalOutputTokens = sessions.reduce((s, c) => s + (c.totalOutputTokens || 0), 0)
|
|
163
|
+
const totalTokens = totalInputTokens + totalOutputTokens
|
|
164
|
+
const msgsPerSession = totalSessions > 0 ? (totalMessages / totalSessions).toFixed(1) : 0
|
|
165
|
+
const tokPerSession = totalSessions > 0 ? Math.round(totalTokens / totalSessions) : 0
|
|
166
|
+
|
|
167
|
+
// Editor breakdown
|
|
168
|
+
const editorMap = {}
|
|
169
|
+
for (const s of sessions) {
|
|
170
|
+
if (s.source) editorMap[s.source] = (editorMap[s.source] || 0) + 1
|
|
171
|
+
}
|
|
172
|
+
const editors = Object.entries(editorMap).sort((a, b) => b[1] - a[1])
|
|
173
|
+
|
|
174
|
+
// Project breakdown
|
|
175
|
+
const projectMap = {}
|
|
176
|
+
for (const s of sessions) {
|
|
177
|
+
if (s.folder) {
|
|
178
|
+
if (!projectMap[s.folder]) projectMap[s.folder] = { count: 0, lastActive: 0, messages: 0, tokens: 0 }
|
|
179
|
+
projectMap[s.folder].count++
|
|
180
|
+
projectMap[s.folder].messages += s.totalMessages || 0
|
|
181
|
+
projectMap[s.folder].tokens += (s.totalInputTokens || 0) + (s.totalOutputTokens || 0)
|
|
182
|
+
if (s.lastUpdatedAt > projectMap[s.folder].lastActive) projectMap[s.folder].lastActive = s.lastUpdatedAt
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const projects = Object.entries(projectMap).sort((a, b) => b[1].count - a[1].count)
|
|
186
|
+
const maxProjectSessions = projects.length > 0 ? Math.max(...projects.map(([, p]) => p.count)) : 1
|
|
187
|
+
|
|
188
|
+
// Model breakdown
|
|
189
|
+
const modelMap = {}
|
|
190
|
+
for (const s of sessions) {
|
|
191
|
+
if (s.models) {
|
|
192
|
+
for (const m of s.models) modelMap[m] = (modelMap[m] || 0) + 1
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const models = Object.entries(modelMap).sort((a, b) => b[1] - a[1]).slice(0, 10)
|
|
196
|
+
|
|
197
|
+
// Tool breakdown
|
|
198
|
+
const toolMap = {}
|
|
199
|
+
for (const s of sessions) {
|
|
200
|
+
if (s.toolCalls) {
|
|
201
|
+
for (const t of s.toolCalls) toolMap[t] = (toolMap[t] || 0) + 1
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const tools = Object.entries(toolMap).sort((a, b) => b[1] - a[1]).slice(0, 15)
|
|
205
|
+
|
|
206
|
+
// Daily activity (sessions per day)
|
|
207
|
+
const dayMap = {}
|
|
208
|
+
for (const s of sessions) {
|
|
209
|
+
const d = new Date(s.lastUpdatedAt || s.createdAt).toISOString().slice(0, 10)
|
|
210
|
+
dayMap[d] = (dayMap[d] || 0) + 1
|
|
211
|
+
}
|
|
212
|
+
const days = Object.entries(dayMap).sort((a, b) => a[0].localeCompare(b[0])).slice(-14)
|
|
213
|
+
|
|
214
|
+
const handleFeedClick = (chatId) => {
|
|
215
|
+
setSelectedChat(chatId)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const sidebarH = 'calc(100vh - 42px)'
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
<div className="fade-in flex" style={{ height: sidebarH }}>
|
|
222
|
+
{/* ── Left sidebar: Sessions tree ── */}
|
|
223
|
+
<div
|
|
224
|
+
className="hidden lg:flex flex-col w-[250px] shrink-0 sticky top-[42px] self-start"
|
|
225
|
+
style={{ height: sidebarH, borderRight: '1px solid var(--c-border)', background: 'var(--c-bg)' }}
|
|
226
|
+
>
|
|
227
|
+
<SessionSidebar sessions={sessions} projects={projects} selectedChat={selectedChat} onSelectChat={setSelectedChat} />
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
{/* ── Center: scrollable content ── */}
|
|
231
|
+
<div className="flex-1 min-w-0 overflow-y-auto p-4 space-y-3">
|
|
232
|
+
{/* Header */}
|
|
233
|
+
<div className="flex items-center gap-3">
|
|
234
|
+
<button
|
|
235
|
+
onClick={() => navigate('/')}
|
|
236
|
+
className="p-1.5 transition hover:bg-[var(--c-card)] rounded-sm"
|
|
237
|
+
style={{ border: '1px solid var(--c-border)', color: 'var(--c-text2)' }}
|
|
238
|
+
>
|
|
239
|
+
<ArrowLeft size={14} />
|
|
240
|
+
</button>
|
|
241
|
+
<div className="flex items-center gap-2.5">
|
|
242
|
+
<div className="w-9 h-9 flex items-center justify-center text-[14px] font-bold rounded-sm" style={{ background: 'rgba(99,102,241,0.15)', color: '#818cf8' }}>
|
|
243
|
+
{username.charAt(0).toUpperCase()}
|
|
244
|
+
</div>
|
|
245
|
+
<div>
|
|
246
|
+
<div className="text-[14px] font-bold" style={{ color: 'var(--c-white)' }}>{username}</div>
|
|
247
|
+
<div className="flex items-center gap-2 mt-0.5">
|
|
248
|
+
{editors.map(([src]) => (
|
|
249
|
+
<EditorIcon key={src} source={src} size={11} />
|
|
250
|
+
))}
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
{/* KPIs */}
|
|
257
|
+
<div className="grid gap-2" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(90px, 1fr))' }}>
|
|
258
|
+
<KpiCard label="sessions" value={totalSessions} />
|
|
259
|
+
<KpiCard label="messages" value={formatNumber(totalMessages)} sub={`${msgsPerSession}/session`} />
|
|
260
|
+
<KpiCard label="editors" value={editors.length} />
|
|
261
|
+
<KpiCard label="projects" value={projects.length} />
|
|
262
|
+
<KpiCard label="tokens" value={formatNumber(totalTokens)} sub={`${formatNumber(tokPerSession)}/session`} />
|
|
263
|
+
<KpiCard label="models" value={models.length} />
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
{/* Token overview */}
|
|
267
|
+
{totalTokens > 0 && (
|
|
268
|
+
<div className="card p-3">
|
|
269
|
+
<div className="flex items-center justify-between mb-2">
|
|
270
|
+
<SectionTitle>token usage</SectionTitle>
|
|
271
|
+
<span className="text-[11px] font-bold" style={{ color: 'var(--c-white)' }}>{formatNumber(totalTokens)} total</span>
|
|
272
|
+
</div>
|
|
273
|
+
<ProportionBar height={10} segments={[
|
|
274
|
+
{ label: 'Input', value: totalInputTokens, color: '#6366f1' },
|
|
275
|
+
{ label: 'Output', value: totalOutputTokens, color: '#a78bfa' },
|
|
276
|
+
]} />
|
|
277
|
+
<div className="flex items-center gap-4 mt-1.5 text-[10px]">
|
|
278
|
+
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#6366f1' }} /> input {formatNumber(totalInputTokens)}</span>
|
|
279
|
+
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#a78bfa' }} /> output {formatNumber(totalOutputTokens)}</span>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
)}
|
|
283
|
+
|
|
284
|
+
{/* Charts: editors + models */}
|
|
285
|
+
<div className="grid grid-cols-1 xl:grid-cols-2 gap-3">
|
|
286
|
+
{editors.length > 0 && (
|
|
287
|
+
<div className="card p-3">
|
|
288
|
+
<SectionTitle>editors</SectionTitle>
|
|
289
|
+
<div style={{ height: 160 }}>
|
|
290
|
+
<Doughnut
|
|
291
|
+
data={{
|
|
292
|
+
labels: editors.map(([src]) => editorLabel(src)),
|
|
293
|
+
datasets: [{ data: editors.map(([, c]) => c), backgroundColor: editors.map(([src]) => editorColor(src)), borderWidth: 0 }],
|
|
294
|
+
}}
|
|
295
|
+
options={{
|
|
296
|
+
responsive: true, maintainAspectRatio: false, cutout: '60%',
|
|
297
|
+
plugins: {
|
|
298
|
+
legend: { position: 'right', labels: { color: legendColor, font: { size: 9, family: MONO }, usePointStyle: true, pointStyle: 'circle', padding: 6 } },
|
|
299
|
+
tooltip: { bodyFont: { family: MONO, size: 10 }, titleFont: { family: MONO, size: 10 } },
|
|
300
|
+
},
|
|
301
|
+
}}
|
|
302
|
+
/>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
)}
|
|
306
|
+
{models.length > 0 && (
|
|
307
|
+
<div className="card p-3">
|
|
308
|
+
<SectionTitle>models</SectionTitle>
|
|
309
|
+
<div style={{ height: 160 }}>
|
|
310
|
+
<Doughnut
|
|
311
|
+
data={{
|
|
312
|
+
labels: models.map(([name]) => name),
|
|
313
|
+
datasets: [{ data: models.map(([, c]) => c), backgroundColor: MODEL_COLORS, borderWidth: 0 }],
|
|
314
|
+
}}
|
|
315
|
+
options={{
|
|
316
|
+
responsive: true, maintainAspectRatio: false, cutout: '60%',
|
|
317
|
+
plugins: {
|
|
318
|
+
legend: { position: 'right', labels: { color: legendColor, font: { size: 8, family: MONO }, usePointStyle: true, pointStyle: 'circle', padding: 6 } },
|
|
319
|
+
tooltip: { bodyFont: { family: MONO, size: 10 }, titleFont: { family: MONO, size: 10 } },
|
|
320
|
+
},
|
|
321
|
+
}}
|
|
322
|
+
/>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
)}
|
|
326
|
+
</div>
|
|
327
|
+
|
|
328
|
+
{/* Daily activity bar */}
|
|
329
|
+
{days.length > 1 && (
|
|
330
|
+
<div className="card p-3">
|
|
331
|
+
<SectionTitle>daily activity (last 14 days)</SectionTitle>
|
|
332
|
+
<div style={{ height: 120 }}>
|
|
333
|
+
<Bar
|
|
334
|
+
data={{
|
|
335
|
+
labels: days.map(([d]) => d.slice(5)),
|
|
336
|
+
datasets: [{
|
|
337
|
+
data: days.map(([, c]) => c),
|
|
338
|
+
backgroundColor: '#6366f1',
|
|
339
|
+
borderRadius: 2,
|
|
340
|
+
}],
|
|
341
|
+
}}
|
|
342
|
+
options={{
|
|
343
|
+
responsive: true, maintainAspectRatio: false,
|
|
344
|
+
plugins: { legend: { display: false }, tooltip: { bodyFont: { family: MONO, size: 10 }, titleFont: { family: MONO, size: 10 } } },
|
|
345
|
+
scales: {
|
|
346
|
+
x: { grid: { display: false }, ticks: { color: txtDim, font: { size: 8, family: MONO } } },
|
|
347
|
+
y: { grid: { color: gridColor }, ticks: { color: txtDim, font: { size: 8, family: MONO }, stepSize: 1 }, beginAtZero: true },
|
|
348
|
+
},
|
|
349
|
+
}}
|
|
350
|
+
/>
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
)}
|
|
354
|
+
|
|
355
|
+
{/* Projects */}
|
|
356
|
+
{projects.length > 0 && (
|
|
357
|
+
<div>
|
|
358
|
+
<SectionTitle>projects</SectionTitle>
|
|
359
|
+
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-2 mt-2">
|
|
360
|
+
{projects.map(([folder, info]) => (
|
|
361
|
+
<div key={folder} className="card px-3 py-2.5">
|
|
362
|
+
<div className="flex items-center gap-1.5 mb-1.5">
|
|
363
|
+
<FolderOpen size={11} style={{ color: '#818cf8' }} />
|
|
364
|
+
<span className="text-[12px] font-medium truncate" style={{ color: 'var(--c-white)' }}>{folder.split('/').pop()}</span>
|
|
365
|
+
</div>
|
|
366
|
+
<div className="text-[8px] truncate mb-2" style={{ color: 'var(--c-text3)' }}>{folder}</div>
|
|
367
|
+
<div className="grid grid-cols-3 gap-1 text-center mb-1.5">
|
|
368
|
+
<div className="p-1 rounded-sm" style={{ background: 'var(--c-code-bg)' }}>
|
|
369
|
+
<div className="text-[11px] font-bold" style={{ color: 'var(--c-white)' }}>{info.count}</div>
|
|
370
|
+
<div className="text-[7px]" style={{ color: 'var(--c-text3)' }}>sessions</div>
|
|
371
|
+
</div>
|
|
372
|
+
<div className="p-1 rounded-sm" style={{ background: 'var(--c-code-bg)' }}>
|
|
373
|
+
<div className="text-[11px] font-bold" style={{ color: 'var(--c-white)' }}>{formatNumber(info.messages)}</div>
|
|
374
|
+
<div className="text-[7px]" style={{ color: 'var(--c-text3)' }}>messages</div>
|
|
375
|
+
</div>
|
|
376
|
+
<div className="p-1 rounded-sm" style={{ background: 'var(--c-code-bg)' }}>
|
|
377
|
+
<div className="text-[11px] font-bold" style={{ color: 'var(--c-white)' }}>{formatNumber(info.tokens)}</div>
|
|
378
|
+
<div className="text-[7px]" style={{ color: 'var(--c-text3)' }}>tokens</div>
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
<div className="h-1.5 rounded-full overflow-hidden" style={{ background: 'var(--c-code-bg)' }}>
|
|
382
|
+
<div className="h-full rounded-full" style={{ width: `${(info.count / maxProjectSessions * 100).toFixed(0)}%`, background: '#6366f1' }} />
|
|
383
|
+
</div>
|
|
384
|
+
<div className="text-[8px] mt-1" style={{ color: 'var(--c-text3)' }}>{formatDate(info.lastActive)}</div>
|
|
385
|
+
</div>
|
|
386
|
+
))}
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
)}
|
|
390
|
+
|
|
391
|
+
{/* Top tools */}
|
|
392
|
+
{tools.length > 0 && (
|
|
393
|
+
<div className="card p-3">
|
|
394
|
+
<SectionTitle>top tools</SectionTitle>
|
|
395
|
+
<div className="flex flex-wrap gap-1.5 mt-1">
|
|
396
|
+
{tools.map(([name, count]) => (
|
|
397
|
+
<span key={name} className="text-[10px] px-2 py-1 rounded-sm" style={{ background: 'var(--c-code-bg)', color: 'var(--c-text2)' }}>
|
|
398
|
+
{name} <span style={{ color: 'var(--c-text3)' }}>×{count}</span>
|
|
399
|
+
</span>
|
|
400
|
+
))}
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
)}
|
|
404
|
+
|
|
405
|
+
{/* Recent sessions table */}
|
|
406
|
+
<div className="card p-3">
|
|
407
|
+
<SectionTitle>recent sessions</SectionTitle>
|
|
408
|
+
<div className="max-h-[400px] overflow-y-auto scrollbar-thin">
|
|
409
|
+
<table className="w-full text-[12px]">
|
|
410
|
+
<tbody>
|
|
411
|
+
{sessions.slice(0, 50).map(s => (
|
|
412
|
+
<tr
|
|
413
|
+
key={s.id}
|
|
414
|
+
className="cursor-pointer transition"
|
|
415
|
+
style={{ borderBottom: '1px solid var(--c-border)' }}
|
|
416
|
+
onMouseEnter={e => e.currentTarget.style.background = 'var(--c-bg3)'}
|
|
417
|
+
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
|
|
418
|
+
onClick={() => setSelectedChat(s.id)}
|
|
419
|
+
>
|
|
420
|
+
<td className="py-2 px-2 w-[24px]"><EditorIcon source={s.source} size={11} /></td>
|
|
421
|
+
<td className="py-2 px-2">
|
|
422
|
+
<div className="text-[11px] font-medium truncate" style={{ color: 'var(--c-white)' }}>{s.name || 'Untitled'}</div>
|
|
423
|
+
<div className="text-[10px] truncate" style={{ color: 'var(--c-text3)' }}>{s.folder ? s.folder.split('/').pop() : ''}</div>
|
|
424
|
+
</td>
|
|
425
|
+
<td className="py-2 px-2 text-[10px] whitespace-nowrap" style={{ color: 'var(--c-text3)' }}>
|
|
426
|
+
{s.totalMessages > 0 && <span>{s.totalMessages}m</span>}
|
|
427
|
+
</td>
|
|
428
|
+
<td className="py-2 px-2">
|
|
429
|
+
{s.mode && (
|
|
430
|
+
<span className="text-[8px] px-1.5 py-0.5 rounded-sm" style={{ background: 'rgba(168,85,247,0.1)', color: '#a855f7' }}>{s.mode}</span>
|
|
431
|
+
)}
|
|
432
|
+
</td>
|
|
433
|
+
<td className="py-2 px-2 text-[10px] whitespace-nowrap text-right" style={{ color: 'var(--c-text3)' }}>
|
|
434
|
+
{formatDate(s.lastUpdatedAt)}
|
|
435
|
+
</td>
|
|
436
|
+
</tr>
|
|
437
|
+
))}
|
|
438
|
+
</tbody>
|
|
439
|
+
</table>
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
</div>
|
|
443
|
+
|
|
444
|
+
{/* ── Right sidebar: Live Feed ── */}
|
|
445
|
+
<div
|
|
446
|
+
className="hidden xl:flex flex-col w-[300px] shrink-0 sticky top-[42px] self-start"
|
|
447
|
+
style={{ height: sidebarH, borderLeft: '1px solid var(--c-border)', background: 'var(--c-bg)' }}
|
|
448
|
+
>
|
|
449
|
+
<LiveFeed onSessionClick={handleFeedClick} />
|
|
450
|
+
</div>
|
|
451
|
+
|
|
452
|
+
{/* Session sidebar */}
|
|
453
|
+
<ChatSidebar
|
|
454
|
+
chatId={selectedChat}
|
|
455
|
+
onClose={() => setSelectedChat(null)}
|
|
456
|
+
fetchFn={fetchFn}
|
|
457
|
+
username={username}
|
|
458
|
+
extraHeader={
|
|
459
|
+
<span className="text-[11px] font-medium px-1.5 py-0.5 shrink-0" style={{ background: 'rgba(99,102,241,0.15)', color: '#818cf8' }}>
|
|
460
|
+
{username}
|
|
461
|
+
</span>
|
|
462
|
+
}
|
|
463
|
+
/>
|
|
464
|
+
</div>
|
|
465
|
+
)
|
|
466
|
+
}
|