claude-session-viewer 0.3.2 → 0.3.4

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.
@@ -0,0 +1,95 @@
1
+ import { readdir, stat } from 'fs/promises';
2
+ import { join } from 'path';
3
+ import { CLAUDE_DIR } from '../constants.js';
4
+ import { parseJsonl } from '../utils/jsonl.js';
5
+ import { getAllProjectsWithSessions } from '../claude/projects/service.js';
6
+ import { getProjectDisplayName } from '../claude/projects/repository.js';
7
+ import { loadAgentSessionsFromFiles } from '../claude/sessions/service.js';
8
+ import { collectAgentDescriptions, injectAgentIdsIntoMessages, findAgentTitleFromParentMessages } from '../claude/sessions/agents.js';
9
+ import { isAgentSession } from '../claude/sessions/filters.js';
10
+ import { extractSessionTitle } from '../claude/sessions/title.js';
11
+ /**
12
+ * Session routes
13
+ */
14
+ export async function registerSessionRoutes(server) {
15
+ // API: Get all sessions grouped by project
16
+ server.get('/api/sessions', async (request, reply) => {
17
+ try {
18
+ const projectsDir = join(CLAUDE_DIR, 'projects');
19
+ const projectGroups = await getAllProjectsWithSessions(projectsDir);
20
+ return { projects: projectGroups };
21
+ }
22
+ catch (error) {
23
+ console.error('Error reading sessions:', error);
24
+ return { projects: [] };
25
+ }
26
+ });
27
+ // API: Get session by ID
28
+ server.get('/api/sessions/:id', async (request, reply) => {
29
+ try {
30
+ const { id } = request.params;
31
+ const projectsDir = join(CLAUDE_DIR, 'projects');
32
+ const projects = await readdir(projectsDir);
33
+ const isAgent = isAgentSession(id);
34
+ for (const project of projects) {
35
+ const projectPath = join(projectsDir, project);
36
+ const sessionFile = join(projectPath, `${id}.jsonl`);
37
+ try {
38
+ const messages = await parseJsonl(sessionFile);
39
+ const fileStat = await stat(sessionFile);
40
+ const projectName = getProjectDisplayName(project);
41
+ let title = extractSessionTitle(messages);
42
+ // For agent sessions, find description from parent
43
+ if (isAgent) {
44
+ const agentId = id.replace('agent-', '');
45
+ const files = await readdir(projectPath);
46
+ for (const file of files) {
47
+ if (!file.startsWith('agent-') && file.endsWith('.jsonl')) {
48
+ try {
49
+ const parentMessages = await parseJsonl(join(projectPath, file));
50
+ const description = findAgentTitleFromParentMessages(parentMessages, agentId);
51
+ if (description) {
52
+ title = description;
53
+ break;
54
+ }
55
+ }
56
+ catch {
57
+ continue;
58
+ }
59
+ }
60
+ }
61
+ }
62
+ // Attach agent sessions for main sessions
63
+ let agentSessions;
64
+ if (!isAgent) {
65
+ const agentDescriptions = collectAgentDescriptions(messages);
66
+ if (agentDescriptions.size > 0) {
67
+ agentSessions = await loadAgentSessionsFromFiles(projectPath, projectName, agentDescriptions);
68
+ }
69
+ }
70
+ const messagesWithAgentIds = injectAgentIdsIntoMessages(messages);
71
+ return {
72
+ session: {
73
+ id,
74
+ project: projectName,
75
+ timestamp: fileStat.mtime.toISOString(),
76
+ messages: messagesWithAgentIds,
77
+ messageCount: messages.length,
78
+ title,
79
+ isAgent,
80
+ agentSessions
81
+ }
82
+ };
83
+ }
84
+ catch {
85
+ continue;
86
+ }
87
+ }
88
+ return reply.code(404).send({ error: 'Session not found' });
89
+ }
90
+ catch (error) {
91
+ console.error('Error reading session:', error);
92
+ return reply.code(500).send({ error: 'Internal server error' });
93
+ }
94
+ });
95
+ }
@@ -0,0 +1,328 @@
1
+ import { readdir, stat } from 'fs/promises';
2
+ import { join } from 'path';
3
+ import { CLAUDE_DIR } from '../constants.js';
4
+ import { PRICING, DEFAULT_PRICING_MODEL, WEEKDAY_NAMES } from '../claude/config.js';
5
+ import { parseJsonl } from '../utils/jsonl.js';
6
+ import { extractTokenUsage, aggregateTokenUsage, calculateCost, createEmptyTokenUsage } from '../utils/tokenStats.js';
7
+ import { getProjectDisplayName } from '../claude/projects/repository.js';
8
+ import { collectAgentDescriptions } from '../claude/sessions/agents.js';
9
+ import { shouldSkipSession, isAgentSession, isEmptyFile } from '../claude/sessions/filters.js';
10
+ /**
11
+ * Statistics routes
12
+ */
13
+ export async function registerStatisticsRoutes(server) {
14
+ server.get('/api/statistics/tokens', async (request, reply) => {
15
+ try {
16
+ const projectsDir = join(CLAUDE_DIR, 'projects');
17
+ const projects = await readdir(projectsDir);
18
+ const daysParam = request.query.days || '30';
19
+ const cutoffDate = new Date();
20
+ if (daysParam !== 'all') {
21
+ const days = parseInt(daysParam, 10);
22
+ cutoffDate.setDate(cutoffDate.getDate() - days);
23
+ cutoffDate.setHours(0, 0, 0, 0);
24
+ }
25
+ else {
26
+ cutoffDate.setFullYear(2000, 0, 1);
27
+ }
28
+ const dailyMap = new Map();
29
+ const projectMap = new Map();
30
+ const modelMap = new Map();
31
+ let totalMessages = 0;
32
+ let totalSessions = 0;
33
+ let minDate = null;
34
+ let maxDate = null;
35
+ let totalCacheCreation = 0;
36
+ let totalCacheRead = 0;
37
+ let ephemeral5mTokens = 0;
38
+ let ephemeral1hTokens = 0;
39
+ const allUsages = [];
40
+ const allCosts = [];
41
+ const toolUsageMap = new Map();
42
+ let totalAgentSessions = 0;
43
+ const hourlyMap = new Map();
44
+ const weekdayMap = new Map();
45
+ for (let hour = 0; hour < 24; hour++) {
46
+ hourlyMap.set(hour, {
47
+ usage: createEmptyTokenUsage(),
48
+ sessionIds: new Set(),
49
+ messageCount: 0
50
+ });
51
+ }
52
+ for (let weekday = 0; weekday < 7; weekday++) {
53
+ weekdayMap.set(weekday, {
54
+ usage: createEmptyTokenUsage(),
55
+ sessionIds: new Set(),
56
+ messageCount: 0
57
+ });
58
+ }
59
+ for (const project of projects) {
60
+ const projectPath = join(projectsDir, project);
61
+ const projectStat = await stat(projectPath);
62
+ if (!projectStat.isDirectory())
63
+ continue;
64
+ const files = await readdir(projectPath);
65
+ const displayName = getProjectDisplayName(project);
66
+ if (!projectMap.has(project)) {
67
+ projectMap.set(project, {
68
+ usage: createEmptyTokenUsage(),
69
+ sessionIds: new Set(),
70
+ displayName
71
+ });
72
+ }
73
+ for (const file of files) {
74
+ if (!file.endsWith('.jsonl'))
75
+ continue;
76
+ const filePath = join(projectPath, file);
77
+ const fileStat = await stat(filePath);
78
+ if (isEmptyFile(fileStat.size))
79
+ continue;
80
+ const sessionId = file.replace('.jsonl', '');
81
+ if (isAgentSession(sessionId))
82
+ continue;
83
+ try {
84
+ const messages = await parseJsonl(filePath);
85
+ if (shouldSkipSession(messages))
86
+ continue;
87
+ totalSessions++;
88
+ const projectData = projectMap.get(project);
89
+ projectData.sessionIds.add(sessionId);
90
+ const agentDescriptions = collectAgentDescriptions(messages);
91
+ if (agentDescriptions.size > 0) {
92
+ totalAgentSessions++;
93
+ }
94
+ for (const message of messages) {
95
+ const tokenData = extractTokenUsage(message);
96
+ if (!tokenData)
97
+ continue;
98
+ const messageDate = new Date(message.timestamp);
99
+ if (messageDate < cutoffDate)
100
+ continue;
101
+ totalMessages++;
102
+ const { usage, model } = tokenData;
103
+ const pricing = PRICING[model] || PRICING[DEFAULT_PRICING_MODEL];
104
+ allUsages.push(usage);
105
+ allCosts.push(calculateCost(usage, pricing));
106
+ const dateKey = messageDate.toISOString().split('T')[0];
107
+ if (!minDate || messageDate < minDate)
108
+ minDate = messageDate;
109
+ if (!maxDate || messageDate > maxDate)
110
+ maxDate = messageDate;
111
+ if (!dailyMap.has(dateKey)) {
112
+ dailyMap.set(dateKey, {
113
+ usage: createEmptyTokenUsage(),
114
+ sessionIds: new Set()
115
+ });
116
+ }
117
+ const dailyData = dailyMap.get(dateKey);
118
+ dailyData.usage.inputTokens += usage.inputTokens;
119
+ dailyData.usage.cacheCreationTokens += usage.cacheCreationTokens;
120
+ dailyData.usage.cacheReadTokens += usage.cacheReadTokens;
121
+ dailyData.usage.outputTokens += usage.outputTokens;
122
+ dailyData.usage.totalTokens += usage.totalTokens;
123
+ dailyData.sessionIds.add(sessionId);
124
+ const hour = messageDate.getHours();
125
+ const hourData = hourlyMap.get(hour);
126
+ hourData.usage.inputTokens += usage.inputTokens;
127
+ hourData.usage.cacheCreationTokens += usage.cacheCreationTokens;
128
+ hourData.usage.cacheReadTokens += usage.cacheReadTokens;
129
+ hourData.usage.outputTokens += usage.outputTokens;
130
+ hourData.usage.totalTokens += usage.totalTokens;
131
+ hourData.sessionIds.add(sessionId);
132
+ hourData.messageCount++;
133
+ const weekday = messageDate.getDay();
134
+ const weekdayData = weekdayMap.get(weekday);
135
+ weekdayData.usage.inputTokens += usage.inputTokens;
136
+ weekdayData.usage.cacheCreationTokens += usage.cacheCreationTokens;
137
+ weekdayData.usage.cacheReadTokens += usage.cacheReadTokens;
138
+ weekdayData.usage.outputTokens += usage.outputTokens;
139
+ weekdayData.usage.totalTokens += usage.totalTokens;
140
+ weekdayData.sessionIds.add(sessionId);
141
+ weekdayData.messageCount++;
142
+ projectData.usage.inputTokens += usage.inputTokens;
143
+ projectData.usage.cacheCreationTokens += usage.cacheCreationTokens;
144
+ projectData.usage.cacheReadTokens += usage.cacheReadTokens;
145
+ projectData.usage.outputTokens += usage.outputTokens;
146
+ projectData.usage.totalTokens += usage.totalTokens;
147
+ if (!modelMap.has(model)) {
148
+ modelMap.set(model, {
149
+ usage: createEmptyTokenUsage(),
150
+ messageCount: 0
151
+ });
152
+ }
153
+ const modelData = modelMap.get(model);
154
+ modelData.usage.inputTokens += usage.inputTokens;
155
+ modelData.usage.cacheCreationTokens += usage.cacheCreationTokens;
156
+ modelData.usage.cacheReadTokens += usage.cacheReadTokens;
157
+ modelData.usage.outputTokens += usage.outputTokens;
158
+ modelData.usage.totalTokens += usage.totalTokens;
159
+ modelData.messageCount++;
160
+ totalCacheCreation += usage.cacheCreationTokens;
161
+ totalCacheRead += usage.cacheReadTokens;
162
+ const cacheCreation = message.message?.usage?.cache_creation;
163
+ if (cacheCreation) {
164
+ ephemeral5mTokens += cacheCreation.ephemeral_5m_input_tokens || 0;
165
+ ephemeral1hTokens += cacheCreation.ephemeral_1h_input_tokens || 0;
166
+ }
167
+ }
168
+ const toolUseIds = new Set();
169
+ const successfulToolUseIds = new Set();
170
+ for (const message of messages) {
171
+ const messageDate = new Date(message.timestamp);
172
+ if (messageDate < cutoffDate)
173
+ continue;
174
+ if (message.type === 'assistant' && message.message?.content && Array.isArray(message.message.content)) {
175
+ for (const item of message.message.content) {
176
+ if (item.type === 'tool_use' && item.name && item.id) {
177
+ const toolName = item.name;
178
+ if (!toolUsageMap.has(toolName)) {
179
+ toolUsageMap.set(toolName, { total: 0, successful: 0 });
180
+ }
181
+ if (!toolUseIds.has(item.id)) {
182
+ toolUseIds.add(item.id);
183
+ toolUsageMap.get(toolName).total++;
184
+ }
185
+ }
186
+ }
187
+ }
188
+ }
189
+ for (const message of messages) {
190
+ const messageDate = new Date(message.timestamp);
191
+ if (messageDate < cutoffDate)
192
+ continue;
193
+ if (message.message?.content && Array.isArray(message.message.content)) {
194
+ for (const item of message.message.content) {
195
+ if (item.type === 'tool_result' && !item.is_error && item.tool_use_id) {
196
+ if (!successfulToolUseIds.has(item.tool_use_id)) {
197
+ successfulToolUseIds.add(item.tool_use_id);
198
+ let found = false;
199
+ for (const msg of messages) {
200
+ if (found)
201
+ break;
202
+ if (msg.type === 'assistant' && msg.message?.content && Array.isArray(msg.message.content)) {
203
+ for (const toolUseItem of msg.message.content) {
204
+ if (toolUseItem.type === 'tool_use' && toolUseItem.id === item.tool_use_id && toolUseItem.name) {
205
+ const toolStats = toolUsageMap.get(toolUseItem.name);
206
+ if (toolStats) {
207
+ toolStats.successful++;
208
+ found = true;
209
+ break;
210
+ }
211
+ }
212
+ }
213
+ }
214
+ }
215
+ }
216
+ }
217
+ }
218
+ }
219
+ }
220
+ }
221
+ catch (error) {
222
+ console.error(`Error processing ${file}:`, error);
223
+ }
224
+ }
225
+ }
226
+ const totalUsage = aggregateTokenUsage(allUsages);
227
+ const totalCost = allCosts.reduce((acc, cost) => ({
228
+ inputCost: acc.inputCost + cost.inputCost,
229
+ outputCost: acc.outputCost + cost.outputCost,
230
+ cacheCreationCost: acc.cacheCreationCost + cost.cacheCreationCost,
231
+ cacheReadCost: acc.cacheReadCost + cost.cacheReadCost,
232
+ totalCost: acc.totalCost + cost.totalCost
233
+ }), { inputCost: 0, outputCost: 0, cacheCreationCost: 0, cacheReadCost: 0, totalCost: 0 });
234
+ const totalPotentialInput = totalUsage.inputTokens + totalCacheCreation + totalCacheRead;
235
+ const cacheHitRate = totalPotentialInput > 0 ? (totalCacheRead / totalPotentialInput) * 100 : 0;
236
+ const defaultPricing = PRICING[DEFAULT_PRICING_MODEL];
237
+ const savedCost = (totalCacheRead / 1000000) * (defaultPricing.input - defaultPricing.cacheRead);
238
+ const daily = Array.from(dailyMap.entries())
239
+ .map(([date, data]) => ({
240
+ date,
241
+ usage: data.usage,
242
+ sessionCount: data.sessionIds.size
243
+ }))
244
+ .sort((a, b) => a.date.localeCompare(b.date));
245
+ const byProject = Array.from(projectMap.entries())
246
+ .map(([project, data]) => ({
247
+ project,
248
+ displayName: data.displayName,
249
+ usage: data.usage,
250
+ sessionCount: data.sessionIds.size
251
+ }))
252
+ .sort((a, b) => b.usage.totalTokens - a.usage.totalTokens);
253
+ const byModel = Array.from(modelMap.entries())
254
+ .map(([model, data]) => ({
255
+ model,
256
+ usage: data.usage,
257
+ messageCount: data.messageCount
258
+ }))
259
+ .sort((a, b) => b.usage.totalTokens - a.usage.totalTokens);
260
+ const toolUsage = Array.from(toolUsageMap.entries())
261
+ .map(([toolName, stats]) => ({
262
+ toolName,
263
+ totalUses: stats.total,
264
+ successfulUses: stats.successful,
265
+ successRate: stats.total > 0 ? (stats.successful / stats.total) * 100 : 0
266
+ }))
267
+ .sort((a, b) => b.totalUses - a.totalUses);
268
+ const totalToolCalls = Array.from(toolUsageMap.values()).reduce((sum, stats) => sum + stats.total, 0);
269
+ const agentUsageRate = totalSessions > 0 ? (totalAgentSessions / totalSessions) * 100 : 0;
270
+ const byHour = Array.from(hourlyMap.entries())
271
+ .map(([hour, data]) => ({
272
+ hour,
273
+ sessionCount: data.sessionIds.size,
274
+ messageCount: data.messageCount,
275
+ usage: data.usage
276
+ }))
277
+ .sort((a, b) => a.hour - b.hour);
278
+ const byWeekday = Array.from(weekdayMap.entries())
279
+ .map(([weekday, data]) => ({
280
+ weekday,
281
+ weekdayName: WEEKDAY_NAMES[weekday],
282
+ sessionCount: data.sessionIds.size,
283
+ messageCount: data.messageCount,
284
+ usage: data.usage
285
+ }))
286
+ .sort((a, b) => a.weekday - b.weekday);
287
+ const statistics = {
288
+ overview: {
289
+ total: totalUsage,
290
+ totalSessions,
291
+ totalMessages,
292
+ dateRange: {
293
+ start: minDate?.toISOString() || new Date().toISOString(),
294
+ end: maxDate?.toISOString() || new Date().toISOString()
295
+ }
296
+ },
297
+ daily,
298
+ byProject,
299
+ byModel,
300
+ cache: {
301
+ totalCacheCreation,
302
+ totalCacheRead,
303
+ ephemeral5mTokens,
304
+ ephemeral1hTokens,
305
+ cacheHitRate,
306
+ estimatedSavings: savedCost
307
+ },
308
+ cost: totalCost,
309
+ productivity: {
310
+ toolUsage,
311
+ totalToolCalls,
312
+ agentSessions: totalAgentSessions,
313
+ totalSessions,
314
+ agentUsageRate
315
+ },
316
+ trends: {
317
+ byHour,
318
+ byWeekday
319
+ }
320
+ };
321
+ return statistics;
322
+ }
323
+ catch (error) {
324
+ console.error('Error calculating token statistics:', error);
325
+ return reply.code(500).send({ error: 'Internal server error' });
326
+ }
327
+ });
328
+ }
@@ -0,0 +1,11 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ /**
4
+ * Project-specific constants
5
+ */
6
+ // Directory paths
7
+ export const CLAUDE_DIR = join(homedir(), '.claude');
8
+ // Server configuration
9
+ export const DEFAULT_PORT = 9090;
10
+ // Session configuration
11
+ export const MAX_TITLE_LENGTH = 100;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import { readFile } from 'fs/promises';
2
+ /**
3
+ * Pure JSONL parser
4
+ * No session-specific logic, just file parsing
5
+ */
6
+ export async function parseJsonl(filePath) {
7
+ const content = await readFile(filePath, 'utf-8');
8
+ return content
9
+ .split('\n')
10
+ .filter(line => line.trim())
11
+ .map(line => JSON.parse(line));
12
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Generic text utilities
3
+ * Pure functions with no project-specific logic
4
+ */
5
+ /**
6
+ * Remove XML-like tags from text
7
+ */
8
+ export function removeTags(text, tags) {
9
+ let result = text;
10
+ for (const tag of tags) {
11
+ const regex = new RegExp(`<${tag}>[\\s\\S]*?</${tag}>`, 'g');
12
+ result = result.replace(regex, ' ');
13
+ }
14
+ return result;
15
+ }
16
+ /**
17
+ * Normalize whitespace in text
18
+ */
19
+ export function normalizeWhitespace(text) {
20
+ return text.replace(/\s+/g, ' ').trim();
21
+ }
22
+ /**
23
+ * Clean text by removing common IDE/system tags
24
+ */
25
+ export function cleanText(text) {
26
+ const cleaned = removeTags(text, ['ide_selection', 'ide_opened_file', 'system-reminder']);
27
+ return normalizeWhitespace(cleaned);
28
+ }
29
+ /**
30
+ * Truncate text to maximum length
31
+ */
32
+ export function truncate(text, maxLength) {
33
+ return text.length > maxLength ? text.substring(0, maxLength) : text;
34
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Generic token statistics utilities
3
+ * Pure calculation functions
4
+ */
5
+ /**
6
+ * Extract token usage from a message object
7
+ */
8
+ export function extractTokenUsage(message) {
9
+ if (message.type !== 'assistant' || !message.message?.usage) {
10
+ return null;
11
+ }
12
+ const usage = message.message.usage;
13
+ const model = message.message.model || 'unknown';
14
+ return {
15
+ usage: {
16
+ inputTokens: usage.input_tokens || 0,
17
+ cacheCreationTokens: usage.cache_creation_input_tokens || 0,
18
+ cacheReadTokens: usage.cache_read_input_tokens || 0,
19
+ outputTokens: usage.output_tokens || 0,
20
+ totalTokens: (usage.input_tokens || 0) +
21
+ (usage.cache_creation_input_tokens || 0) +
22
+ (usage.cache_read_input_tokens || 0) +
23
+ (usage.output_tokens || 0)
24
+ },
25
+ model
26
+ };
27
+ }
28
+ /**
29
+ * Aggregate multiple token usages into a single total
30
+ */
31
+ export function aggregateTokenUsage(usages) {
32
+ return usages.reduce((acc, usage) => ({
33
+ inputTokens: acc.inputTokens + usage.inputTokens,
34
+ cacheCreationTokens: acc.cacheCreationTokens + usage.cacheCreationTokens,
35
+ cacheReadTokens: acc.cacheReadTokens + usage.cacheReadTokens,
36
+ outputTokens: acc.outputTokens + usage.outputTokens,
37
+ totalTokens: acc.totalTokens + usage.totalTokens
38
+ }), createEmptyTokenUsage());
39
+ }
40
+ /**
41
+ * Calculate cost for token usage based on pricing
42
+ */
43
+ export function calculateCost(usage, pricing) {
44
+ return {
45
+ inputCost: (usage.inputTokens / 1000000) * pricing.input,
46
+ outputCost: (usage.outputTokens / 1000000) * pricing.output,
47
+ cacheCreationCost: (usage.cacheCreationTokens / 1000000) * pricing.cacheCreation,
48
+ cacheReadCost: (usage.cacheReadTokens / 1000000) * pricing.cacheRead,
49
+ totalCost: (usage.inputTokens / 1000000) * pricing.input +
50
+ (usage.outputTokens / 1000000) * pricing.output +
51
+ (usage.cacheCreationTokens / 1000000) * pricing.cacheCreation +
52
+ (usage.cacheReadTokens / 1000000) * pricing.cacheRead
53
+ };
54
+ }
55
+ /**
56
+ * Create an empty token usage object
57
+ */
58
+ export function createEmptyTokenUsage() {
59
+ return {
60
+ inputTokens: 0,
61
+ cacheCreationTokens: 0,
62
+ cacheReadTokens: 0,
63
+ outputTokens: 0,
64
+ totalTokens: 0
65
+ };
66
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-session-viewer",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "claude-session-viewer": "./bin/cli.js"
@@ -1 +0,0 @@
1
- *,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:1280px}}@media (min-width: 1536px){.container{max-width:1536px}}.visible{visibility:visible}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.right-0{right:0}.top-0{top:0}.z-10{z-index:10}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-2{margin-left:.5rem}.ml-6{margin-left:1.5rem}.ml-auto{margin-left:auto}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-12{height:3rem}.h-2{height:.5rem}.h-3{height:.75rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-8{height:2rem}.h-96{height:24rem}.h-full{height:100%}.h-screen{height:100vh}.w-1{width:.25rem}.w-1\/2{width:50%}.w-1\/3{width:33.333333%}.w-12{width:3rem}.w-16{width:4rem}.w-24{width:6rem}.w-3{width:.75rem}.w-3\/4{width:75%}.w-32{width:8rem}.w-4{width:1rem}.w-4\/5{width:80%}.w-5\/6{width:83.333333%}.w-8{width:2rem}.w-full{width:100%}.min-w-0{min-width:0px}.max-w-4xl{max-width:56rem}.max-w-7xl{max-width:80rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.rotate-90{--tw-rotate: 90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-col-resize{cursor:col-resize}.auto-rows-fr{grid-auto-rows:minmax(0,1fr)}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-l-2{border-left-width:2px}.border-r{border-right-width:1px}.border-blue-400{--tw-border-opacity: 1;border-color:rgb(96 165 250 / var(--tw-border-opacity, 1))}.border-blue-500{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.border-blue-500\/20{border-color:#3b82f633}.border-cyan-500\/20{border-color:#06b6d433}.border-gray-700{--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity, 1))}.border-green-500\/20{border-color:#22c55e33}.border-orange-500\/20{border-color:#f9731633}.border-pink-500\/20{border-color:#ec489933}.border-purple-500\/20{border-color:#a855f733}.border-transparent{border-color:transparent}.bg-blue-500{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity, 1))}.bg-blue-500\/10{background-color:#3b82f61a}.bg-blue-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.bg-blue-900{--tw-bg-opacity: 1;background-color:rgb(30 58 138 / var(--tw-bg-opacity, 1))}.bg-blue-900\/50{background-color:#1e3a8a80}.bg-cyan-500{--tw-bg-opacity: 1;background-color:rgb(6 182 212 / var(--tw-bg-opacity, 1))}.bg-cyan-500\/10{background-color:#06b6d41a}.bg-gray-700{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.bg-gray-800\/30{background-color:#1f29374d}.bg-gray-800\/50{background-color:#1f293780}.bg-gray-900{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.bg-gray-900\/50{background-color:#11182780}.bg-gray-900\/70{background-color:#111827b3}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-green-500\/10{background-color:#22c55e1a}.bg-green-900{--tw-bg-opacity: 1;background-color:rgb(20 83 45 / var(--tw-bg-opacity, 1))}.bg-green-900\/30{background-color:#14532d4d}.bg-green-900\/50{background-color:#14532d80}.bg-orange-500{--tw-bg-opacity: 1;background-color:rgb(249 115 22 / var(--tw-bg-opacity, 1))}.bg-orange-500\/10{background-color:#f973161a}.bg-pink-500{--tw-bg-opacity: 1;background-color:rgb(236 72 153 / var(--tw-bg-opacity, 1))}.bg-pink-500\/10{background-color:#ec48991a}.bg-purple-500{--tw-bg-opacity: 1;background-color:rgb(168 85 247 / var(--tw-bg-opacity, 1))}.bg-purple-500\/10{background-color:#a855f71a}.bg-purple-700{--tw-bg-opacity: 1;background-color:rgb(126 34 206 / var(--tw-bg-opacity, 1))}.bg-purple-900\/50{background-color:#581c8780}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.bg-transparent{background-color:transparent}.bg-yellow-500{--tw-bg-opacity: 1;background-color:rgb(234 179 8 / var(--tw-bg-opacity, 1))}.bg-yellow-900\/30{background-color:#713f124d}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.from-blue-500{--tw-gradient-from: #3b82f6 var(--tw-gradient-from-position);--tw-gradient-to: rgb(59 130 246 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.to-green-500{--tw-gradient-to: #22c55e var(--tw-gradient-to-position)}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pl-14{padding-left:3.5rem}.pl-4{padding-left:1rem}.pl-8{padding-left:2rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-\[10px\]{font-size:10px}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.leading-tight{line-height:1.25}.text-blue-200{--tw-text-opacity: 1;color:rgb(191 219 254 / var(--tw-text-opacity, 1))}.text-blue-300{--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity, 1))}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-cyan-400{--tw-text-opacity: 1;color:rgb(34 211 238 / var(--tw-text-opacity, 1))}.text-gray-200{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-green-200{--tw-text-opacity: 1;color:rgb(187 247 208 / var(--tw-text-opacity, 1))}.text-green-300{--tw-text-opacity: 1;color:rgb(134 239 172 / var(--tw-text-opacity, 1))}.text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.text-purple-100{--tw-text-opacity: 1;color:rgb(243 232 255 / var(--tw-text-opacity, 1))}.text-purple-300{--tw-text-opacity: 1;color:rgb(216 180 254 / var(--tw-text-opacity, 1))}.text-purple-400{--tw-text-opacity: 1;color:rgb(192 132 252 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-yellow-400{--tw-text-opacity: 1;color:rgb(250 204 21 / var(--tw-text-opacity, 1))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.\@container{container-type:inline-size}:root{font-family:Inter,system-ui,Avenir,Helvetica,Arial,sans-serif;line-height:1.5;font-weight:400;color-scheme:light dark;color:#ffffffde;background-color:#242424;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{margin:0;display:flex;place-items:center;min-width:320px;min-height:100vh}#root{width:100%;min-height:100vh}*{scrollbar-width:thin;scrollbar-color:#4B5563 #1F2937}*::-webkit-scrollbar{width:8px;height:8px}*::-webkit-scrollbar-track{background:#1f2937}*::-webkit-scrollbar-thumb{background-color:#4b5563;border-radius:4px}*::-webkit-scrollbar-thumb:hover{background-color:#6b7280}.last\:border-b-0:last-child{border-bottom-width:0px}.last\:pb-0:last-child{padding-bottom:0}.hover\:bg-gray-700:hover{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-700\/50:hover{background-color:#37415180}.hover\:bg-gray-800:hover{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.hover\:bg-purple-600:hover{--tw-bg-opacity: 1;background-color:rgb(147 51 234 / var(--tw-bg-opacity, 1))}.hover\:text-blue-400:hover{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.hover\:text-gray-200:hover{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.hover\:text-gray-300:hover{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.group:hover .group-hover\:bg-blue-500{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity, 1))}@container (min-width: 300px){.\@\[300px\]\:inline{display:inline}.\@\[300px\]\:hidden{display:none}}@media (min-width: 768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width: 1024px){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (min-width: 1280px){.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}