ccwrap 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +19 -31
- package/dist/data/commentary.d.ts +2 -12
- package/dist/data/commentary.js +12 -4
- package/dist/data/parser.d.ts +30 -2
- package/dist/data/parser.js +126 -121
- package/dist/data/types.d.ts +2 -1
- package/dist/render.js +2 -5
- package/dist/video/Composition.js +5 -2
- package/dist/video/Root.js +2 -2
- package/dist/video/components/ParticleField.js +5 -4
- package/dist/video/slides/ArchetypeSlide.js +55 -5
- package/dist/video/slides/BusiestDaySlide.js +13 -4
- package/dist/video/slides/CostSlide.js +19 -4
- package/dist/video/slides/IntroSlide.js +42 -1
- package/dist/video/slides/ModelSlide.js +23 -4
- package/dist/video/slides/PeakHoursSlide.js +14 -4
- package/dist/video/slides/SessionSlide.js +24 -4
- package/dist/video/slides/SummarySlide.js +39 -7
- package/dist/video/slides/TokensSlide.js +21 -6
- package/dist/video/styles.d.ts +0 -15
- package/dist/video/styles.js +0 -1
- package/package.json +15 -2
package/dist/cli.js
CHANGED
|
@@ -50,7 +50,10 @@ function resolveDateRange(options) {
|
|
|
50
50
|
const sinceStr = since.toLocaleDateString("en-US", fmtOpts);
|
|
51
51
|
const untilYear = until.getFullYear();
|
|
52
52
|
const sinceYear = since.getFullYear();
|
|
53
|
-
const untilStr = until.toLocaleDateString("en-US", {
|
|
53
|
+
const untilStr = until.toLocaleDateString("en-US", {
|
|
54
|
+
...fmtOpts,
|
|
55
|
+
year: sinceYear !== untilYear ? "numeric" : undefined,
|
|
56
|
+
});
|
|
54
57
|
const periodLabel = `${sinceStr} – ${untilStr}, ${untilYear}`;
|
|
55
58
|
return { range: { since, until }, periodLabel };
|
|
56
59
|
}
|
|
@@ -78,42 +81,34 @@ const banner = `
|
|
|
78
81
|
╚██████╗╚██████╗╚███╔███╔╝██║ ██║██║ ██║██║ ██║ ███████╗██████╔╝
|
|
79
82
|
╚═════╝ ╚═════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚══════╝╚═════╝
|
|
80
83
|
`;
|
|
81
|
-
const darkBanner = `
|
|
82
|
-
██████╗ ██████╗██╗ ██╗██████╗ █████╗ ██████╗ ██████╗ ███████╗██████╗
|
|
83
|
-
██╔════╝██╔════╝██║ ██║██╔══██╗██╔══██╗██╔══██╗██╔══██╗██╔════╝██╔══██╗
|
|
84
|
-
██║ ██║ ██║ █╗ ██║██████╔╝███████║██████╔╝██████╔╝█████╗ ██║ ██║
|
|
85
|
-
██║ ██║ ██║███╗██║██╔══██╗██╔══██║██╔═══╝ ██╔═══╝ ██╔══╝ ██║ ██║
|
|
86
|
-
╚██████╗╚██████╗╚███╔███╔╝██║ ██║██║ ██║██║ ██║ ███████╗██████╔╝
|
|
87
|
-
╚═════╝ ╚═════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚══════╝╚═════╝
|
|
88
|
-
`;
|
|
89
84
|
program
|
|
90
85
|
.name("ccwrap")
|
|
91
|
-
.description("
|
|
92
|
-
.version("0.
|
|
93
|
-
.option("-o, --output <path>", "Output video file path", "
|
|
86
|
+
.description("ccwrap - Your AI coding stats, meme-ified into a shareable video")
|
|
87
|
+
.version("0.3.0")
|
|
88
|
+
.option("-o, --output <path>", "Output video file path", "ccwrap.mp4")
|
|
94
89
|
.option("-m, --mode <mode>", "Commentary mode: sassy or dark", "sassy")
|
|
95
90
|
.option("-p, --period <period>", "Time period: week, month, quarter, all (default: month)")
|
|
96
91
|
.option("--since <date>", "Start date (YYYY-MM-DD), mutually exclusive with --period")
|
|
97
92
|
.option("--until <date>", "End date (YYYY-MM-DD), requires --since")
|
|
93
|
+
.option("--skip-weekends", "Exclude weekend activity from stats")
|
|
98
94
|
.option("--json", "Output stats as JSON instead of video")
|
|
99
95
|
.option("--stats-only", "Print stats to console without rendering video")
|
|
100
96
|
.action(async (options) => {
|
|
101
97
|
const mode = options.mode === "dark" ? "dark" : "sassy";
|
|
102
98
|
const isDark = mode === "dark";
|
|
103
|
-
console.log(isDark ? chalk.red(
|
|
104
|
-
console.log(chalk.gray(isDark
|
|
105
|
-
? " Your AI usage. The planet remembers.\n"
|
|
106
|
-
: " Your AI-assisted coding, recapped.\n"));
|
|
99
|
+
console.log(isDark ? chalk.red(banner) : chalk.hex("#7c3aed")(banner));
|
|
100
|
+
console.log(chalk.gray(isDark ? " Your AI usage. The planet remembers.\n" : " Your AI-assisted coding, recapped.\n"));
|
|
107
101
|
// Resolve date range
|
|
108
102
|
const { range, periodLabel } = resolveDateRange(options);
|
|
109
103
|
// Step 1: Load and compute stats
|
|
110
|
-
const periodHint = range
|
|
111
|
-
|
|
112
|
-
:
|
|
113
|
-
|
|
104
|
+
const periodHint = range ? chalk.gray(` (${periodLabel})`) : "";
|
|
105
|
+
const spinner = ora({
|
|
106
|
+
text: `Scanning Claude Code usage data...${periodHint}`,
|
|
107
|
+
color: isDark ? "red" : "magenta",
|
|
108
|
+
}).start();
|
|
114
109
|
let stats;
|
|
115
110
|
try {
|
|
116
|
-
stats = await loadAndComputeStats(range, periodLabel);
|
|
111
|
+
stats = await loadAndComputeStats(range, periodLabel, options.skipWeekends);
|
|
117
112
|
spinner.succeed(chalk.green("Usage data loaded!"));
|
|
118
113
|
}
|
|
119
114
|
catch (err) {
|
|
@@ -150,16 +145,9 @@ program
|
|
|
150
145
|
text: isDark ? "Generating dark commentary..." : "Generating sassy commentary...",
|
|
151
146
|
color: isDark ? "red" : "magenta",
|
|
152
147
|
}).start();
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
commentarySpinner.succeed(chalk.green("Commentary generated!"));
|
|
157
|
-
}
|
|
158
|
-
catch (err) {
|
|
159
|
-
commentarySpinner.fail(chalk.yellow(`Commentary generation failed, using fallbacks`));
|
|
160
|
-
// Fallbacks are handled inside generateCommentary
|
|
161
|
-
commentary = await generateCommentary(stats, mode);
|
|
162
|
-
}
|
|
148
|
+
// generateCommentary handles its own errors and returns fallbacks
|
|
149
|
+
const commentary = await generateCommentary(stats, mode);
|
|
150
|
+
commentarySpinner.succeed(chalk.green("Commentary generated!"));
|
|
163
151
|
// Step 3: Render video
|
|
164
152
|
const renderSpinner = ora({ text: "Rendering your Wrapped video...", color: isDark ? "red" : "magenta" }).start();
|
|
165
153
|
try {
|
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
import type { WrappedStats } from "./types";
|
|
2
|
-
export
|
|
3
|
-
tokensLine: string;
|
|
4
|
-
costLine: string;
|
|
5
|
-
modelLine: string;
|
|
6
|
-
busiestDayLine: string;
|
|
7
|
-
sessionLine: string;
|
|
8
|
-
peakHoursLine: string;
|
|
9
|
-
archetypeLine: string;
|
|
10
|
-
summaryLine: string;
|
|
11
|
-
}
|
|
12
|
-
export type Mode = "sassy" | "dark";
|
|
1
|
+
import type { WrappedStats, Commentary, Mode } from "./types.js";
|
|
2
|
+
export declare function buildPrompt(stats: WrappedStats, mode: Mode): string;
|
|
13
3
|
export declare function generateCommentary(stats: WrappedStats, mode: Mode): Promise<Commentary>;
|
package/dist/data/commentary.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
-
function buildPrompt(stats, mode) {
|
|
2
|
+
export function buildPrompt(stats, mode) {
|
|
3
3
|
const modeDescription = mode === "dark"
|
|
4
4
|
? `DARK MODE: Your commentary should be darkly humorous with environmental/existential undertones.
|
|
5
5
|
Think: water usage, carbon footprint, e-waste, heat death of the universe, humanity's dependence on AI.
|
|
@@ -19,7 +19,7 @@ function buildPrompt(stats, mode) {
|
|
|
19
19
|
const periodContext = stats.periodLabel.startsWith("Your ")
|
|
20
20
|
? `This recap covers ${stats.periodLabel.toLowerCase().replace("your ", "the user's last ")} of usage.`
|
|
21
21
|
: `This recap covers the period ${stats.periodLabel}.`;
|
|
22
|
-
return `You are writing short, punchy one-liner commentary for a "
|
|
22
|
+
return `You are writing short, punchy one-liner commentary for a "ccwrap" recap video — a shareable stats recap of someone's AI coding assistant usage. Each line appears as a caption on an animated slide.
|
|
23
23
|
|
|
24
24
|
${periodContext}
|
|
25
25
|
|
|
@@ -73,6 +73,7 @@ export async function generateCommentary(stats, mode) {
|
|
|
73
73
|
console.log(" No ANTHROPIC_API_KEY found, using pre-written commentary.");
|
|
74
74
|
return mode === "dark" ? darkFallbacks : sassyFallbacks;
|
|
75
75
|
}
|
|
76
|
+
/* v8 ignore start -- requires live API key */
|
|
76
77
|
try {
|
|
77
78
|
const client = new Anthropic({ apiKey });
|
|
78
79
|
const message = await client.messages.create({
|
|
@@ -84,8 +85,14 @@ export async function generateCommentary(stats, mode) {
|
|
|
84
85
|
const parsed = JSON.parse(text);
|
|
85
86
|
// Validate all keys exist
|
|
86
87
|
const keys = [
|
|
87
|
-
"tokensLine",
|
|
88
|
-
"
|
|
88
|
+
"tokensLine",
|
|
89
|
+
"costLine",
|
|
90
|
+
"modelLine",
|
|
91
|
+
"busiestDayLine",
|
|
92
|
+
"sessionLine",
|
|
93
|
+
"peakHoursLine",
|
|
94
|
+
"archetypeLine",
|
|
95
|
+
"summaryLine",
|
|
89
96
|
];
|
|
90
97
|
const fallback = mode === "dark" ? darkFallbacks : sassyFallbacks;
|
|
91
98
|
for (const key of keys) {
|
|
@@ -99,4 +106,5 @@ export async function generateCommentary(stats, mode) {
|
|
|
99
106
|
console.log(` AI commentary failed (${err.message}), using pre-written commentary.`);
|
|
100
107
|
return mode === "dark" ? darkFallbacks : sassyFallbacks;
|
|
101
108
|
}
|
|
109
|
+
/* v8 ignore stop */
|
|
102
110
|
}
|
package/dist/data/parser.d.ts
CHANGED
|
@@ -1,2 +1,30 @@
|
|
|
1
|
-
import type { WrappedStats, DateRange } from "./types.js";
|
|
2
|
-
|
|
1
|
+
import type { RawJSONLEntry, WrappedStats, DateRange } from "./types.js";
|
|
2
|
+
declare const PRICING: Record<string, {
|
|
3
|
+
input: number;
|
|
4
|
+
output: number;
|
|
5
|
+
cacheWrite: number;
|
|
6
|
+
cacheRead: number;
|
|
7
|
+
}>;
|
|
8
|
+
export declare function estimateCost(model: string, inputTokens: number, outputTokens: number, cacheCreationTokens: number, cacheReadTokens: number, cache?: Map<string, (typeof PRICING)[string]>): number;
|
|
9
|
+
export declare function extractProject(filePath: string): string;
|
|
10
|
+
export declare function loadAndComputeStats(range?: DateRange, periodLabel?: string, skipWeekends?: boolean): Promise<WrappedStats>;
|
|
11
|
+
export declare function computeStatsFromEntries(rawEntries: (RawJSONLEntry & {
|
|
12
|
+
_file: string;
|
|
13
|
+
})[], opts?: {
|
|
14
|
+
range?: DateRange;
|
|
15
|
+
periodLabel?: string;
|
|
16
|
+
skipWeekends?: boolean;
|
|
17
|
+
}): WrappedStats;
|
|
18
|
+
export declare function computeArchetype(params: {
|
|
19
|
+
nightOwlScore: number;
|
|
20
|
+
weekendWarriorScore: number;
|
|
21
|
+
avgSessionMinutes: number;
|
|
22
|
+
totalSessions: number;
|
|
23
|
+
streakDays: number;
|
|
24
|
+
totalTokens: number;
|
|
25
|
+
favoriteModel: string;
|
|
26
|
+
}): {
|
|
27
|
+
archetype: string;
|
|
28
|
+
archetypeDescription: string;
|
|
29
|
+
};
|
|
30
|
+
export {};
|
package/dist/data/parser.js
CHANGED
|
@@ -5,29 +5,35 @@ import { join } from "path";
|
|
|
5
5
|
// Pricing per million tokens (from Anthropic pricing page)
|
|
6
6
|
// Each model has: input, output, cache_write (25% premium), cache_read (90% discount)
|
|
7
7
|
const PRICING = {
|
|
8
|
-
"claude-opus-4-6": { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.
|
|
9
|
-
"claude-sonnet-4-6": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.
|
|
10
|
-
"claude-sonnet-4-5-20250514": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.
|
|
11
|
-
"claude-sonnet-4-20250514": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.
|
|
12
|
-
"claude-haiku-4-5-20251001": { input: 0.
|
|
13
|
-
"claude-3-5-sonnet-20241022": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.
|
|
14
|
-
"claude-3-5-haiku-20241022": { input: 0.
|
|
8
|
+
"claude-opus-4-6": { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.5 },
|
|
9
|
+
"claude-sonnet-4-6": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },
|
|
10
|
+
"claude-sonnet-4-5-20250514": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },
|
|
11
|
+
"claude-sonnet-4-20250514": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },
|
|
12
|
+
"claude-haiku-4-5-20251001": { input: 0.8, output: 4, cacheWrite: 1.0, cacheRead: 0.08 },
|
|
13
|
+
"claude-3-5-sonnet-20241022": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },
|
|
14
|
+
"claude-3-5-haiku-20241022": { input: 0.8, output: 4, cacheWrite: 1.0, cacheRead: 0.08 },
|
|
15
15
|
};
|
|
16
|
-
function estimateCost(model, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
export function estimateCost(model, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens, cache) {
|
|
17
|
+
let pricing = cache?.get(model);
|
|
18
|
+
if (!pricing) {
|
|
19
|
+
pricing =
|
|
20
|
+
Object.entries(PRICING).find(([key]) => model.includes(key))?.[1] ??
|
|
21
|
+
(model.includes("opus")
|
|
22
|
+
? PRICING["claude-opus-4-6"]
|
|
23
|
+
: model.includes("haiku")
|
|
24
|
+
? PRICING["claude-haiku-4-5-20251001"]
|
|
25
|
+
: PRICING["claude-sonnet-4-6"]);
|
|
26
|
+
cache?.set(model, pricing);
|
|
27
|
+
}
|
|
28
|
+
return ((inputTokens * pricing.input +
|
|
22
29
|
outputTokens * pricing.output +
|
|
23
30
|
cacheCreationTokens * pricing.cacheWrite +
|
|
24
|
-
cacheReadTokens * pricing.cacheRead) /
|
|
31
|
+
cacheReadTokens * pricing.cacheRead) /
|
|
32
|
+
1_000_000);
|
|
25
33
|
}
|
|
26
34
|
async function findJSONLFiles() {
|
|
27
35
|
const claudeDir = join(homedir(), ".claude", "projects");
|
|
28
|
-
const configDir = process.env.CLAUDE_CONFIG_DIR
|
|
29
|
-
? join(process.env.CLAUDE_CONFIG_DIR, "projects")
|
|
30
|
-
: null;
|
|
36
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR ? join(process.env.CLAUDE_CONFIG_DIR, "projects") : null;
|
|
31
37
|
const dirs = [claudeDir, configDir].filter(Boolean);
|
|
32
38
|
const allFiles = [];
|
|
33
39
|
for (const dir of dirs) {
|
|
@@ -59,7 +65,7 @@ async function parseJSONLFile(filePath) {
|
|
|
59
65
|
}
|
|
60
66
|
return entries;
|
|
61
67
|
}
|
|
62
|
-
function extractProject(filePath) {
|
|
68
|
+
export function extractProject(filePath) {
|
|
63
69
|
// Path format: ~/.claude/projects/{encoded-project-path}/{sessionId}.jsonl
|
|
64
70
|
const parts = filePath.split("/projects/");
|
|
65
71
|
if (parts.length < 2)
|
|
@@ -68,11 +74,9 @@ function extractProject(filePath) {
|
|
|
68
74
|
const segments = afterProjects.split("/");
|
|
69
75
|
const projectEncoded = segments[0] ?? "unknown";
|
|
70
76
|
// Decode: -home-user-myproject -> /home/user/myproject
|
|
71
|
-
return projectEncoded
|
|
72
|
-
.replace(/^-/, "/")
|
|
73
|
-
.replace(/-/g, "/");
|
|
77
|
+
return projectEncoded.replace(/^-/, "/").replace(/-/g, "/");
|
|
74
78
|
}
|
|
75
|
-
export async function loadAndComputeStats(range, periodLabel) {
|
|
79
|
+
export async function loadAndComputeStats(range, periodLabel, skipWeekends) {
|
|
76
80
|
const files = await findJSONLFiles();
|
|
77
81
|
if (files.length === 0) {
|
|
78
82
|
throw new Error("No Claude Code usage data found. Make sure you have used Claude Code before.");
|
|
@@ -88,40 +92,94 @@ export async function loadAndComputeStats(range, periodLabel) {
|
|
|
88
92
|
if (allEntries.length === 0) {
|
|
89
93
|
throw new Error("No usage data entries found in Claude Code logs.");
|
|
90
94
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
95
|
+
return computeStatsFromEntries(allEntries, { range, periodLabel, skipWeekends });
|
|
96
|
+
}
|
|
97
|
+
export function computeStatsFromEntries(rawEntries, opts = {}) {
|
|
98
|
+
const { range, periodLabel, skipWeekends } = opts;
|
|
99
|
+
let entries = rawEntries.map((e) => {
|
|
100
|
+
const _date = new Date(e.timestamp);
|
|
101
|
+
return { ...e, _ts: _date.getTime(), _date };
|
|
102
|
+
});
|
|
103
|
+
entries.sort((a, b) => a._ts - b._ts);
|
|
104
|
+
// Filter by date range
|
|
94
105
|
if (range) {
|
|
95
106
|
const sinceMs = range.since.getTime();
|
|
96
107
|
const untilMs = range.until.getTime();
|
|
97
|
-
const before =
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return ts >= sinceMs && ts <= untilMs;
|
|
101
|
-
});
|
|
102
|
-
allEntries.length = 0;
|
|
103
|
-
allEntries.push(...filtered);
|
|
104
|
-
if (allEntries.length === 0) {
|
|
108
|
+
const before = entries.length;
|
|
109
|
+
entries = entries.filter((e) => e._ts >= sinceMs && e._ts <= untilMs);
|
|
110
|
+
if (entries.length === 0) {
|
|
105
111
|
throw new Error(`No usage data found in the selected time period (${before} entries exist outside this range).`);
|
|
106
112
|
}
|
|
107
113
|
}
|
|
108
|
-
//
|
|
114
|
+
// Filter out weekends
|
|
115
|
+
if (skipWeekends) {
|
|
116
|
+
const before = entries.length;
|
|
117
|
+
entries = entries.filter((e) => {
|
|
118
|
+
const day = e._date.getDay();
|
|
119
|
+
return day !== 0 && day !== 6;
|
|
120
|
+
});
|
|
121
|
+
if (entries.length === 0) {
|
|
122
|
+
throw new Error(`No weekday usage data found (${before} weekend entries were excluded).`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Single pass: sessions, hourly, daily, model usage, weekend count
|
|
109
126
|
const sessionMap = new Map();
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
127
|
+
const hourly = Array.from({ length: 24 }, (_, i) => ({ hour: i, messageCount: 0, tokenCount: 0 }));
|
|
128
|
+
const dailyMap = new Map();
|
|
129
|
+
const dailySessionSets = new Map();
|
|
130
|
+
const modelMap = new Map();
|
|
131
|
+
let weekendCount = 0;
|
|
132
|
+
for (const entry of entries) {
|
|
133
|
+
const hour = entry._date.getHours();
|
|
134
|
+
const dateStr = entry.timestamp.split("T")[0] ?? entry._date.toISOString().split("T")[0];
|
|
135
|
+
const model = entry.message?.model ?? "unknown";
|
|
136
|
+
const u = entry.message?.usage;
|
|
137
|
+
const inOut = u ? (u.input_tokens ?? 0) + (u.output_tokens ?? 0) : 0;
|
|
138
|
+
// Session grouping
|
|
139
|
+
if (!sessionMap.has(entry.sessionId))
|
|
140
|
+
sessionMap.set(entry.sessionId, []);
|
|
141
|
+
sessionMap.get(entry.sessionId).push(entry);
|
|
142
|
+
// Hourly
|
|
143
|
+
hourly[hour].messageCount++;
|
|
144
|
+
hourly[hour].tokenCount += inOut;
|
|
145
|
+
// Daily
|
|
146
|
+
if (!dailyMap.has(dateStr)) {
|
|
147
|
+
dailyMap.set(dateStr, { date: dateStr, messageCount: 0, tokenCount: 0, costUSD: 0, sessions: 0 });
|
|
148
|
+
dailySessionSets.set(dateStr, new Set());
|
|
149
|
+
}
|
|
150
|
+
dailyMap.get(dateStr).messageCount++;
|
|
151
|
+
dailyMap.get(dateStr).tokenCount += inOut;
|
|
152
|
+
dailySessionSets.get(dateStr).add(entry.sessionId);
|
|
153
|
+
// Model usage
|
|
154
|
+
if (!modelMap.has(model))
|
|
155
|
+
modelMap.set(model, { tokens: 0, messages: 0, cost: 0 });
|
|
156
|
+
const md = modelMap.get(model);
|
|
157
|
+
md.messages++;
|
|
158
|
+
md.tokens += inOut;
|
|
159
|
+
// Weekend count
|
|
160
|
+
const dow = entry._date.getDay();
|
|
161
|
+
if (dow === 0 || dow === 6)
|
|
162
|
+
weekendCount++;
|
|
115
163
|
}
|
|
164
|
+
// Finalize daily sessions
|
|
165
|
+
for (const [date, sessionSet] of dailySessionSets) {
|
|
166
|
+
dailyMap.get(date).sessions = sessionSet.size;
|
|
167
|
+
}
|
|
168
|
+
const dailyActivity = [...dailyMap.values()].sort((a, b) => a.date.localeCompare(b.date));
|
|
116
169
|
// Compute session stats
|
|
170
|
+
const costCache = new Map();
|
|
117
171
|
const sessions = [];
|
|
118
|
-
for (const [sessionId,
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
const endTime = new Date(Math.max(...timestamps.map(t => t.getTime())));
|
|
122
|
-
const models = [...new Set(entries.map(e => e.message?.model).filter(Boolean))];
|
|
172
|
+
for (const [sessionId, sessionEntries] of sessionMap) {
|
|
173
|
+
let minTs = Infinity, maxTs = -Infinity;
|
|
174
|
+
const models = new Set();
|
|
123
175
|
let totalInput = 0, totalOutput = 0, totalCacheCreate = 0, totalCacheRead = 0, totalCost = 0;
|
|
124
|
-
for (const entry of
|
|
176
|
+
for (const entry of sessionEntries) {
|
|
177
|
+
if (entry._ts < minTs)
|
|
178
|
+
minTs = entry._ts;
|
|
179
|
+
if (entry._ts > maxTs)
|
|
180
|
+
maxTs = entry._ts;
|
|
181
|
+
if (entry.message?.model)
|
|
182
|
+
models.add(entry.message.model);
|
|
125
183
|
const u = entry.message?.usage;
|
|
126
184
|
if (u) {
|
|
127
185
|
totalInput += u.input_tokens ?? 0;
|
|
@@ -133,73 +191,26 @@ export async function loadAndComputeStats(range, periodLabel) {
|
|
|
133
191
|
totalCost += entry.costUSD;
|
|
134
192
|
}
|
|
135
193
|
else if (entry.message?.model && u) {
|
|
136
|
-
totalCost += estimateCost(entry.message.model, u.input_tokens ?? 0, u.output_tokens ?? 0, u.cache_creation_input_tokens ?? 0, u.cache_read_input_tokens ?? 0);
|
|
194
|
+
totalCost += estimateCost(entry.message.model, u.input_tokens ?? 0, u.output_tokens ?? 0, u.cache_creation_input_tokens ?? 0, u.cache_read_input_tokens ?? 0, costCache);
|
|
137
195
|
}
|
|
138
196
|
}
|
|
139
197
|
sessions.push({
|
|
140
198
|
sessionId,
|
|
141
|
-
project: extractProject(
|
|
142
|
-
startTime,
|
|
143
|
-
endTime,
|
|
144
|
-
durationMinutes: (
|
|
145
|
-
messageCount:
|
|
199
|
+
project: extractProject(sessionEntries[0]._file),
|
|
200
|
+
startTime: new Date(minTs),
|
|
201
|
+
endTime: new Date(maxTs),
|
|
202
|
+
durationMinutes: (maxTs - minTs) / 60_000,
|
|
203
|
+
messageCount: sessionEntries.length,
|
|
146
204
|
totalInputTokens: totalInput,
|
|
147
205
|
totalOutputTokens: totalOutput,
|
|
148
206
|
totalCacheCreationTokens: totalCacheCreate,
|
|
149
207
|
totalCacheReadTokens: totalCacheRead,
|
|
150
208
|
totalTokens: totalInput + totalOutput + totalCacheCreate + totalCacheRead,
|
|
151
|
-
models,
|
|
209
|
+
models: [...models],
|
|
152
210
|
costUSD: totalCost,
|
|
153
211
|
});
|
|
154
212
|
}
|
|
155
|
-
//
|
|
156
|
-
const hourly = Array.from({ length: 24 }, (_, i) => ({
|
|
157
|
-
hour: i,
|
|
158
|
-
messageCount: 0,
|
|
159
|
-
tokenCount: 0,
|
|
160
|
-
}));
|
|
161
|
-
for (const entry of allEntries) {
|
|
162
|
-
const hour = new Date(entry.timestamp).getHours();
|
|
163
|
-
hourly[hour].messageCount++;
|
|
164
|
-
const u = entry.message?.usage;
|
|
165
|
-
if (u) {
|
|
166
|
-
hourly[hour].tokenCount += (u.input_tokens ?? 0) + (u.output_tokens ?? 0);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
// Daily activity
|
|
170
|
-
const dailyMap = new Map();
|
|
171
|
-
const dailySessionSets = new Map();
|
|
172
|
-
for (const entry of allEntries) {
|
|
173
|
-
const date = new Date(entry.timestamp).toISOString().split("T")[0];
|
|
174
|
-
if (!dailyMap.has(date)) {
|
|
175
|
-
dailyMap.set(date, { date, messageCount: 0, tokenCount: 0, costUSD: 0, sessions: 0 });
|
|
176
|
-
dailySessionSets.set(date, new Set());
|
|
177
|
-
}
|
|
178
|
-
const day = dailyMap.get(date);
|
|
179
|
-
day.messageCount++;
|
|
180
|
-
const u = entry.message?.usage;
|
|
181
|
-
if (u) {
|
|
182
|
-
day.tokenCount += (u.input_tokens ?? 0) + (u.output_tokens ?? 0);
|
|
183
|
-
}
|
|
184
|
-
dailySessionSets.get(date).add(entry.sessionId);
|
|
185
|
-
}
|
|
186
|
-
for (const [date, sessionSet] of dailySessionSets) {
|
|
187
|
-
dailyMap.get(date).sessions = sessionSet.size;
|
|
188
|
-
}
|
|
189
|
-
const dailyActivity = [...dailyMap.values()].sort((a, b) => a.date.localeCompare(b.date));
|
|
190
|
-
// Model usage
|
|
191
|
-
const modelMap = new Map();
|
|
192
|
-
for (const entry of allEntries) {
|
|
193
|
-
const model = entry.message?.model ?? "unknown";
|
|
194
|
-
if (!modelMap.has(model))
|
|
195
|
-
modelMap.set(model, { tokens: 0, messages: 0, cost: 0 });
|
|
196
|
-
const m = modelMap.get(model);
|
|
197
|
-
m.messages++;
|
|
198
|
-
const u = entry.message?.usage;
|
|
199
|
-
if (u) {
|
|
200
|
-
m.tokens += (u.input_tokens ?? 0) + (u.output_tokens ?? 0);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
213
|
+
// Model usage percentages
|
|
203
214
|
const totalTokensAll = [...modelMap.values()].reduce((sum, m) => sum + m.tokens, 0);
|
|
204
215
|
const modelUsage = [...modelMap.entries()]
|
|
205
216
|
.map(([model, data]) => ({
|
|
@@ -215,16 +226,16 @@ export async function loadAndComputeStats(range, periodLabel) {
|
|
|
215
226
|
const totalInputTokens = sessions.reduce((s, sess) => s + sess.totalInputTokens, 0);
|
|
216
227
|
const totalOutputTokens = sessions.reduce((s, sess) => s + sess.totalOutputTokens, 0);
|
|
217
228
|
const totalCost = sessions.reduce((s, sess) => s + sess.costUSD, 0);
|
|
218
|
-
const totalMessages =
|
|
219
|
-
// Dates
|
|
220
|
-
const firstActivity =
|
|
221
|
-
const lastActivity =
|
|
222
|
-
const uniqueDays = new Set(
|
|
229
|
+
const totalMessages = entries.length;
|
|
230
|
+
// Dates — reuse dailyMap keys instead of reparsing
|
|
231
|
+
const firstActivity = entries[0]._date;
|
|
232
|
+
const lastActivity = entries[entries.length - 1]._date;
|
|
233
|
+
const uniqueDays = new Set(dailyMap.keys());
|
|
223
234
|
const totalDaysActive = uniqueDays.size;
|
|
224
235
|
// Peak stats
|
|
225
|
-
const busiestDay = dailyActivity.reduce((best, d) => d.tokenCount > best.tokenCount ? d : best);
|
|
226
|
-
const busiestHour = hourly.reduce((best, h) => h.tokenCount > best.tokenCount ? h : best).hour;
|
|
227
|
-
const longestSession = sessions.reduce((best, s) => s.durationMinutes > best.durationMinutes ? s : best);
|
|
236
|
+
const busiestDay = dailyActivity.reduce((best, d) => (d.tokenCount > best.tokenCount ? d : best));
|
|
237
|
+
const busiestHour = hourly.reduce((best, h) => (h.tokenCount > best.tokenCount ? h : best)).hour;
|
|
238
|
+
const longestSession = sessions.reduce((best, s) => (s.durationMinutes > best.durationMinutes ? s : best));
|
|
228
239
|
// Project frequency
|
|
229
240
|
const projectCounts = new Map();
|
|
230
241
|
for (const sess of sessions) {
|
|
@@ -247,15 +258,9 @@ export async function loadAndComputeStats(range, periodLabel) {
|
|
|
247
258
|
}
|
|
248
259
|
}
|
|
249
260
|
// Scores
|
|
250
|
-
const nightTokens = hourly.filter(h => h.hour >= 22 || h.hour < 5).reduce((s, h) => s + h.tokenCount, 0);
|
|
261
|
+
const nightTokens = hourly.filter((h) => h.hour >= 22 || h.hour < 5).reduce((s, h) => s + h.tokenCount, 0);
|
|
251
262
|
const nightOwlScore = totalTokensAll > 0 ? Math.round((nightTokens / totalTokensAll) * 100) : 0;
|
|
252
|
-
const
|
|
253
|
-
const day = new Date(e.timestamp).getDay();
|
|
254
|
-
return day === 0 || day === 6;
|
|
255
|
-
});
|
|
256
|
-
const weekendWarriorScore = allEntries.length > 0
|
|
257
|
-
? Math.round((weekendEntries.length / allEntries.length) * 100)
|
|
258
|
-
: 0;
|
|
263
|
+
const weekendWarriorScore = entries.length > 0 ? Math.round((weekendCount / entries.length) * 100) : 0;
|
|
259
264
|
const favoriteModel = modelUsage[0]?.model ?? "unknown";
|
|
260
265
|
const totalSessionHours = sessions.reduce((s, sess) => s + sess.durationMinutes, 0) / 60;
|
|
261
266
|
const avgSessionMinutes = sessions.length > 0 ? sessions.reduce((s, sess) => s + sess.durationMinutes, 0) / sessions.length : 0;
|
|
@@ -300,15 +305,15 @@ export async function loadAndComputeStats(range, periodLabel) {
|
|
|
300
305
|
marathonEquivalent: Math.round((totalSessionHours / 4.5) * 100) / 100,
|
|
301
306
|
// Environmental estimates (rough but directionally correct)
|
|
302
307
|
// ~0.5 liters of water per 1M tokens for data center cooling
|
|
303
|
-
waterLiters: Math.round(totalTokens / 1_000_000 * 0.5 * 100) / 100,
|
|
308
|
+
waterLiters: Math.round((totalTokens / 1_000_000) * 0.5 * 100) / 100,
|
|
304
309
|
// ~0.3g CO2 per 1K tokens (inference energy + grid mix)
|
|
305
|
-
co2Grams: Math.round(totalTokens / 1_000 * 0.3),
|
|
310
|
+
co2Grams: Math.round((totalTokens / 1_000) * 0.3),
|
|
306
311
|
// ~0.001 kWh per 1K tokens
|
|
307
|
-
kwhUsed: Math.round(totalTokens / 1_000 * 0.001 * 100) / 100,
|
|
312
|
+
kwhUsed: Math.round((totalTokens / 1_000) * 0.001 * 100) / 100,
|
|
308
313
|
};
|
|
309
314
|
}
|
|
310
|
-
function computeArchetype(params) {
|
|
311
|
-
const { nightOwlScore, weekendWarriorScore, avgSessionMinutes, totalSessions, streakDays, totalTokens, favoriteModel } = params;
|
|
315
|
+
export function computeArchetype(params) {
|
|
316
|
+
const { nightOwlScore, weekendWarriorScore, avgSessionMinutes, totalSessions, streakDays, totalTokens, favoriteModel, } = params;
|
|
312
317
|
if (nightOwlScore > 50) {
|
|
313
318
|
return {
|
|
314
319
|
archetype: "The Night Owl Demon",
|
package/dist/data/types.d.ts
CHANGED
|
@@ -92,10 +92,11 @@ export interface WrappedStats {
|
|
|
92
92
|
co2Grams: number;
|
|
93
93
|
kwhUsed: number;
|
|
94
94
|
}
|
|
95
|
+
export type Mode = "sassy" | "dark";
|
|
95
96
|
export interface VideoProps {
|
|
96
97
|
stats: WrappedStats;
|
|
97
98
|
commentary: Commentary;
|
|
98
|
-
mode:
|
|
99
|
+
mode: Mode;
|
|
99
100
|
}
|
|
100
101
|
export interface Commentary {
|
|
101
102
|
tokensLine: string;
|
package/dist/render.js
CHANGED
|
@@ -11,7 +11,7 @@ function findChromeBinary() {
|
|
|
11
11
|
"/usr/bin/chromium",
|
|
12
12
|
"/usr/bin/google-chrome",
|
|
13
13
|
];
|
|
14
|
-
return candidates.find(p => existsSync(p));
|
|
14
|
+
return candidates.find((p) => existsSync(p));
|
|
15
15
|
}
|
|
16
16
|
export async function renderVideo(stats, commentary, mode, outputPath) {
|
|
17
17
|
const resolvedOutput = resolve(process.cwd(), outputPath);
|
|
@@ -26,10 +26,7 @@ export async function renderVideo(stats, commentary, mode, outputPath) {
|
|
|
26
26
|
...config,
|
|
27
27
|
module: {
|
|
28
28
|
...config.module,
|
|
29
|
-
rules: [
|
|
30
|
-
...(config.module?.rules ?? []),
|
|
31
|
-
{ test: /\.m?js$/, resolve: { fullySpecified: false } },
|
|
32
|
-
],
|
|
29
|
+
rules: [...(config.module?.rules ?? []), { test: /\.m?js$/, resolve: { fullySpecified: false } }],
|
|
33
30
|
},
|
|
34
31
|
}),
|
|
35
32
|
});
|
|
@@ -30,9 +30,12 @@ const SlideWithTransition = ({ children, index }) => {
|
|
|
30
30
|
extrapolateRight: "clamp",
|
|
31
31
|
});
|
|
32
32
|
const fadeOut = index < TOTAL_SLIDES - 1
|
|
33
|
-
? interpolate(frame, [FADE_IN + READABLE, SLIDE_CONTENT], [1, 0], {
|
|
33
|
+
? interpolate(frame, [FADE_IN + READABLE, SLIDE_CONTENT], [1, 0], {
|
|
34
|
+
extrapolateLeft: "clamp",
|
|
35
|
+
extrapolateRight: "clamp",
|
|
36
|
+
})
|
|
34
37
|
: 1; // last slide stays visible
|
|
35
|
-
return
|
|
38
|
+
return _jsx(AbsoluteFill, { style: { opacity: fadeIn * fadeOut }, children: children });
|
|
36
39
|
};
|
|
37
40
|
export const CCWrappedComposition = ({ stats, commentary, mode }) => {
|
|
38
41
|
const slides = [
|
package/dist/video/Root.js
CHANGED
|
@@ -25,7 +25,7 @@ const defaultProps = {
|
|
|
25
25
|
totalDaysActive: 89,
|
|
26
26
|
firstActivity: new Date("2025-06-01"),
|
|
27
27
|
lastActivity: new Date("2026-03-15"),
|
|
28
|
-
busiestDay: { date: "2026-02-14", messageCount: 247, tokenCount: 2_400_000, costUSD: 18.
|
|
28
|
+
busiestDay: { date: "2026-02-14", messageCount: 247, tokenCount: 2_400_000, costUSD: 18.5, sessions: 8 },
|
|
29
29
|
busiestHour: 22,
|
|
30
30
|
longestSession: {
|
|
31
31
|
sessionId: "demo",
|
|
@@ -40,7 +40,7 @@ const defaultProps = {
|
|
|
40
40
|
totalCacheReadTokens: 300_000,
|
|
41
41
|
totalTokens: 3_200_000,
|
|
42
42
|
models: ["claude-sonnet-4-6"],
|
|
43
|
-
costUSD: 18.
|
|
43
|
+
costUSD: 18.5,
|
|
44
44
|
},
|
|
45
45
|
mostActiveProject: "/home/user/megaproject",
|
|
46
46
|
hourlyActivity: Array.from({ length: 24 }, (_, i) => ({
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo } from "react";
|
|
3
3
|
import { useCurrentFrame, interpolate } from "remotion";
|
|
4
|
-
import {
|
|
4
|
+
import { getColors } from "../styles";
|
|
5
5
|
export const ParticleField = ({ count = 40, seed = 42 }) => {
|
|
6
6
|
const frame = useCurrentFrame();
|
|
7
7
|
const particles = useMemo(() => {
|
|
8
8
|
const rng = (s) => {
|
|
9
|
-
|
|
9
|
+
const x = Math.sin(s) * 10000;
|
|
10
10
|
return x - Math.floor(x);
|
|
11
11
|
};
|
|
12
|
-
const
|
|
12
|
+
const c = getColors("sassy");
|
|
13
|
+
const particleColors = [c.primary, c.secondary, c.accent, c.gold, c.pink];
|
|
13
14
|
return Array.from({ length: count }, (_, i) => ({
|
|
14
15
|
x: rng(seed + i * 7) * 100,
|
|
15
16
|
y: rng(seed + i * 13) * 100,
|
|
@@ -20,7 +21,7 @@ export const ParticleField = ({ count = 40, seed = 42 }) => {
|
|
|
20
21
|
}));
|
|
21
22
|
}, [count, seed]);
|
|
22
23
|
return (_jsx("div", { style: { position: "absolute", inset: 0, overflow: "hidden", pointerEvents: "none" }, children: particles.map((p, i) => {
|
|
23
|
-
const y = (p.y + frame * p.speed * 0.3) % 110 - 5;
|
|
24
|
+
const y = ((p.y + frame * p.speed * 0.3) % 110) - 5;
|
|
24
25
|
const twinkle = interpolate(Math.sin(frame * 0.1 + i), [-1, 1], [p.opacity * 0.5, p.opacity]);
|
|
25
26
|
return (_jsx("div", { style: {
|
|
26
27
|
position: "absolute",
|
|
@@ -15,20 +15,70 @@ function getArchetypeColor(archetype, mode) {
|
|
|
15
15
|
if (archetype.includes("Streak"))
|
|
16
16
|
return c.gold;
|
|
17
17
|
if (archetype.includes("Whale"))
|
|
18
|
-
return mode === "dark" ? c.accent :
|
|
18
|
+
return mode === "dark" ? c.accent : c.green;
|
|
19
19
|
if (archetype.includes("Connoisseur"))
|
|
20
20
|
return c.gold;
|
|
21
21
|
if (archetype.includes("Serial"))
|
|
22
22
|
return c.pink;
|
|
23
23
|
return c.secondary;
|
|
24
24
|
}
|
|
25
|
-
export const ArchetypeSlide = ({ stats, commentary, mode }) => {
|
|
25
|
+
export const ArchetypeSlide = ({ stats, commentary, mode, }) => {
|
|
26
26
|
const frame = useCurrentFrame();
|
|
27
27
|
const { fps } = useVideoConfig();
|
|
28
28
|
const c = getColors(mode);
|
|
29
29
|
const color = getArchetypeColor(stats.archetype, mode);
|
|
30
|
-
const revealScale = spring({
|
|
30
|
+
const revealScale = spring({
|
|
31
|
+
frame: Math.max(0, frame - 20),
|
|
32
|
+
fps,
|
|
33
|
+
from: 0,
|
|
34
|
+
to: 1,
|
|
35
|
+
config: { damping: 8, stiffness: 80 },
|
|
36
|
+
});
|
|
31
37
|
const glowIntensity = interpolate(Math.sin(frame * 0.08), [-1, 1], [0.5, 1]);
|
|
32
|
-
return (_jsxs("div", { style: { ...baseSlide, ...gradientBg(200, c), color: c.text }, children: [_jsx(ParticleField, { count: 40, seed: 333 }), _jsx(GlowOrb, { color: color, size: 350, x: 50, y: 45, speed: 0.3 }), _jsx(GlowOrb, { color: c.primary, size: 180, x: 15, y: 20 }), _jsx(GlowOrb, { color: c.accent, size: 180, x: 85, y: 80 }), _jsx(FadeIn, { delay: 5, children: _jsx("div", { style: { ...label, color, zIndex: 1, letterSpacing: "0.3em" }, children: mode === "dark" ? "Your Classification" : "You Are..." }) }), _jsxs("div", { style: {
|
|
38
|
+
return (_jsxs("div", { style: { ...baseSlide, ...gradientBg(200, c), color: c.text }, children: [_jsx(ParticleField, { count: 40, seed: 333 }), _jsx(GlowOrb, { color: color, size: 350, x: 50, y: 45, speed: 0.3 }), _jsx(GlowOrb, { color: c.primary, size: 180, x: 15, y: 20 }), _jsx(GlowOrb, { color: c.accent, size: 180, x: 85, y: 80 }), _jsx(FadeIn, { delay: 5, children: _jsx("div", { style: { ...label, color, zIndex: 1, letterSpacing: "0.3em" }, children: mode === "dark" ? "Your Classification" : "You Are..." }) }), _jsxs("div", { style: {
|
|
39
|
+
transform: `scale(${revealScale})`,
|
|
40
|
+
opacity: revealScale,
|
|
41
|
+
zIndex: 1,
|
|
42
|
+
marginTop: 24,
|
|
43
|
+
textAlign: "center",
|
|
44
|
+
}, children: [_jsx("div", { style: {
|
|
45
|
+
width: 90,
|
|
46
|
+
height: 3,
|
|
47
|
+
background: `linear-gradient(90deg, transparent, ${color}, transparent)`,
|
|
48
|
+
margin: "0 auto 18px",
|
|
49
|
+
opacity: glowIntensity,
|
|
50
|
+
} }), _jsx("div", { style: {
|
|
51
|
+
fontSize: 68,
|
|
52
|
+
fontWeight: 900,
|
|
53
|
+
lineHeight: 1.1,
|
|
54
|
+
...glowText(color),
|
|
55
|
+
background: `linear-gradient(135deg, ${color}, ${c.text}, ${color})`,
|
|
56
|
+
WebkitBackgroundClip: "text",
|
|
57
|
+
WebkitTextFillColor: "transparent",
|
|
58
|
+
filter: `brightness(${0.8 + glowIntensity * 0.4})`,
|
|
59
|
+
}, children: stats.archetype }), _jsx("div", { style: {
|
|
60
|
+
width: 90,
|
|
61
|
+
height: 3,
|
|
62
|
+
background: `linear-gradient(90deg, transparent, ${color}, transparent)`,
|
|
63
|
+
margin: "18px auto 0",
|
|
64
|
+
opacity: glowIntensity,
|
|
65
|
+
} })] }), _jsx(FadeIn, { delay: 40, children: _jsx("div", { style: {
|
|
66
|
+
fontSize: 18,
|
|
67
|
+
color: c.textMuted,
|
|
68
|
+
maxWidth: 580,
|
|
69
|
+
textAlign: "center",
|
|
70
|
+
lineHeight: 1.5,
|
|
71
|
+
marginTop: 28,
|
|
72
|
+
zIndex: 1,
|
|
73
|
+
padding: "8px 18px",
|
|
74
|
+
borderRadius: 10,
|
|
75
|
+
background: "rgba(0,0,0,0.35)",
|
|
76
|
+
}, children: commentary }) }), _jsx(FadeIn, { delay: 55, style: { zIndex: 1, marginTop: 28 }, children: _jsxs("div", { style: { display: "flex", gap: 14 }, children: [_jsx(Badge, { label: "Night Owl", value: `${stats.nightOwlScore}%`, color: c.primary, textColor: c.textMuted }), _jsx(Badge, { label: "Weekend", value: `${stats.weekendWarriorScore}%`, color: c.secondary, textColor: c.textMuted }), _jsx(Badge, { label: "Streak", value: `${stats.streakDays}d`, color: c.gold, textColor: c.textMuted })] }) })] }));
|
|
33
77
|
};
|
|
34
|
-
const Badge = ({ label: l, value, color, textColor }) => (_jsxs("div", { style: {
|
|
78
|
+
const Badge = ({ label: l, value, color, textColor, }) => (_jsxs("div", { style: {
|
|
79
|
+
background: `${color}20`,
|
|
80
|
+
border: `1px solid ${color}50`,
|
|
81
|
+
borderRadius: 12,
|
|
82
|
+
padding: "10px 20px",
|
|
83
|
+
textAlign: "center",
|
|
84
|
+
}, children: [_jsx("div", { style: { fontSize: 22, fontWeight: 700, color }, children: value }), _jsx("div", { style: { fontSize: 11, color: textColor, marginTop: 2, textTransform: "uppercase" }, children: l })] }));
|
|
@@ -9,15 +9,24 @@ function formatDate(dateStr) {
|
|
|
9
9
|
const d = new Date(dateStr + "T00:00:00");
|
|
10
10
|
return d.toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric" });
|
|
11
11
|
}
|
|
12
|
-
export const BusiestDaySlide = ({ stats, commentary, mode }) => {
|
|
12
|
+
export const BusiestDaySlide = ({ stats, commentary, mode, }) => {
|
|
13
13
|
const frame = useCurrentFrame();
|
|
14
14
|
const c = getColors(mode);
|
|
15
15
|
const day = stats.busiestDay;
|
|
16
16
|
const recentDays = stats.dailyActivity.slice(-14);
|
|
17
|
-
const maxTokens = Math.max(...recentDays.map(d => d.tokenCount), 1);
|
|
17
|
+
const maxTokens = Math.max(...recentDays.map((d) => d.tokenCount), 1);
|
|
18
18
|
return (_jsxs("div", { style: { ...baseSlide, ...gradientBg(45, c), color: c.text }, children: [_jsx(ParticleField, { count: 30, seed: 55 }), _jsx(GlowOrb, { color: c.accent, size: 250, x: 50, y: 30 }), _jsx(GlowOrb, { color: c.pink, size: 180, x: 25, y: 70 }), _jsx(FadeIn, { delay: 5, children: _jsx("div", { style: { ...label, color: c.accent, zIndex: 1 }, children: mode === "dark" ? "Peak Destruction Day" : "Your Biggest Day" }) }), _jsx(FadeIn, { delay: 12, children: _jsx("div", { style: { fontSize: 44, fontWeight: 900, ...glowText(c.accent), color: c.text, zIndex: 1, marginTop: 16 }, children: formatDate(day.date) }) }), _jsx(FadeIn, { delay: 25, children: _jsxs("div", { style: { display: "flex", gap: 48, marginTop: 24, zIndex: 1 }, children: [_jsxs("div", { style: { textAlign: "center" }, children: [_jsx(AnimatedNumber, { value: day.tokenCount, style: { fontSize: 40, fontWeight: 800, color: c.accent }, startFrame: 25, duration: 25 }), _jsx("div", { style: { fontSize: 13, color: c.textMuted, marginTop: 4 }, children: "TOKENS" })] }), _jsxs("div", { style: { textAlign: "center" }, children: [_jsx(AnimatedNumber, { value: day.messageCount, style: { fontSize: 40, fontWeight: 800, color: c.pink }, startFrame: 28, duration: 25 }), _jsx("div", { style: { fontSize: 13, color: c.textMuted, marginTop: 4 }, children: "MESSAGES" })] }), _jsxs("div", { style: { textAlign: "center" }, children: [_jsx(AnimatedNumber, { value: day.sessions, style: { fontSize: 40, fontWeight: 800, color: c.secondary }, startFrame: 31, duration: 25 }), _jsx("div", { style: { fontSize: 13, color: c.textMuted, marginTop: 4 }, children: "SESSIONS" })] })] }) }), _jsx(FadeIn, { delay: 45, style: { width: "88%", marginTop: 28, zIndex: 1 }, children: _jsx("div", { style: { display: "flex", alignItems: "flex-end", gap: 4, height: 100 }, children: recentDays.map((d, i) => {
|
|
19
|
-
const barHeight = interpolate(frame, [48 + i, 68 + i], [0, (d.tokenCount / maxTokens) * 100], {
|
|
19
|
+
const barHeight = interpolate(frame, [48 + i, 68 + i], [0, (d.tokenCount / maxTokens) * 100], {
|
|
20
|
+
extrapolateLeft: "clamp",
|
|
21
|
+
extrapolateRight: "clamp",
|
|
22
|
+
});
|
|
20
23
|
const isBusiest = d.date === day.date;
|
|
21
|
-
return (_jsx("div", { style: {
|
|
24
|
+
return (_jsx("div", { style: {
|
|
25
|
+
flex: 1,
|
|
26
|
+
height: barHeight,
|
|
27
|
+
borderRadius: 3,
|
|
28
|
+
background: isBusiest ? `linear-gradient(180deg, ${c.accent}, ${c.pink})` : `${c.primary}90`,
|
|
29
|
+
boxShadow: isBusiest ? `0 0 8px ${c.accent}80` : undefined,
|
|
30
|
+
} }, d.date));
|
|
22
31
|
}) }) }), _jsx(FadeIn, { delay: 60, children: _jsx("div", { style: { ...memeCaption, color: c.textMuted, zIndex: 1 }, children: commentary }) })] }));
|
|
23
32
|
};
|
|
@@ -4,9 +4,24 @@ import { FadeIn } from "../components/FadeIn";
|
|
|
4
4
|
import { AnimatedNumber } from "../components/AnimatedNumber";
|
|
5
5
|
import { ParticleField } from "../components/ParticleField";
|
|
6
6
|
import { GlowOrb } from "../components/GlowOrb";
|
|
7
|
-
export const CostSlide = ({ stats, commentary, mode }) => {
|
|
7
|
+
export const CostSlide = ({ stats, commentary, mode, }) => {
|
|
8
8
|
const c = getColors(mode);
|
|
9
|
-
const costColor = mode === "dark" ? c.accent :
|
|
10
|
-
return (_jsxs("div", { style: { ...baseSlide, ...gradientBg(320, c), color: c.text }, children: [_jsx(ParticleField, { count: 25, seed: 42 }), _jsx(GlowOrb, { color: costColor, size: 280, x: 50, y: 35 }), _jsx(GlowOrb, { color: c.primary, size: 160, x: 80, y: 75 }), _jsx(FadeIn, { delay: 5, children: _jsx("div", { style: { ...label, color: costColor, zIndex: 1 }, children: mode === "dark" ? "The Price of Progress" : "Estimated Cost" }) }), _jsx(AnimatedNumber, { value: stats.totalCost, decimals: 2, prefix: "$", style: {
|
|
9
|
+
const costColor = mode === "dark" ? c.accent : c.green;
|
|
10
|
+
return (_jsxs("div", { style: { ...baseSlide, ...gradientBg(320, c), color: c.text }, children: [_jsx(ParticleField, { count: 25, seed: 42 }), _jsx(GlowOrb, { color: costColor, size: 280, x: 50, y: 35 }), _jsx(GlowOrb, { color: c.primary, size: 160, x: 80, y: 75 }), _jsx(FadeIn, { delay: 5, children: _jsx("div", { style: { ...label, color: costColor, zIndex: 1 }, children: mode === "dark" ? "The Price of Progress" : "Estimated Cost" }) }), _jsx(AnimatedNumber, { value: stats.totalCost, decimals: 2, prefix: "$", style: {
|
|
11
|
+
...bigNumber,
|
|
12
|
+
...glowText(costColor),
|
|
13
|
+
color: costColor,
|
|
14
|
+
display: "block",
|
|
15
|
+
textAlign: "center",
|
|
16
|
+
zIndex: 1,
|
|
17
|
+
marginTop: 20,
|
|
18
|
+
}, startFrame: 10, duration: 35 }), _jsx(FadeIn, { delay: 40, children: _jsx("div", { style: { display: "flex", gap: 50, marginTop: 36, zIndex: 1 }, children: mode === "dark" ? (_jsxs(_Fragment, { children: [_jsx(Metric, { icon: "\uD83D\uDCA7", value: `${stats.waterLiters.toLocaleString(undefined, { maximumFractionDigits: 1 })}L`, label: "Water" }), _jsx(Metric, { icon: "\uD83C\uDFED", value: `${(stats.co2Grams / 1000).toLocaleString(undefined, { maximumFractionDigits: 1 })}kg`, label: "CO\u2082" }), _jsx(Metric, { icon: "\u26A1", value: `${stats.kwhUsed.toLocaleString(undefined, { maximumFractionDigits: 1 })}`, label: "kWh" })] })) : (_jsxs(_Fragment, { children: [_jsx(Metric, { icon: "\u2615", value: stats.coffeeEquivalent.toLocaleString(), label: "Coffees" }), _jsx(Metric, { icon: "\uD83C\uDFC3", value: stats.marathonEquivalent.toLocaleString(undefined, { maximumFractionDigits: 1 }), label: "Marathons" }), _jsx(Metric, { icon: "\uD83D\uDCD6", value: stats.warAndPeaceEquivalent.toLocaleString(undefined, { maximumFractionDigits: 1 }), label: "War & Peaces" })] })) }) }), _jsx(FadeIn, { delay: 55, children: _jsx("div", { style: { ...memeCaption, color: c.textMuted, zIndex: 1 }, children: commentary }) })] }));
|
|
11
19
|
};
|
|
12
|
-
const Metric = ({ icon, value, label: l }) => (_jsxs("div", { style: { textAlign: "center" }, children: [_jsx("div", { style: { fontSize: 36 }, children: icon }), _jsx("div", { style: { fontSize: 30, fontWeight: 800, color: "#f1f5f9", marginTop: 6 }, children: value }), _jsx("div", { style: {
|
|
20
|
+
const Metric = ({ icon, value, label: l }) => (_jsxs("div", { style: { textAlign: "center" }, children: [_jsx("div", { style: { fontSize: 36 }, children: icon }), _jsx("div", { style: { fontSize: 30, fontWeight: 800, color: "#f1f5f9", marginTop: 6 }, children: value }), _jsx("div", { style: {
|
|
21
|
+
fontSize: 13,
|
|
22
|
+
color: "inherit",
|
|
23
|
+
opacity: 0.7,
|
|
24
|
+
marginTop: 3,
|
|
25
|
+
textTransform: "uppercase",
|
|
26
|
+
letterSpacing: "0.1em",
|
|
27
|
+
}, children: l })] }));
|
|
@@ -13,7 +13,48 @@ export const IntroSlide = ({ mode, periodLabel }) => {
|
|
|
13
13
|
const taglineScale = spring({ frame: Math.max(0, frame - 35), fps, from: 0.5, to: 1, config: { damping: 8 } });
|
|
14
14
|
const taglineOpacity = interpolate(frame, [35, 50], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
|
|
15
15
|
const rotation = interpolate(frame, [0, 150], [0, 360], { extrapolateRight: "extend" });
|
|
16
|
-
return (_jsxs("div", { style: { ...baseSlide, ...gradientBg(135, c), color: c.text }, children: [_jsx(ParticleField, { count: 40 }), _jsx(GlowOrb, { color: c.primary, size: 250, x: 15, y: 25 }), _jsx(GlowOrb, { color: c.secondary, size: 200, x: 80, y: 70 }), _jsx(GlowOrb, { color: c.accent, size: 150, x: 50, y: 15, speed: 0.3 }), _jsx("div", { style: {
|
|
16
|
+
return (_jsxs("div", { style: { ...baseSlide, ...gradientBg(135, c), color: c.text }, children: [_jsx(ParticleField, { count: 40 }), _jsx(GlowOrb, { color: c.primary, size: 250, x: 15, y: 25 }), _jsx(GlowOrb, { color: c.secondary, size: 200, x: 80, y: 70 }), _jsx(GlowOrb, { color: c.accent, size: 150, x: 50, y: 15, speed: 0.3 }), _jsx("div", { style: {
|
|
17
|
+
position: "absolute",
|
|
18
|
+
width: 180,
|
|
19
|
+
height: 180,
|
|
20
|
+
borderRadius: "50%",
|
|
21
|
+
border: `2px solid ${c.primary}30`,
|
|
22
|
+
transform: `scale(${logoScale}) rotate(${rotation}deg)`,
|
|
23
|
+
} }), _jsx("div", { style: {
|
|
24
|
+
position: "absolute",
|
|
25
|
+
width: 150,
|
|
26
|
+
height: 150,
|
|
27
|
+
borderRadius: "50%",
|
|
28
|
+
border: `1px solid ${c.secondary}20`,
|
|
29
|
+
transform: `scale(${logoScale}) rotate(-${rotation * 0.7}deg)`,
|
|
30
|
+
} }), _jsx("div", { style: {
|
|
31
|
+
transform: `scale(${logoScale})`,
|
|
32
|
+
fontSize: 72,
|
|
33
|
+
fontWeight: 900,
|
|
34
|
+
background: `linear-gradient(135deg, ${c.primary}, ${c.secondary})`,
|
|
35
|
+
WebkitBackgroundClip: "text",
|
|
36
|
+
WebkitTextFillColor: "transparent",
|
|
37
|
+
zIndex: 1,
|
|
38
|
+
}, children: "</>" }), _jsx("div", { style: {
|
|
39
|
+
opacity: titleOpacity,
|
|
40
|
+
fontSize: 48,
|
|
41
|
+
fontWeight: 800,
|
|
42
|
+
marginTop: 24,
|
|
43
|
+
letterSpacing: "-0.02em",
|
|
44
|
+
zIndex: 1,
|
|
45
|
+
}, children: _jsx("span", { style: {
|
|
46
|
+
background: `linear-gradient(90deg, ${c.primary}, ${c.accent})`,
|
|
47
|
+
WebkitBackgroundClip: "text",
|
|
48
|
+
WebkitTextFillColor: "transparent",
|
|
49
|
+
}, children: "ccwrap" }) }), _jsx(FadeIn, { delay: 35, children: _jsx("div", { style: {
|
|
50
|
+
opacity: taglineOpacity,
|
|
51
|
+
transform: `scale(${taglineScale})`,
|
|
52
|
+
fontSize: 22,
|
|
53
|
+
fontWeight: 500,
|
|
54
|
+
color: c.textMuted,
|
|
55
|
+
marginTop: 20,
|
|
56
|
+
zIndex: 1,
|
|
57
|
+
}, children: periodLabel.startsWith("Your ")
|
|
17
58
|
? mode === "dark"
|
|
18
59
|
? `${periodLabel} in AI. The planet remembers.`
|
|
19
60
|
: `${periodLabel} in AI-assisted coding`
|
|
@@ -31,15 +31,34 @@ function getModelColor(model, mode) {
|
|
|
31
31
|
return c.secondary;
|
|
32
32
|
return c.accent;
|
|
33
33
|
}
|
|
34
|
-
export const ModelSlide = ({ stats, commentary, mode }) => {
|
|
34
|
+
export const ModelSlide = ({ stats, commentary, mode, }) => {
|
|
35
35
|
const frame = useCurrentFrame();
|
|
36
36
|
const { fps } = useVideoConfig();
|
|
37
37
|
const c = getColors(mode);
|
|
38
38
|
const favoriteColor = getModelColor(stats.favoriteModel, mode);
|
|
39
39
|
const revealScale = spring({ frame: Math.max(0, frame - 15), fps, from: 0.3, to: 1, config: { damping: 10 } });
|
|
40
|
-
return (_jsxs("div", { style: { ...baseSlide, ...gradientBg(260, c), color: c.text }, children: [_jsx(ParticleField, { count: 35, seed: 77 }), _jsx(GlowOrb, { color: favoriteColor, size: 300, x: 50, y: 40 }), _jsx(FadeIn, { delay: 5, children: _jsx("div", { style: { ...label, color: favoriteColor, zIndex: 1 }, children: mode === "dark" ? "Your Weapon of Choice" : "Your #1 Model" }) }), _jsx(FadeIn, { delay: 15, children: _jsx("div", { style: {
|
|
41
|
-
|
|
40
|
+
return (_jsxs("div", { style: { ...baseSlide, ...gradientBg(260, c), color: c.text }, children: [_jsx(ParticleField, { count: 35, seed: 77 }), _jsx(GlowOrb, { color: favoriteColor, size: 300, x: 50, y: 40 }), _jsx(FadeIn, { delay: 5, children: _jsx("div", { style: { ...label, color: favoriteColor, zIndex: 1 }, children: mode === "dark" ? "Your Weapon of Choice" : "Your #1 Model" }) }), _jsx(FadeIn, { delay: 15, children: _jsx("div", { style: {
|
|
41
|
+
fontSize: 90,
|
|
42
|
+
fontWeight: 900,
|
|
43
|
+
...glowText(favoriteColor),
|
|
44
|
+
background: `linear-gradient(135deg, ${favoriteColor}, ${c.text})`,
|
|
45
|
+
WebkitBackgroundClip: "text",
|
|
46
|
+
WebkitTextFillColor: "transparent",
|
|
47
|
+
transform: `scale(${revealScale})`,
|
|
48
|
+
zIndex: 1,
|
|
49
|
+
marginTop: 12,
|
|
50
|
+
}, children: getModelDisplayName(stats.favoriteModel) }) }), _jsx(FadeIn, { delay: 30, children: _jsx("div", { style: { ...memeCaption, color: c.textMuted, zIndex: 1 }, children: commentary }) }), _jsx(FadeIn, { delay: 40, style: { width: "85%", marginTop: 28, zIndex: 1 }, children: _jsx("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: stats.modelUsage.slice(0, 4).map((m, i) => {
|
|
51
|
+
const barWidth = interpolate(frame, [45 + i * 5, 65 + i * 5], [0, m.percentage], {
|
|
52
|
+
extrapolateLeft: "clamp",
|
|
53
|
+
extrapolateRight: "clamp",
|
|
54
|
+
});
|
|
42
55
|
const modelColor = getModelColor(m.model, mode);
|
|
43
|
-
return (_jsxs("div", { style: { display: "flex", alignItems: "center", gap: 12 }, children: [_jsx("div", { style: { width: 100, fontSize: 14, color: c.textMuted, textAlign: "right", fontWeight: 500 }, children: getModelDisplayName(m.model) }), _jsx("div", { style: { flex: 1, height: 24, borderRadius: 12, background: `${c.text}20`, overflow: "hidden" }, children: _jsx("div", { style: {
|
|
56
|
+
return (_jsxs("div", { style: { display: "flex", alignItems: "center", gap: 12 }, children: [_jsx("div", { style: { width: 100, fontSize: 14, color: c.textMuted, textAlign: "right", fontWeight: 500 }, children: getModelDisplayName(m.model) }), _jsx("div", { style: { flex: 1, height: 24, borderRadius: 12, background: `${c.text}20`, overflow: "hidden" }, children: _jsx("div", { style: {
|
|
57
|
+
width: `${barWidth}%`,
|
|
58
|
+
height: "100%",
|
|
59
|
+
borderRadius: 12,
|
|
60
|
+
background: `linear-gradient(90deg, ${modelColor}, ${modelColor}cc)`,
|
|
61
|
+
boxShadow: `0 0 8px ${modelColor}60`,
|
|
62
|
+
} }) }), _jsxs("div", { style: { width: 44, fontSize: 14, color: c.textMuted, fontWeight: 600 }, children: [m.percentage.toFixed(0), "%"] })] }, m.model));
|
|
44
63
|
}) }) })] }));
|
|
45
64
|
};
|
|
@@ -35,14 +35,24 @@ function getTimeTitle(hour, mode) {
|
|
|
35
35
|
return "Night Coder";
|
|
36
36
|
return "Midnight Demon";
|
|
37
37
|
}
|
|
38
|
-
export const PeakHoursSlide = ({ stats, commentary, mode }) => {
|
|
38
|
+
export const PeakHoursSlide = ({ stats, commentary, mode, }) => {
|
|
39
39
|
const frame = useCurrentFrame();
|
|
40
40
|
const c = getColors(mode);
|
|
41
|
-
const maxActivity = Math.max(...stats.hourlyActivity.map(h => h.tokenCount), 1);
|
|
41
|
+
const maxActivity = Math.max(...stats.hourlyActivity.map((h) => h.tokenCount), 1);
|
|
42
42
|
return (_jsxs("div", { style: { ...baseSlide, ...gradientBg(280, c), color: c.text }, children: [_jsx(ParticleField, { count: 20, seed: 200 }), _jsx(GlowOrb, { color: c.secondary, size: 250, x: 50, y: 30 }), _jsx(GlowOrb, { color: c.primary, size: 180, x: 20, y: 70 }), _jsx(FadeIn, { delay: 5, children: _jsx("div", { style: { ...label, color: c.secondary, zIndex: 1 }, children: mode === "dark" ? "Peak Power Draw" : "Peak Coding Hours" }) }), _jsxs(FadeIn, { delay: 12, children: [_jsx("div", { style: { fontSize: 56, fontWeight: 900, ...glowText(c.secondary), color: c.text, zIndex: 1, marginTop: 12 }, children: getTimeTitle(stats.busiestHour, mode) }), _jsxs("div", { style: { fontSize: 18, color: c.textMuted, textAlign: "center", marginTop: 8, zIndex: 1 }, children: ["Your peak: ", _jsx("span", { style: { color: c.secondary, fontWeight: 700 }, children: formatHour(stats.busiestHour) })] })] }), _jsx(FadeIn, { delay: 25, style: { marginTop: 32, zIndex: 1 }, children: _jsxs("div", { style: { width: 700, height: 180 }, children: [_jsx("div", { style: { display: "flex", alignItems: "flex-end", gap: 3, height: 150 }, children: stats.hourlyActivity.map((h, i) => {
|
|
43
43
|
const barHeight = interpolate(frame, [28 + i * 0.5, 48 + i * 0.5], [0, (h.tokenCount / maxActivity) * 150], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
|
|
44
44
|
const isPeak = h.hour === stats.busiestHour;
|
|
45
45
|
const isNight = h.hour >= 22 || h.hour < 5;
|
|
46
|
-
return (_jsx("div", { style: { flex: 1 }, children: _jsx("div", { style: {
|
|
47
|
-
|
|
46
|
+
return (_jsx("div", { style: { flex: 1 }, children: _jsx("div", { style: {
|
|
47
|
+
width: "100%",
|
|
48
|
+
height: barHeight,
|
|
49
|
+
borderRadius: 3,
|
|
50
|
+
background: isPeak
|
|
51
|
+
? `linear-gradient(180deg, ${c.secondary}, ${c.primary})`
|
|
52
|
+
: isNight
|
|
53
|
+
? `${c.primary}cc`
|
|
54
|
+
: `${c.text}60`,
|
|
55
|
+
boxShadow: isPeak ? `0 0 8px ${c.secondary}80` : undefined,
|
|
56
|
+
} }) }, i));
|
|
57
|
+
}) }), _jsx("div", { style: { display: "flex", justifyContent: "space-between", marginTop: 6 }, children: [0, 6, 12, 18, 23].map((h) => (_jsx("div", { style: { fontSize: 12, color: c.textMuted }, children: formatHour(h) }, h))) })] }) }), _jsx(FadeIn, { delay: 55, children: _jsx("div", { style: { ...memeCaption, color: c.textMuted, zIndex: 1 }, children: commentary }) })] }));
|
|
48
58
|
};
|
|
@@ -11,12 +11,32 @@ function formatDuration(minutes) {
|
|
|
11
11
|
const mins = Math.round(minutes % 60);
|
|
12
12
|
return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
|
|
13
13
|
}
|
|
14
|
-
export const SessionSlide = ({ stats, commentary, mode }) => {
|
|
14
|
+
export const SessionSlide = ({ stats, commentary, mode, }) => {
|
|
15
15
|
const frame = useCurrentFrame();
|
|
16
16
|
const { fps } = useVideoConfig();
|
|
17
17
|
const c = getColors(mode);
|
|
18
18
|
const pulse = spring({ frame: frame % 40, fps, from: 1, to: 1.04, config: { damping: 5 } });
|
|
19
|
-
return (_jsxs("div", { style: { ...baseSlide, ...gradientBg(160, c), color: c.text }, children: [_jsx(ParticleField, { count: 25, seed: 123 }), _jsx(GlowOrb, { color: c.pink, size: 300, x: 50, y: 40 }), _jsx(GlowOrb, { color: c.primary, size: 180, x: 75, y: 20 }), _jsx(FadeIn, { delay: 5, children: _jsx("div", { style: { ...label, color: c.pink, zIndex: 1 }, children: mode === "dark" ? "Endurance Test" : "Marathon Session" }) }), _jsxs(FadeIn, { delay: 12, children: [_jsx("div", { style: {
|
|
19
|
+
return (_jsxs("div", { style: { ...baseSlide, ...gradientBg(160, c), color: c.text }, children: [_jsx(ParticleField, { count: 25, seed: 123 }), _jsx(GlowOrb, { color: c.pink, size: 300, x: 50, y: 40 }), _jsx(GlowOrb, { color: c.primary, size: 180, x: 75, y: 20 }), _jsx(FadeIn, { delay: 5, children: _jsx("div", { style: { ...label, color: c.pink, zIndex: 1 }, children: mode === "dark" ? "Endurance Test" : "Marathon Session" }) }), _jsxs(FadeIn, { delay: 12, children: [_jsx("div", { style: {
|
|
20
|
+
fontSize: 80,
|
|
21
|
+
fontWeight: 900,
|
|
22
|
+
...glowText(c.pink),
|
|
23
|
+
color: c.text,
|
|
24
|
+
zIndex: 1,
|
|
25
|
+
marginTop: 16,
|
|
26
|
+
transform: `scale(${pulse})`,
|
|
27
|
+
}, children: formatDuration(stats.longestSession.durationMinutes) }), _jsx("div", { style: { fontSize: 18, color: c.textMuted, zIndex: 1, marginTop: 8 }, children: "longest session" })] }), _jsx(FadeIn, { delay: 30, children: _jsxs("div", { style: { display: "flex", gap: 36, marginTop: 32, zIndex: 1 }, children: [_jsx(Pill, { label: "Sessions", value: stats.totalSessions, color: c.primary, textColor: c.textMuted }), _jsx(Pill, { label: "Avg", value: `${stats.avgSessionMinutes}m`, color: c.secondary, textColor: c.textMuted }), _jsx(Pill, { label: "Messages", value: stats.totalMessages, color: mode === "dark" ? c.textMuted : c.green, textColor: c.textMuted }), _jsx(Pill, { label: "Streak", value: `${stats.streakDays}d`, color: c.gold, textColor: c.textMuted })] }) }), _jsx(FadeIn, { delay: 50, children: _jsxs("div", { style: { display: "flex", gap: 14, marginTop: 24, zIndex: 1 }, children: [_jsx(Chip, { label: "Active Days", value: stats.totalDaysActive, color: c.accent, textColor: c.textMuted }), _jsx(Chip, { label: "Night Owl", value: `${stats.nightOwlScore}%`, color: c.primary, textColor: c.textMuted }), _jsx(Chip, { label: "Weekend", value: `${stats.weekendWarriorScore}%`, color: c.secondary, textColor: c.textMuted })] }) }), _jsx(FadeIn, { delay: 60, children: _jsx("div", { style: { ...memeCaption, color: c.textMuted, zIndex: 1 }, children: commentary }) })] }));
|
|
20
28
|
};
|
|
21
|
-
const Pill = ({ label: l, value, color, textColor }) => (_jsxs("div", { style: { textAlign: "center" }, children: [_jsx("div", { style: { fontSize: 34, fontWeight: 800, color, ...glowText(color) }, children: typeof value === "number" ? value.toLocaleString() : value }), _jsx("div", { style: {
|
|
22
|
-
|
|
29
|
+
const Pill = ({ label: l, value, color, textColor, }) => (_jsxs("div", { style: { textAlign: "center" }, children: [_jsx("div", { style: { fontSize: 34, fontWeight: 800, color, ...glowText(color) }, children: typeof value === "number" ? value.toLocaleString() : value }), _jsx("div", { style: {
|
|
30
|
+
fontSize: 12,
|
|
31
|
+
color: textColor,
|
|
32
|
+
marginTop: 3,
|
|
33
|
+
textTransform: "uppercase",
|
|
34
|
+
letterSpacing: "0.1em",
|
|
35
|
+
}, children: l })] }));
|
|
36
|
+
const Chip = ({ label: l, value, color, textColor, }) => (_jsxs("div", { style: {
|
|
37
|
+
background: `${color}20`,
|
|
38
|
+
border: `1px solid ${color}50`,
|
|
39
|
+
borderRadius: 10,
|
|
40
|
+
padding: "8px 18px",
|
|
41
|
+
textAlign: "center",
|
|
42
|
+
}, children: [_jsx("div", { style: { fontSize: 24, fontWeight: 700, color }, children: typeof value === "number" ? value.toLocaleString() : value }), _jsx("div", { style: { fontSize: 11, color: textColor, marginTop: 2 }, children: l })] }));
|
|
@@ -34,7 +34,7 @@ function getSummaryTitle(periodLabel, mode) {
|
|
|
34
34
|
return mode === "dark" ? match[1] : match[0];
|
|
35
35
|
return periodLabel; // custom date range string
|
|
36
36
|
}
|
|
37
|
-
export const SummarySlide = ({ stats, commentary, mode }) => {
|
|
37
|
+
export const SummarySlide = ({ stats, commentary, mode, }) => {
|
|
38
38
|
const frame = useCurrentFrame();
|
|
39
39
|
const { fps } = useVideoConfig();
|
|
40
40
|
const c = getColors(mode);
|
|
@@ -42,11 +42,43 @@ export const SummarySlide = ({ stats, commentary, mode }) => {
|
|
|
42
42
|
const cardOpacity = interpolate(frame, [5, 20], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
|
|
43
43
|
const rotation = interpolate(Math.sin(frame * 0.02), [-1, 1], [-0.8, 0.8]);
|
|
44
44
|
return (_jsxs("div", { style: { ...baseSlide, ...gradientBg(135, c), color: c.text }, children: [_jsx(ParticleField, { count: 50, seed: 444 }), _jsx(GlowOrb, { color: c.primary, size: 240, x: 25, y: 20 }), _jsx(GlowOrb, { color: c.secondary, size: 200, x: 75, y: 75 }), _jsx(GlowOrb, { color: c.accent, size: 170, x: 50, y: 50, speed: 0.2 }), _jsxs("div", { style: {
|
|
45
|
-
opacity: cardOpacity,
|
|
45
|
+
opacity: cardOpacity,
|
|
46
|
+
transform: `scale(${cardScale}) rotate(${rotation}deg)`,
|
|
46
47
|
background: `linear-gradient(145deg, ${c.bgGradient1}ee, ${c.bgGradient3}ee)`,
|
|
47
|
-
border: `1px solid ${c.primary}50`,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
border: `1px solid ${c.primary}50`,
|
|
49
|
+
borderRadius: 24,
|
|
50
|
+
padding: "36px 44px",
|
|
51
|
+
maxWidth: 620,
|
|
52
|
+
width: "92%",
|
|
53
|
+
boxShadow: `0 0 36px ${c.primary}20, 0 12px 36px rgba(0,0,0,0.5)`,
|
|
54
|
+
zIndex: 1,
|
|
55
|
+
}, children: [_jsx("div", { style: { textAlign: "center", marginBottom: 20 }, children: _jsx("div", { style: {
|
|
56
|
+
fontSize: 28,
|
|
57
|
+
fontWeight: 900,
|
|
58
|
+
...glowText(c.primary),
|
|
59
|
+
background: `linear-gradient(90deg, ${c.primary}, ${c.secondary})`,
|
|
60
|
+
WebkitBackgroundClip: "text",
|
|
61
|
+
WebkitTextFillColor: "transparent",
|
|
62
|
+
}, children: getSummaryTitle(stats.periodLabel, mode) }) }), _jsx(FadeIn, { delay: 15, children: _jsxs("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: "14px 36px" }, children: [_jsx(Row, { label: "Tokens", value: formatNumber(stats.totalTokens), color: c.gold, textColor: c.textMuted }), _jsx(Row, { label: "Cost", value: `$${stats.totalCost.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`, color: mode === "dark" ? c.accent : c.green, textColor: c.textMuted }), _jsx(Row, { label: "Sessions", value: stats.totalSessions.toLocaleString(), color: c.primary, textColor: c.textMuted }), _jsx(Row, { label: "Messages", value: formatNumber(stats.totalMessages), color: c.pink, textColor: c.textMuted }), _jsx(Row, { label: "Active Days", value: stats.totalDaysActive.toLocaleString(), color: c.secondary, textColor: c.textMuted }), _jsx(Row, { label: "Fav Model", value: getModelShortName(stats.favoriteModel), color: c.accent, textColor: c.textMuted }), mode === "dark" && (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Water", value: `${stats.waterLiters.toLocaleString(undefined, { maximumFractionDigits: 1 })}L`, color: c.secondary, textColor: c.textMuted }), _jsx(Row, { label: "CO\u2082", value: `${(stats.co2Grams / 1000).toLocaleString(undefined, { maximumFractionDigits: 1 })}kg`, color: c.textMuted, textColor: c.textMuted })] }))] }) }), _jsx(FadeIn, { delay: 30, children: _jsxs("div", { style: {
|
|
63
|
+
marginTop: 20,
|
|
64
|
+
textAlign: "center",
|
|
65
|
+
padding: "12px 20px",
|
|
66
|
+
background: `linear-gradient(135deg, ${c.primary}20, ${c.accent}20)`,
|
|
67
|
+
borderRadius: 12,
|
|
68
|
+
border: `1px solid ${c.primary}40`,
|
|
69
|
+
}, children: [_jsx("div", { style: { fontSize: 12, color: c.textMuted, textTransform: "uppercase", letterSpacing: "0.15em" }, children: "Your Archetype" }), _jsx("div", { style: {
|
|
70
|
+
fontSize: 26,
|
|
71
|
+
fontWeight: 800,
|
|
72
|
+
background: `linear-gradient(90deg, ${c.primary}, ${c.accent})`,
|
|
73
|
+
WebkitBackgroundClip: "text",
|
|
74
|
+
WebkitTextFillColor: "transparent",
|
|
75
|
+
marginTop: 3,
|
|
76
|
+
}, children: stats.archetype })] }) }), _jsx(FadeIn, { delay: 40, children: _jsx("div", { style: {
|
|
77
|
+
textAlign: "center",
|
|
78
|
+
marginTop: 16,
|
|
79
|
+
fontSize: 15,
|
|
80
|
+
fontStyle: "italic",
|
|
81
|
+
color: c.textMuted,
|
|
82
|
+
}, children: commentary }) }), _jsx(FadeIn, { delay: 50, children: _jsxs("div", { style: { textAlign: "center", marginTop: 14, fontSize: 12, color: c.textDim }, children: ["ccwrap", mode === "dark" ? " — know your impact" : " — put meme software on the map"] }) })] })] }));
|
|
51
83
|
};
|
|
52
|
-
const Row = ({ label, value, color, textColor }) => (_jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [_jsx("div", { style: { fontSize: 14, color: textColor }, children: label }), _jsx("div", { style: { fontSize: 20, fontWeight: 700, color }, children: value })] }));
|
|
84
|
+
const Row = ({ label, value, color, textColor, }) => (_jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [_jsx("div", { style: { fontSize: 14, color: textColor }, children: label }), _jsx("div", { style: { fontSize: 20, fontWeight: 700, color }, children: value })] }));
|
|
@@ -7,19 +7,34 @@ import { ParticleField } from "../components/ParticleField";
|
|
|
7
7
|
import { GlowOrb } from "../components/GlowOrb";
|
|
8
8
|
function formatTokens(n) {
|
|
9
9
|
if (n >= 1_000_000_000)
|
|
10
|
-
return { value: Math.round(n / 1_000_000_000 * 10) / 10, suffix: "B" };
|
|
10
|
+
return { value: Math.round((n / 1_000_000_000) * 10) / 10, suffix: "B" };
|
|
11
11
|
if (n >= 1_000_000)
|
|
12
|
-
return { value: Math.round(n / 1_000_000 * 10) / 10, suffix: "M" };
|
|
12
|
+
return { value: Math.round((n / 1_000_000) * 10) / 10, suffix: "M" };
|
|
13
13
|
if (n >= 1_000)
|
|
14
|
-
return { value: Math.round(n / 1_000 * 10) / 10, suffix: "K" };
|
|
14
|
+
return { value: Math.round((n / 1_000) * 10) / 10, suffix: "K" };
|
|
15
15
|
return { value: n, suffix: "" };
|
|
16
16
|
}
|
|
17
|
-
export const TokensSlide = ({ stats, commentary, mode }) => {
|
|
17
|
+
export const TokensSlide = ({ stats, commentary, mode, }) => {
|
|
18
18
|
const frame = useCurrentFrame();
|
|
19
19
|
const c = getColors(mode);
|
|
20
20
|
const formatted = formatTokens(stats.totalTokens);
|
|
21
21
|
const shake = frame > 30 && frame < 45 ? Math.sin(frame * 2) * 2 : 0;
|
|
22
22
|
const accentColor = mode === "dark" ? c.accent : c.gold;
|
|
23
|
-
return (_jsxs("div", { style: { ...baseSlide, ...gradientBg(200, c), color: c.text }, children: [_jsx(ParticleField, { count: 35, seed: 99 }), _jsx(GlowOrb, { color: accentColor, size: 300, x: 50, y: 40 }), _jsx(GlowOrb, { color: c.accent, size: 180, x: 20, y: 75 }), _jsx(FadeIn, { delay: 5, children: _jsx("div", { style: { ...label, color: accentColor, zIndex: 1 }, children: "Total Tokens Consumed" }) }), _jsx("div", { style: { transform: `translateX(${shake}px)`, zIndex: 1, marginTop: 20 }, children: _jsx(AnimatedNumber, { value: formatted.value, decimals: formatted.suffix ? 1 : 0, suffix: formatted.suffix, style: {
|
|
23
|
+
return (_jsxs("div", { style: { ...baseSlide, ...gradientBg(200, c), color: c.text }, children: [_jsx(ParticleField, { count: 35, seed: 99 }), _jsx(GlowOrb, { color: accentColor, size: 300, x: 50, y: 40 }), _jsx(GlowOrb, { color: c.accent, size: 180, x: 20, y: 75 }), _jsx(FadeIn, { delay: 5, children: _jsx("div", { style: { ...label, color: accentColor, zIndex: 1 }, children: "Total Tokens Consumed" }) }), _jsx("div", { style: { transform: `translateX(${shake}px)`, zIndex: 1, marginTop: 20 }, children: _jsx(AnimatedNumber, { value: formatted.value, decimals: formatted.suffix ? 1 : 0, suffix: formatted.suffix, style: {
|
|
24
|
+
...bigNumber,
|
|
25
|
+
...glowText(accentColor),
|
|
26
|
+
background: `linear-gradient(135deg, ${accentColor}, ${c.text})`,
|
|
27
|
+
WebkitBackgroundClip: "text",
|
|
28
|
+
WebkitTextFillColor: "transparent",
|
|
29
|
+
display: "block",
|
|
30
|
+
textAlign: "center",
|
|
31
|
+
}, startFrame: 10, duration: 40 }) }), _jsx(FadeIn, { delay: 45, children: _jsxs("div", { style: { display: "flex", gap: 50, marginTop: 32, zIndex: 1 }, children: [_jsx(StatBox, { label: "Input", value: formatTokens(stats.totalInputTokens), color: c.secondary }), _jsx(StatBox, { label: "Output", value: formatTokens(stats.totalOutputTokens), color: mode === "dark" ? c.textMuted : c.green }), mode === "dark" && (_jsx(StatBox, { label: "Water", value: { value: stats.waterLiters, suffix: "L" }, color: c.secondary })), mode === "dark" && (_jsx(StatBox, { label: "Energy", value: { value: stats.kwhUsed, suffix: " kWh" }, color: c.gold }))] }) }), _jsx(FadeIn, { delay: 55, children: _jsx("div", { style: { ...memeCaption, color: c.textMuted, zIndex: 1 }, children: commentary }) })] }));
|
|
24
32
|
};
|
|
25
|
-
const StatBox = ({ label: l, value, color }) => (_jsxs("div", { style: { textAlign: "center" }, children: [_jsxs("div", { style: { fontSize: 36, fontWeight: 800, color, ...glowText(color) }, children: [value.value.toLocaleString(undefined, { maximumFractionDigits: value.suffix ? 1 : 0 }), value.suffix] }), _jsx("div", { style: {
|
|
33
|
+
const StatBox = ({ label: l, value, color, }) => (_jsxs("div", { style: { textAlign: "center" }, children: [_jsxs("div", { style: { fontSize: 36, fontWeight: 800, color, ...glowText(color) }, children: [value.value.toLocaleString(undefined, { maximumFractionDigits: value.suffix ? 1 : 0 }), value.suffix] }), _jsx("div", { style: {
|
|
34
|
+
fontSize: 13,
|
|
35
|
+
color: "inherit",
|
|
36
|
+
opacity: 0.7,
|
|
37
|
+
marginTop: 4,
|
|
38
|
+
textTransform: "uppercase",
|
|
39
|
+
letterSpacing: "0.1em",
|
|
40
|
+
}, children: l })] }));
|
package/dist/video/styles.d.ts
CHANGED
|
@@ -16,21 +16,6 @@ declare const sassyColors: {
|
|
|
16
16
|
};
|
|
17
17
|
export type ColorScheme = typeof sassyColors;
|
|
18
18
|
export declare function getColors(mode: "sassy" | "dark"): ColorScheme;
|
|
19
|
-
export declare const colors: {
|
|
20
|
-
bg: string;
|
|
21
|
-
bgGradient1: string;
|
|
22
|
-
bgGradient2: string;
|
|
23
|
-
bgGradient3: string;
|
|
24
|
-
primary: string;
|
|
25
|
-
secondary: string;
|
|
26
|
-
accent: string;
|
|
27
|
-
gold: string;
|
|
28
|
-
green: string;
|
|
29
|
-
pink: string;
|
|
30
|
-
text: string;
|
|
31
|
-
textMuted: string;
|
|
32
|
-
textDim: string;
|
|
33
|
-
};
|
|
34
19
|
export declare const fonts: {
|
|
35
20
|
heading: string;
|
|
36
21
|
body: string;
|
package/dist/video/styles.js
CHANGED
|
@@ -32,7 +32,6 @@ const darkColors = {
|
|
|
32
32
|
export function getColors(mode) {
|
|
33
33
|
return mode === "dark" ? darkColors : sassyColors;
|
|
34
34
|
}
|
|
35
|
-
export const colors = sassyColors;
|
|
36
35
|
export const fonts = {
|
|
37
36
|
heading: "Inter, system-ui, sans-serif",
|
|
38
37
|
body: "Inter, system-ui, sans-serif",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccwrap",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Claude Code Wrapped - Your AI coding stats, meme-ified into a shareable video",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,6 +23,12 @@
|
|
|
23
23
|
"start": "tsx src/cli.ts",
|
|
24
24
|
"preview": "remotion preview src/video/index.ts",
|
|
25
25
|
"render": "tsx src/cli.ts",
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"test:coverage": "vitest run --coverage",
|
|
28
|
+
"lint": "eslint src/ tests/",
|
|
29
|
+
"lint:fix": "eslint src/ tests/ --fix",
|
|
30
|
+
"format": "prettier --write 'src/**/*.{ts,tsx}' 'tests/**/*.ts'",
|
|
31
|
+
"format:check": "prettier --check 'src/**/*.{ts,tsx}' 'tests/**/*.ts'",
|
|
26
32
|
"prepublishOnly": "npm run build"
|
|
27
33
|
},
|
|
28
34
|
"bin": {
|
|
@@ -50,9 +56,16 @@
|
|
|
50
56
|
"remotion": "^4.0.0"
|
|
51
57
|
},
|
|
52
58
|
"devDependencies": {
|
|
59
|
+
"@eslint/js": "^10.0.1",
|
|
53
60
|
"@types/node": "^22.0.0",
|
|
54
61
|
"@types/react": "^18.3.0",
|
|
62
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
63
|
+
"eslint": "^10.0.3",
|
|
64
|
+
"eslint-config-prettier": "^10.1.8",
|
|
65
|
+
"prettier": "^3.8.1",
|
|
55
66
|
"tsx": "^4.0.0",
|
|
56
|
-
"typescript": "^5.6.0"
|
|
67
|
+
"typescript": "^5.6.0",
|
|
68
|
+
"typescript-eslint": "^8.57.1",
|
|
69
|
+
"vitest": "^4.1.0"
|
|
57
70
|
}
|
|
58
71
|
}
|