ccwrap 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +15 -28
- package/dist/data/commentary.d.ts +1 -12
- package/dist/data/commentary.js +11 -3
- package/dist/data/parser.d.ts +9 -2
- package/dist/data/parser.js +114 -126
- 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 +10 -1
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,18 +81,10 @@ 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
86
|
.description("ccwrap - Your AI coding stats, meme-ified into a shareable video")
|
|
92
|
-
.version("0.
|
|
87
|
+
.version("0.3.1")
|
|
93
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)")
|
|
@@ -101,17 +96,16 @@ program
|
|
|
101
96
|
.action(async (options) => {
|
|
102
97
|
const mode = options.mode === "dark" ? "dark" : "sassy";
|
|
103
98
|
const isDark = mode === "dark";
|
|
104
|
-
console.log(isDark ? chalk.red(
|
|
105
|
-
console.log(chalk.gray(isDark
|
|
106
|
-
? " Your AI usage. The planet remembers.\n"
|
|
107
|
-
: " 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"));
|
|
108
101
|
// Resolve date range
|
|
109
102
|
const { range, periodLabel } = resolveDateRange(options);
|
|
110
103
|
// Step 1: Load and compute stats
|
|
111
|
-
const periodHint = range
|
|
112
|
-
|
|
113
|
-
:
|
|
114
|
-
|
|
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();
|
|
115
109
|
let stats;
|
|
116
110
|
try {
|
|
117
111
|
stats = await loadAndComputeStats(range, periodLabel, options.skipWeekends);
|
|
@@ -151,16 +145,9 @@ program
|
|
|
151
145
|
text: isDark ? "Generating dark commentary..." : "Generating sassy commentary...",
|
|
152
146
|
color: isDark ? "red" : "magenta",
|
|
153
147
|
}).start();
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
commentarySpinner.succeed(chalk.green("Commentary generated!"));
|
|
158
|
-
}
|
|
159
|
-
catch (err) {
|
|
160
|
-
commentarySpinner.fail(chalk.yellow(`Commentary generation failed, using fallbacks`));
|
|
161
|
-
// Fallbacks are handled inside generateCommentary
|
|
162
|
-
commentary = await generateCommentary(stats, mode);
|
|
163
|
-
}
|
|
148
|
+
// generateCommentary handles its own errors and returns fallbacks
|
|
149
|
+
const commentary = await generateCommentary(stats, mode);
|
|
150
|
+
commentarySpinner.succeed(chalk.green("Commentary generated!"));
|
|
164
151
|
// Step 3: Render video
|
|
165
152
|
const renderSpinner = ora({ text: "Rendering your Wrapped video...", color: isDark ? "red" : "magenta" }).start();
|
|
166
153
|
try {
|
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
import type { WrappedStats } from "./types";
|
|
2
|
-
export interface Commentary {
|
|
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";
|
|
13
2
|
export declare function buildPrompt(stats: WrappedStats, mode: Mode): string;
|
|
14
3
|
export declare function generateCommentary(stats: WrappedStats, mode: Mode): Promise<Commentary>;
|
package/dist/data/commentary.js
CHANGED
|
@@ -81,12 +81,20 @@ export async function generateCommentary(stats, mode) {
|
|
|
81
81
|
max_tokens: 1024,
|
|
82
82
|
messages: [{ role: "user", content: buildPrompt(stats, mode) }],
|
|
83
83
|
});
|
|
84
|
-
const
|
|
84
|
+
const raw = message.content[0]?.type === "text" ? message.content[0].text : "";
|
|
85
|
+
// Strip markdown fences if the model wraps the JSON in ```json ... ```
|
|
86
|
+
const text = raw.replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/i, "");
|
|
85
87
|
const parsed = JSON.parse(text);
|
|
86
88
|
// Validate all keys exist
|
|
87
89
|
const keys = [
|
|
88
|
-
"tokensLine",
|
|
89
|
-
"
|
|
90
|
+
"tokensLine",
|
|
91
|
+
"costLine",
|
|
92
|
+
"modelLine",
|
|
93
|
+
"busiestDayLine",
|
|
94
|
+
"sessionLine",
|
|
95
|
+
"peakHoursLine",
|
|
96
|
+
"archetypeLine",
|
|
97
|
+
"summaryLine",
|
|
90
98
|
];
|
|
91
99
|
const fallback = mode === "dark" ? darkFallbacks : sassyFallbacks;
|
|
92
100
|
for (const key of keys) {
|
package/dist/data/parser.d.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import type { RawJSONLEntry, WrappedStats, DateRange } from "./types.js";
|
|
2
|
-
|
|
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;
|
|
3
9
|
export declare function extractProject(filePath: string): string;
|
|
4
10
|
export declare function loadAndComputeStats(range?: DateRange, periodLabel?: string, skipWeekends?: boolean): Promise<WrappedStats>;
|
|
5
|
-
export declare function computeStatsFromEntries(
|
|
11
|
+
export declare function computeStatsFromEntries(rawEntries: (RawJSONLEntry & {
|
|
6
12
|
_file: string;
|
|
7
13
|
})[], opts?: {
|
|
8
14
|
range?: DateRange;
|
|
@@ -21,3 +27,4 @@ export declare function computeArchetype(params: {
|
|
|
21
27
|
archetype: string;
|
|
22
28
|
archetypeDescription: string;
|
|
23
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
|
-
export 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) {
|
|
@@ -68,9 +74,7 @@ export 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
79
|
export async function loadAndComputeStats(range, periodLabel, skipWeekends) {
|
|
76
80
|
const files = await findJSONLFiles();
|
|
@@ -90,55 +94,92 @@ export async function loadAndComputeStats(range, periodLabel, skipWeekends) {
|
|
|
90
94
|
}
|
|
91
95
|
return computeStatsFromEntries(allEntries, { range, periodLabel, skipWeekends });
|
|
92
96
|
}
|
|
93
|
-
export function computeStatsFromEntries(
|
|
97
|
+
export function computeStatsFromEntries(rawEntries, opts = {}) {
|
|
94
98
|
const { range, periodLabel, skipWeekends } = opts;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
98
105
|
if (range) {
|
|
99
106
|
const sinceMs = range.since.getTime();
|
|
100
107
|
const untilMs = range.until.getTime();
|
|
101
|
-
const before =
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return ts >= sinceMs && ts <= untilMs;
|
|
105
|
-
});
|
|
106
|
-
allEntries.length = 0;
|
|
107
|
-
allEntries.push(...filtered);
|
|
108
|
-
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) {
|
|
109
111
|
throw new Error(`No usage data found in the selected time period (${before} entries exist outside this range).`);
|
|
110
112
|
}
|
|
111
113
|
}
|
|
112
|
-
// Filter out weekends
|
|
114
|
+
// Filter out weekends
|
|
113
115
|
if (skipWeekends) {
|
|
114
|
-
const before =
|
|
115
|
-
|
|
116
|
-
const day =
|
|
116
|
+
const before = entries.length;
|
|
117
|
+
entries = entries.filter((e) => {
|
|
118
|
+
const day = e._date.getDay();
|
|
117
119
|
return day !== 0 && day !== 6;
|
|
118
120
|
});
|
|
119
|
-
|
|
120
|
-
allEntries.push(...filtered);
|
|
121
|
-
if (allEntries.length === 0) {
|
|
121
|
+
if (entries.length === 0) {
|
|
122
122
|
throw new Error(`No weekday usage data found (${before} weekend entries were excluded).`);
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
|
-
//
|
|
125
|
+
// Single pass: sessions, hourly, daily, model usage, weekend count
|
|
126
126
|
const sessionMap = new Map();
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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++;
|
|
163
|
+
}
|
|
164
|
+
// Finalize daily sessions
|
|
165
|
+
for (const [date, sessionSet] of dailySessionSets) {
|
|
166
|
+
dailyMap.get(date).sessions = sessionSet.size;
|
|
132
167
|
}
|
|
168
|
+
const dailyActivity = [...dailyMap.values()].sort((a, b) => a.date.localeCompare(b.date));
|
|
133
169
|
// Compute session stats
|
|
170
|
+
const costCache = new Map();
|
|
134
171
|
const sessions = [];
|
|
135
|
-
for (const [sessionId,
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
const endTime = new Date(Math.max(...timestamps.map(t => t.getTime())));
|
|
139
|
-
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();
|
|
140
175
|
let totalInput = 0, totalOutput = 0, totalCacheCreate = 0, totalCacheRead = 0, totalCost = 0;
|
|
141
|
-
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);
|
|
142
183
|
const u = entry.message?.usage;
|
|
143
184
|
if (u) {
|
|
144
185
|
totalInput += u.input_tokens ?? 0;
|
|
@@ -150,73 +191,26 @@ export function computeStatsFromEntries(allEntries, opts = {}) {
|
|
|
150
191
|
totalCost += entry.costUSD;
|
|
151
192
|
}
|
|
152
193
|
else if (entry.message?.model && u) {
|
|
153
|
-
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);
|
|
154
195
|
}
|
|
155
196
|
}
|
|
156
197
|
sessions.push({
|
|
157
198
|
sessionId,
|
|
158
|
-
project: extractProject(
|
|
159
|
-
startTime,
|
|
160
|
-
endTime,
|
|
161
|
-
durationMinutes: (
|
|
162
|
-
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,
|
|
163
204
|
totalInputTokens: totalInput,
|
|
164
205
|
totalOutputTokens: totalOutput,
|
|
165
206
|
totalCacheCreationTokens: totalCacheCreate,
|
|
166
207
|
totalCacheReadTokens: totalCacheRead,
|
|
167
208
|
totalTokens: totalInput + totalOutput + totalCacheCreate + totalCacheRead,
|
|
168
|
-
models,
|
|
209
|
+
models: [...models],
|
|
169
210
|
costUSD: totalCost,
|
|
170
211
|
});
|
|
171
212
|
}
|
|
172
|
-
//
|
|
173
|
-
const hourly = Array.from({ length: 24 }, (_, i) => ({
|
|
174
|
-
hour: i,
|
|
175
|
-
messageCount: 0,
|
|
176
|
-
tokenCount: 0,
|
|
177
|
-
}));
|
|
178
|
-
for (const entry of allEntries) {
|
|
179
|
-
const hour = new Date(entry.timestamp).getHours();
|
|
180
|
-
hourly[hour].messageCount++;
|
|
181
|
-
const u = entry.message?.usage;
|
|
182
|
-
if (u) {
|
|
183
|
-
hourly[hour].tokenCount += (u.input_tokens ?? 0) + (u.output_tokens ?? 0);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
// Daily activity
|
|
187
|
-
const dailyMap = new Map();
|
|
188
|
-
const dailySessionSets = new Map();
|
|
189
|
-
for (const entry of allEntries) {
|
|
190
|
-
const date = new Date(entry.timestamp).toISOString().split("T")[0];
|
|
191
|
-
if (!dailyMap.has(date)) {
|
|
192
|
-
dailyMap.set(date, { date, messageCount: 0, tokenCount: 0, costUSD: 0, sessions: 0 });
|
|
193
|
-
dailySessionSets.set(date, new Set());
|
|
194
|
-
}
|
|
195
|
-
const day = dailyMap.get(date);
|
|
196
|
-
day.messageCount++;
|
|
197
|
-
const u = entry.message?.usage;
|
|
198
|
-
if (u) {
|
|
199
|
-
day.tokenCount += (u.input_tokens ?? 0) + (u.output_tokens ?? 0);
|
|
200
|
-
}
|
|
201
|
-
dailySessionSets.get(date).add(entry.sessionId);
|
|
202
|
-
}
|
|
203
|
-
for (const [date, sessionSet] of dailySessionSets) {
|
|
204
|
-
dailyMap.get(date).sessions = sessionSet.size;
|
|
205
|
-
}
|
|
206
|
-
const dailyActivity = [...dailyMap.values()].sort((a, b) => a.date.localeCompare(b.date));
|
|
207
|
-
// Model usage
|
|
208
|
-
const modelMap = new Map();
|
|
209
|
-
for (const entry of allEntries) {
|
|
210
|
-
const model = entry.message?.model ?? "unknown";
|
|
211
|
-
if (!modelMap.has(model))
|
|
212
|
-
modelMap.set(model, { tokens: 0, messages: 0, cost: 0 });
|
|
213
|
-
const m = modelMap.get(model);
|
|
214
|
-
m.messages++;
|
|
215
|
-
const u = entry.message?.usage;
|
|
216
|
-
if (u) {
|
|
217
|
-
m.tokens += (u.input_tokens ?? 0) + (u.output_tokens ?? 0);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
213
|
+
// Model usage percentages
|
|
220
214
|
const totalTokensAll = [...modelMap.values()].reduce((sum, m) => sum + m.tokens, 0);
|
|
221
215
|
const modelUsage = [...modelMap.entries()]
|
|
222
216
|
.map(([model, data]) => ({
|
|
@@ -232,16 +226,16 @@ export function computeStatsFromEntries(allEntries, opts = {}) {
|
|
|
232
226
|
const totalInputTokens = sessions.reduce((s, sess) => s + sess.totalInputTokens, 0);
|
|
233
227
|
const totalOutputTokens = sessions.reduce((s, sess) => s + sess.totalOutputTokens, 0);
|
|
234
228
|
const totalCost = sessions.reduce((s, sess) => s + sess.costUSD, 0);
|
|
235
|
-
const totalMessages =
|
|
236
|
-
// Dates
|
|
237
|
-
const firstActivity =
|
|
238
|
-
const lastActivity =
|
|
239
|
-
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());
|
|
240
234
|
const totalDaysActive = uniqueDays.size;
|
|
241
235
|
// Peak stats
|
|
242
|
-
const busiestDay = dailyActivity.reduce((best, d) => d.tokenCount > best.tokenCount ? d : best);
|
|
243
|
-
const busiestHour = hourly.reduce((best, h) => h.tokenCount > best.tokenCount ? h : best).hour;
|
|
244
|
-
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));
|
|
245
239
|
// Project frequency
|
|
246
240
|
const projectCounts = new Map();
|
|
247
241
|
for (const sess of sessions) {
|
|
@@ -264,15 +258,9 @@ export function computeStatsFromEntries(allEntries, opts = {}) {
|
|
|
264
258
|
}
|
|
265
259
|
}
|
|
266
260
|
// Scores
|
|
267
|
-
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);
|
|
268
262
|
const nightOwlScore = totalTokensAll > 0 ? Math.round((nightTokens / totalTokensAll) * 100) : 0;
|
|
269
|
-
const
|
|
270
|
-
const day = new Date(e.timestamp).getDay();
|
|
271
|
-
return day === 0 || day === 6;
|
|
272
|
-
});
|
|
273
|
-
const weekendWarriorScore = allEntries.length > 0
|
|
274
|
-
? Math.round((weekendEntries.length / allEntries.length) * 100)
|
|
275
|
-
: 0;
|
|
263
|
+
const weekendWarriorScore = entries.length > 0 ? Math.round((weekendCount / entries.length) * 100) : 0;
|
|
276
264
|
const favoriteModel = modelUsage[0]?.model ?? "unknown";
|
|
277
265
|
const totalSessionHours = sessions.reduce((s, sess) => s + sess.durationMinutes, 0) / 60;
|
|
278
266
|
const avgSessionMinutes = sessions.length > 0 ? sessions.reduce((s, sess) => s + sess.durationMinutes, 0) / sessions.length : 0;
|
|
@@ -317,15 +305,15 @@ export function computeStatsFromEntries(allEntries, opts = {}) {
|
|
|
317
305
|
marathonEquivalent: Math.round((totalSessionHours / 4.5) * 100) / 100,
|
|
318
306
|
// Environmental estimates (rough but directionally correct)
|
|
319
307
|
// ~0.5 liters of water per 1M tokens for data center cooling
|
|
320
|
-
waterLiters: Math.round(totalTokens / 1_000_000 * 0.5 * 100) / 100,
|
|
308
|
+
waterLiters: Math.round((totalTokens / 1_000_000) * 0.5 * 100) / 100,
|
|
321
309
|
// ~0.3g CO2 per 1K tokens (inference energy + grid mix)
|
|
322
|
-
co2Grams: Math.round(totalTokens / 1_000 * 0.3),
|
|
310
|
+
co2Grams: Math.round((totalTokens / 1_000) * 0.3),
|
|
323
311
|
// ~0.001 kWh per 1K tokens
|
|
324
|
-
kwhUsed: Math.round(totalTokens / 1_000 * 0.001 * 100) / 100,
|
|
312
|
+
kwhUsed: Math.round((totalTokens / 1_000) * 0.001 * 100) / 100,
|
|
325
313
|
};
|
|
326
314
|
}
|
|
327
315
|
export function computeArchetype(params) {
|
|
328
|
-
const { nightOwlScore, weekendWarriorScore, avgSessionMinutes, totalSessions, streakDays, totalTokens, favoriteModel } = params;
|
|
316
|
+
const { nightOwlScore, weekendWarriorScore, avgSessionMinutes, totalSessions, streakDays, totalTokens, favoriteModel, } = params;
|
|
329
317
|
if (nightOwlScore > 50) {
|
|
330
318
|
return {
|
|
331
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.1",
|
|
4
4
|
"description": "Claude Code Wrapped - Your AI coding stats, meme-ified into a shareable video",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
"render": "tsx src/cli.ts",
|
|
26
26
|
"test": "vitest run",
|
|
27
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'",
|
|
28
32
|
"prepublishOnly": "npm run build"
|
|
29
33
|
},
|
|
30
34
|
"bin": {
|
|
@@ -52,11 +56,16 @@
|
|
|
52
56
|
"remotion": "^4.0.0"
|
|
53
57
|
},
|
|
54
58
|
"devDependencies": {
|
|
59
|
+
"@eslint/js": "^10.0.1",
|
|
55
60
|
"@types/node": "^22.0.0",
|
|
56
61
|
"@types/react": "^18.3.0",
|
|
57
62
|
"@vitest/coverage-v8": "^4.1.0",
|
|
63
|
+
"eslint": "^10.0.3",
|
|
64
|
+
"eslint-config-prettier": "^10.1.8",
|
|
65
|
+
"prettier": "^3.8.1",
|
|
58
66
|
"tsx": "^4.0.0",
|
|
59
67
|
"typescript": "^5.6.0",
|
|
68
|
+
"typescript-eslint": "^8.57.1",
|
|
60
69
|
"vitest": "^4.1.0"
|
|
61
70
|
}
|
|
62
71
|
}
|