usehour 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/README.md +4 -3
- package/hour.mjs +388 -75
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
Your year of building with AI, as Wrapped-style cards.
|
|
4
4
|
|
|
5
5
|
```
|
|
6
|
-
npx usehour
|
|
6
|
+
npx usehour@0.3.0
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
- Asks before it reads anything, and lists the folders it will read.
|
|
10
10
|
- Reads your Claude Code and Codex session logs on your machine, read only.
|
|
11
|
-
-
|
|
11
|
+
- Counts your habits, and finds a few of your best prompts (anything that looks like a key, password, email or home folder already hidden). You can pick some for your profile and black out words, but not add or change any.
|
|
12
12
|
- Opens your cards in your browser. Save any card as an image to post it.
|
|
13
|
-
-
|
|
13
|
+
- Optional: claim usehour.com/@you from the cards page. You see exactly what is sent first. The script itself never connects to the internet.
|
|
14
|
+
- No dependencies, nothing installed.
|
|
14
15
|
|
|
15
16
|
It's one file: read [hour.mjs](https://usehour.com/hour.mjs) before you run it.
|
|
16
17
|
|
package/hour.mjs
CHANGED
|
@@ -2,21 +2,24 @@
|
|
|
2
2
|
/*
|
|
3
3
|
Hour Wrapped: a year of building with AI, from the sessions already on your machine.
|
|
4
4
|
|
|
5
|
-
npx
|
|
5
|
+
npx usehour (or download this file and run: node hour.mjs)
|
|
6
6
|
|
|
7
7
|
What this script does, in order:
|
|
8
8
|
1. Tells you which folders it will read, and asks before reading anything.
|
|
9
9
|
2. Reads your Claude Code (~/.claude/projects) and Codex (~/.codex/sessions)
|
|
10
10
|
session logs on this machine. Read only. Nothing is changed or deleted.
|
|
11
|
-
3.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
3. Counts: sessions, hours, models, how you brief and steer the agent.
|
|
12
|
+
It also picks a few of your strongest prompts (with anything that looks
|
|
13
|
+
like a key, token, password, email or home path already hidden) so you
|
|
14
|
+
can choose to put them on your profile.
|
|
15
|
+
4. Writes ./hour-wrapped/index.html (your cards) and wrapped.json (the
|
|
16
|
+
counts only), then opens the cards in your browser.
|
|
17
|
+
5. This script never uses the network. The cards page can make one request,
|
|
18
|
+
to usehour.com, and only when you press "Create my private profile" after
|
|
19
|
+
seeing exactly what will be sent. The page is locked so it cannot contact
|
|
20
|
+
anything else.
|
|
18
21
|
|
|
19
|
-
No dependencies. No install step.
|
|
22
|
+
No dependencies. No install step. Run with --no-share for cards only.
|
|
20
23
|
Source: https://usehour.com/hour.mjs Questions: lush@usehour.com
|
|
21
24
|
*/
|
|
22
25
|
import fs from "node:fs";
|
|
@@ -27,8 +30,8 @@ import { spawn } from "node:child_process";
|
|
|
27
30
|
import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
|
|
28
31
|
import { fileURLToPath } from "node:url";
|
|
29
32
|
|
|
30
|
-
const VERSION = "0.
|
|
31
|
-
const
|
|
33
|
+
const VERSION = "0.3.0";
|
|
34
|
+
const API_DEFAULT = "https://usehour.com/api";
|
|
32
35
|
|
|
33
36
|
/* ------------------------------------------------------------------ patterns */
|
|
34
37
|
const TEST = /\b(pytest|vitest|jest|mocha|playwright|go test|cargo test|npm (run )?test|pnpm (run )?test|yarn test|bun test|node --test|npm run (check|lint|typecheck|build)|tsc\b|astro check|ruff|eslint|rspec|phpunit|dotnet test|mvn test|gradle test|--check\b)/i;
|
|
@@ -37,6 +40,43 @@ const CORRECT = /^(no\b|nope|not\b|wait\b|stop\b|don'?t\b|do not\b|actually\b|in
|
|
|
37
40
|
const GOAHEAD = /^(ok(ay)?|all right|alright|yes|yup|yep|sure|great|cool|nice|perfect|good|please proceed|proceed|go ahead|continue|do it|let'?s go)\b/i;
|
|
38
41
|
const QUESTION = /^(what|why|how|where|which|who|can you|could you|is it|are you|do you|does)\b/i;
|
|
39
42
|
const DONTKNOW = /\bi don'?t know\b/i;
|
|
43
|
+
/* How someone briefs an agent. Each is a yes/no per prompt. */
|
|
44
|
+
const CTX = /\b(because|so that|the goal|goal is|goal:|context|background|currently|right now|we'?re building|i'?m building|we have an?|the problem|the issue is|users?|customers?|the idea is)\b/i;
|
|
45
|
+
const LIM = /\b(don'?t|do not|must|never|only|without|avoid|keep (it|the|this)|no more than|at most|should ?n[o']t|make sure|constraints?)\b/i;
|
|
46
|
+
const FIN = /\b(done when|when (you'?re|you are) done|should (be able|return|show|work|pass|look|be)|expected|acceptance|success|until|end result|output should|deliverable|so i can|report back|let me know|the result)\b/i;
|
|
47
|
+
const SEQ = /\b(first|then|after that|next|finally|once (that|this|it)'?s? (is )?done)\b/gi;
|
|
48
|
+
const LISTLINE = /^\s*(\d+[.)]|[-*•]|step \d+)\s+\S/gim;
|
|
49
|
+
const SPECIFIC = /`[^`\n]+`|\b[\w./-]+\.(tsx?|jsx?|mjs|py|go|rs|rb|java|kt|swift|md|json|css|html|astro|vue|svelte|sql|ya?ml|toml|sh|env)\b|https?:\/\/\S+|\b[a-z]+[A-Z][A-Za-z0-9]+\b|\b[a-z0-9]+_[a-z0-9_]+\b|\bline \d+|\b\d+(px|ms|s|kb|mb|%)\b/;
|
|
50
|
+
const EXPLAIN = /\b(because|instead|should|expected|the (error|issue|problem|bug)|it (shows|says|returns|still)|still|wrong|broke|missing)\b/i;
|
|
51
|
+
/* Text copied from an agent's own output rather than written by you. */
|
|
52
|
+
const PASTED = /\b(Ran|Read|Edited|Wrote|Searched) \d+ (commands?|files?|lines?|patterns?)\b|[⏺⎿✻]|^\s*(Thinking|Thought for \d+)/m;
|
|
53
|
+
const CODEY = /[{};]\s*$|^\s{4,}\S|^\s*(at |import |from |const |let |def |function |class |return |\$ |>>> |File ")|^\s*[<{[]/;
|
|
54
|
+
|
|
55
|
+
/* Anything that looks secret or personal is replaced before a prompt leaves
|
|
56
|
+
the file parser. This runs on the few prompts kept as examples; the rest
|
|
57
|
+
are never kept at all. */
|
|
58
|
+
const SECRETS = [
|
|
59
|
+
/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(-----END [A-Z ]*PRIVATE KEY-----|$)/g,
|
|
60
|
+
/\b(sk|pk|rk)-(ant-|proj-|live_|test_)?[A-Za-z0-9_-]{16,}/g,
|
|
61
|
+
/\b(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}|\bgithub_pat_[A-Za-z0-9_]{20,}/g,
|
|
62
|
+
/\bxox[abposr]-[A-Za-z0-9-]{10,}/g,
|
|
63
|
+
/\bAKIA[0-9A-Z]{16}\b/g,
|
|
64
|
+
/\bAIza[0-9A-Za-z_-]{30,}/g,
|
|
65
|
+
/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g,
|
|
66
|
+
/\b(bearer|basic)\s+[A-Za-z0-9._~+\/=-]{12,}/gi,
|
|
67
|
+
/\b(api[_-]?key|secret|token|password|passwd|pwd|auth|credential|private[_-]?key|access[_-]?key)(\s*[:=]\s*|\s+is\s+)["']?[^\s"',;]{4,}/gi,
|
|
68
|
+
/\b[a-z][a-z0-9+.-]*:\/\/[^\s:@\/]+:[^\s@\/]+@/gi,
|
|
69
|
+
/\b[A-Fa-f0-9]{32,}\b/g,
|
|
70
|
+
/\b[A-Za-z0-9+\/_-]{40,}={0,2}/g,
|
|
71
|
+
/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g,
|
|
72
|
+
/\+?\b\d[\d ()-]{8,}\d\b/g,
|
|
73
|
+
/\b\d{1,3}(\.\d{1,3}){3}\b/g,
|
|
74
|
+
];
|
|
75
|
+
function redact(t) {
|
|
76
|
+
let out = t.replace(/(\/Users\/|\/home\/|[A-Z]:\\Users\\)[^\/\\\s]+/g, "~");
|
|
77
|
+
for (const re of SECRETS) out = out.replace(re, "[hidden]");
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
40
80
|
|
|
41
81
|
/* ------------------------------------------------------------ file parsing */
|
|
42
82
|
/* One file in, one small summary out. Prompt text stays inside this function:
|
|
@@ -51,14 +91,29 @@ function shortKey(t) {
|
|
|
51
91
|
}
|
|
52
92
|
function promptFacts(ts, text) {
|
|
53
93
|
const t = text.trim();
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
94
|
+
const words = t.split(/\s+/).filter(Boolean).length;
|
|
95
|
+
const lines = t.split("\n").filter((l) => l.trim());
|
|
96
|
+
const codey = lines.length >= 4 && lines.filter((l) => CODEY.test(l)).length / lines.length > 0.35;
|
|
97
|
+
const kind = CORRECT.test(t) ? "correct" : (GOAHEAD.test(t) && words <= 12) ? "go" : (t.endsWith("?") || QUESTION.test(t)) ? "question" : "direct";
|
|
98
|
+
const f = {
|
|
99
|
+
ts, words, kind, codey,
|
|
57
100
|
verify: VERIFY.test(t),
|
|
58
101
|
dontknow: DONTKNOW.test(t),
|
|
59
|
-
kind: CORRECT.test(t) ? "correct" : (GOAHEAD.test(t) && t.split(/\s+/).length <= 12) ? "go" : (t.endsWith("?") || QUESTION.test(t)) ? "question" : "direct",
|
|
60
102
|
short: shortKey(t),
|
|
103
|
+
ctx: CTX.test(t), lim: LIM.test(t), fin: FIN.test(t),
|
|
104
|
+
steps: (t.match(LISTLINE) || []).length >= 2 || (t.match(SEQ) || []).length >= 2,
|
|
105
|
+
spec: SPECIFIC.test(t),
|
|
106
|
+
explains: kind === "correct" && words >= 12 && EXPLAIN.test(t),
|
|
107
|
+
pasted: PASTED.test(t),
|
|
61
108
|
};
|
|
109
|
+
return f;
|
|
110
|
+
}
|
|
111
|
+
/* How good an example a prompt is: a clear brief with context, limits, a
|
|
112
|
+
finish line, steps, specifics and a request for proof. Long pastes of code
|
|
113
|
+
or logs are not examples of prompting, so they are skipped. */
|
|
114
|
+
function exampleScore(f, first) {
|
|
115
|
+
if (f.codey || f.pasted || f.words < 30 || f.words > 700) return 0;
|
|
116
|
+
return f.ctx + f.lim + f.fin + f.steps + f.verify + 0.5 * f.spec + Math.min(1, f.words / 120) + (first ? 0.5 : 0);
|
|
62
117
|
}
|
|
63
118
|
function codexUserText(p) {
|
|
64
119
|
const pick = (c) => typeof c === "string" ? c : Array.isArray(c) ? c.map((x) => x && (x.text || x.input_text) || "").filter((x) => typeof x === "string").join("\n") : "";
|
|
@@ -75,10 +130,11 @@ function cleanPrompt(t) {
|
|
|
75
130
|
if (!t || t.startsWith("<") || t.startsWith("# AGENTS.md") || t.startsWith("Caveat:") || t.startsWith("[Request interrupted") || t.startsWith("# Files mentioned")) return null;
|
|
76
131
|
return t;
|
|
77
132
|
}
|
|
133
|
+
let KEEP_EXAMPLES = true;
|
|
78
134
|
const KEEP_CODEX = /"(session_meta|turn_context|user_message|UserMessage|function_call|custom_tool_call|local_shell_call|token_count|turn_aborted)"|"role":\s*"user"/;
|
|
79
135
|
const KEEP_CLAUDE = /"type":\s*"(user|assistant)"/;
|
|
80
136
|
async function parseFile(file, src) {
|
|
81
|
-
const s = { src, sub: file.includes(`${path.sep}subagents${path.sep}`), start: null, end: null, cwd: null, models: {}, prompts: [], ids: [], patches: 0, cmds: 0, tests: 0, aborts: 0, tokens: 0 };
|
|
137
|
+
const s = { src, sub: file.includes(`${path.sep}subagents${path.sep}`), start: null, end: null, cwd: null, models: {}, prompts: [], ids: [], examples: [], patches: 0, cmds: 0, tests: 0, aborts: 0, tokens: 0 };
|
|
82
138
|
const seenMsg = new Set(); const seenText = new Set();
|
|
83
139
|
const rl = readline.createInterface({ input: fs.createReadStream(file, { encoding: "utf8" }), crlfDelay: Infinity });
|
|
84
140
|
for await (const line of rl) {
|
|
@@ -98,7 +154,7 @@ async function parseFile(file, src) {
|
|
|
98
154
|
if (text && text.trim().startsWith("[Request interrupted")) s.aborts++;
|
|
99
155
|
if (o.isMeta) continue;
|
|
100
156
|
const u = cleanPrompt(text);
|
|
101
|
-
if (u) {
|
|
157
|
+
if (u) { const f = promptFacts(ts, u); keepExample(s, f, u); s.prompts.push(f); s.ids.push(o.uuid || null); }
|
|
102
158
|
} else if (o.type === "assistant") {
|
|
103
159
|
if (m.id && !seenMsg.has(m.id)) {
|
|
104
160
|
seenMsg.add(m.id);
|
|
@@ -127,7 +183,7 @@ async function parseFile(file, src) {
|
|
|
127
183
|
const ut = codexUserText(p);
|
|
128
184
|
if (ut !== null && (o.type === "event_msg" || o.type === "response_item")) {
|
|
129
185
|
const u = cleanPrompt(ut);
|
|
130
|
-
if (u && !seenText.has(u)) { seenText.add(u);
|
|
186
|
+
if (u && !seenText.has(u)) { seenText.add(u); const f = promptFacts(ts, u); keepExample(s, f, u); s.prompts.push(f); s.ids.push(null); }
|
|
131
187
|
}
|
|
132
188
|
if (o.type === "response_item" && ["function_call", "custom_tool_call", "local_shell_call"].includes(p.type)) {
|
|
133
189
|
const name = p.name || "";
|
|
@@ -140,10 +196,21 @@ async function parseFile(file, src) {
|
|
|
140
196
|
}
|
|
141
197
|
return s;
|
|
142
198
|
}
|
|
199
|
+
/* Keeps the best three example prompts of a file, secrets hidden. */
|
|
200
|
+
function keepExample(s, f, text) {
|
|
201
|
+
if (!KEEP_EXAMPLES) return;
|
|
202
|
+
const q = exampleScore(f, s.prompts.length === 0);
|
|
203
|
+
if (q < 3) return;
|
|
204
|
+
if (s.examples.length >= 3 && q <= s.examples[2].q) return;
|
|
205
|
+
const clean = redact(text).slice(0, 2400);
|
|
206
|
+
s.examples.push({ q: clean.includes("[hidden]") ? q - 0.5 : q, text: clean, ts: f.ts, words: f.words });
|
|
207
|
+
s.examples.sort((a, b) => b.q - a.q); s.examples.length = Math.min(3, s.examples.length);
|
|
208
|
+
}
|
|
143
209
|
|
|
144
210
|
/* Worker threads parse files in parallel; each returns only its summary. */
|
|
145
211
|
if (!isMainThread) {
|
|
146
|
-
parentPort.on("message", async ({ file, src }) => {
|
|
212
|
+
parentPort.on("message", async ({ file, src, examples }) => {
|
|
213
|
+
KEEP_EXAMPLES = examples !== false;
|
|
147
214
|
try { parentPort.postMessage({ ok: true, s: await parseFile(file, src) }); }
|
|
148
215
|
catch (e) { parentPort.postMessage({ ok: false, error: String(e && e.message || e) }); }
|
|
149
216
|
});
|
|
@@ -206,6 +273,20 @@ function compute(summaries) {
|
|
|
206
273
|
const goto = Object.entries(shorts).filter(([, c]) => c >= 5).sort((a, b) => b[1] - a[1])[0] || null;
|
|
207
274
|
const days = new Set(P.map((p) => p.d.toDateString())).size;
|
|
208
275
|
const projects = new Set(main.map((s) => s.cwd).filter(Boolean)).size;
|
|
276
|
+
/* How you prompt: five habits, each the share of prompts where it shows.
|
|
277
|
+
"notable" is the level where the habit stands out; the bar fills there. */
|
|
278
|
+
const long = P.filter((p) => p.words >= 25 && !p.codey), mid = P.filter((p) => p.words >= 8 && !p.codey);
|
|
279
|
+
const briefs = P.filter((p) => p.i === 0 && !p.codey);
|
|
280
|
+
const corrections = follows.filter((p) => p.kind === "correct");
|
|
281
|
+
const rate = (a, f) => (a.length ? a.filter(f).length / a.length : 0);
|
|
282
|
+
const habit = (key, label, value, notable, base, what) => ({ key, label, value, notable, base, what });
|
|
283
|
+
const promptProfile = [
|
|
284
|
+
habit("brief", "Briefing", rate(briefs, (p) => p.words >= 30 && p.ctx + p.lim + p.fin >= 2), 0.4, briefs.length, "opening prompts that give context, limits or a finish line (at least two)"),
|
|
285
|
+
habit("steps", "Breaking it down", rate(long, (p) => p.steps), 0.25, long.length, "longer prompts laid out as steps"),
|
|
286
|
+
habit("proof", "Asking for proof", main.length ? verifySessions / main.length : 0, 0.45, main.length, "sessions that asked the agent to test, verify or screenshot"),
|
|
287
|
+
habit("specific", "Being specific", rate(mid, (p) => p.spec), 0.35, mid.length, "prompts that name files, functions, errors or numbers"),
|
|
288
|
+
habit("recover", "Correcting clearly", rate(corrections, (p) => p.explains), 0.5, corrections.length, "corrections that say what is wrong or what to do instead"),
|
|
289
|
+
].filter((h) => h.base >= 10);
|
|
209
290
|
const first = new Date(Math.min(...P.map((p) => p.d.getTime()))), last = new Date(Math.max(...P.map((p) => p.d.getTime())));
|
|
210
291
|
return {
|
|
211
292
|
version: VERSION,
|
|
@@ -223,8 +304,25 @@ function compute(summaries) {
|
|
|
223
304
|
goToPrompt: goto ? { text: goto[0], count: goto[1] } : null,
|
|
224
305
|
dontKnow: P.filter((p) => p.dontknow).length,
|
|
225
306
|
tokens: summaries.reduce((a, s) => a + s.tokens, 0),
|
|
307
|
+
promptProfile,
|
|
226
308
|
};
|
|
227
309
|
}
|
|
310
|
+
/* The strongest example prompts across all sessions: no repeats, at most two
|
|
311
|
+
from any one project, best first. They stay on this computer unless you
|
|
312
|
+
choose some for your profile. */
|
|
313
|
+
function pickExamples(summaries, n = 12) {
|
|
314
|
+
const all = summaries.filter((s) => !s.sub).flatMap((s) => (s.examples || []).map((e) => ({ ...e, cwd: s.cwd || "" })));
|
|
315
|
+
all.sort((a, b) => b.q - a.q);
|
|
316
|
+
const seen = new Set(), per = {}, out = [];
|
|
317
|
+
for (const e of all) {
|
|
318
|
+
const k = e.text.toLowerCase().replace(/\s+/g, " ").slice(0, 200);
|
|
319
|
+
if (seen.has(k) || (per[e.cwd] || 0) >= 2) continue;
|
|
320
|
+
seen.add(k); per[e.cwd] = (per[e.cwd] || 0) + 1;
|
|
321
|
+
out.push({ text: e.text, words: e.words, date: (e.ts || "").slice(0, 10) });
|
|
322
|
+
if (out.length >= n) break;
|
|
323
|
+
}
|
|
324
|
+
return out;
|
|
325
|
+
}
|
|
228
326
|
|
|
229
327
|
/* Builder types. Each signal is scored against the level where it becomes
|
|
230
328
|
notable; the highest score wins, and below 1 everywhere you are a Pair
|
|
@@ -253,6 +351,9 @@ function builderType(m) {
|
|
|
253
351
|
const monthYear = (d) => new Date(d + "T12:00:00Z").toLocaleString("en", { month: "short", year: "numeric" });
|
|
254
352
|
const hourLabel = (h) => (h % 12 || 12) + (h < 12 ? " AM" : " PM");
|
|
255
353
|
const compact = (n) => n >= 1e9 ? (n / 1e9).toFixed(1).replace(/\.0$/, "") + "B" : n >= 1e6 ? (n / 1e6).toFixed(1).replace(/\.0$/, "") + "M" : n >= 1e3 ? Math.round(n / 1e3) + "K" : String(n);
|
|
354
|
+
/* Dots for a habit: reaching the notable level fills 7 of 10, the last 3 are
|
|
355
|
+
for going beyond it. */
|
|
356
|
+
const habitFill = (h) => Math.min(1, 0.7 * Math.min(1, h.value / h.notable) + 0.3 * Math.max(0, (h.value - h.notable) / (1 - h.notable)));
|
|
256
357
|
function cards(m, name) {
|
|
257
358
|
const type = TYPES[builderType(m)];
|
|
258
359
|
const tools = [m.sessions.claudeCode && "Claude Code", m.sessions.codex && "Codex"].filter(Boolean).join(" and ");
|
|
@@ -273,31 +374,221 @@ function cards(m, name) {
|
|
|
273
374
|
if (m.dontKnow >= 10) C.push({ id: "honest", bg: "#FB65C2", fg: "#0B0B0A", dot: "#0B0B0A", pat: "rain", ask: "Your most honest phrase", big: "“I don't know”", line: `${m.dontKnow} times. You think out loud, with the agent.`, bs: "14cqw" });
|
|
274
375
|
if (m.tokens > 0) C.push({ id: "tokens", bg: "#2B5FE3", fg: "#FFFFFF", dot: "#FFFFFF", pat: "field", ask: "How much you built", big: compact(m.tokens), line: "tokens processed.", bs: "30cqw" });
|
|
275
376
|
if (m.proof.codingSessions >= 5) C.push({ id: "edge", bg: "#F7F3EC", fg: "#0B0B0A", dot: "#F2601F", pat: "ring", pct: tested, ask: tested < 0.75 ? "Your growth edge" : "Your strength", big: `${pct(tested)}%`, line: tested < 0.75 ? "of coding sessions ran a test. Push that higher." : "of coding sessions ran a test. Keep it that way.", bs: "30cqw" });
|
|
377
|
+
if (m.promptProfile && m.promptProfile.length >= 3) {
|
|
378
|
+
const hp = m.promptProfile, best = [...hp].sort((a, b) => b.value / b.notable - a.value / a.notable)[0];
|
|
379
|
+
C.push({ id: "prompting", bg: "#F7F3EC", fg: "#0B0B0A", bs: "11cqw", rows: hp.map((h) => [h.label, habitFill(h), `${pct(h.value)}%`]), ask: "How you prompt", big: best.label, line: `Your strongest habit: ${pct(best.value)}% of ${best.what}.` });
|
|
380
|
+
}
|
|
276
381
|
C.push({ id: "summary", bg: "#0B0B0A", fg: "#F4F1EA", summary: true, who: name || "", period: `${monthYear(m.window.first)} – ${monthYear(m.window.last)}`, type: type.name,
|
|
277
382
|
stats: [["Sessions", m.sessions.total.toLocaleString("en")], ["Busiest hour", hourLabel(m.busiestHour)], ["Go-to prompt", m.goToPrompt ? `“${m.goToPrompt.text}”` : "–"], ["Agents at once", String(m.parallelMax)], ["Made it prove it", `${pct(verify)}%`], ["Model", top ? top.name.replace(/^Claude /, "") : "–"]] });
|
|
278
383
|
return C;
|
|
279
384
|
}
|
|
280
385
|
|
|
386
|
+
/* ------------------------------------------------------- the claim flow
|
|
387
|
+
Shown only when sharing is on. It runs in the cards page on your computer.
|
|
388
|
+
The page talks to usehour.com twice at most, each time because you pressed
|
|
389
|
+
a button: "Continue" checks that your handle is free (sends the handle
|
|
390
|
+
only), "Create" sends the profile you saw in step 3. */
|
|
391
|
+
const CLAIM_CSS = String.raw`
|
|
392
|
+
|
|
393
|
+
.claimc .cdots{margin-top:4cqw;height:14cqw;background-image:radial-gradient(circle,rgba(244,241,234,.95) 0 .95cqw,transparent 1.05cqw);background-size:3.6cqw 3.6cqw;-webkit-mask-image:linear-gradient(115deg,transparent 8%,#000 55%,transparent 96%);mask-image:linear-gradient(115deg,transparent 8%,#000 55%,transparent 96%)}
|
|
394
|
+
.claimc .pill{margin-top:4cqw;align-self:flex-start;padding:2cqw 3.6cqw;border-radius:999px;background:rgba(244,241,234,.1);box-shadow:inset 0 0 0 .3cqw rgba(244,241,234,.22);font:4.2cqw var(--mono)}.claimc .pill span{opacity:.6}.claimc .pill b{font-weight:500}
|
|
395
|
+
.claimc .cta{position:relative;z-index:5;margin-top:5cqw;height:13cqw;border-radius:999px;border:0;background:#F4F1EA;color:#0B0B0A;font:600 4.6cqw var(--sans);cursor:pointer}
|
|
396
|
+
.claimbar{margin-top:28px;display:flex;justify-content:space-between;align-items:center;gap:16px;flex-wrap:wrap;background:var(--ink);color:#F4F1EA;border-radius:22px;padding:22px 24px}.claimbar b{display:block;font-size:20px;letter-spacing:-.02em}.claimbar span{font-size:14px;opacity:.8}
|
|
397
|
+
.claimbar button{height:48px;padding:0 22px;border-radius:999px;border:0;background:#F4F1EA;color:var(--ink);font:600 15px var(--sans);cursor:pointer}
|
|
398
|
+
.cl{position:fixed;inset:0;z-index:50;background:rgba(11,11,10,.55);display:flex;align-items:center;justify-content:center;padding:16px}.cl[hidden]{display:none}
|
|
399
|
+
.cl-sheet{width:100%;max-width:420px;height:min(860px,calc(100svh - 32px));background:var(--paper);border-radius:28px;display:flex;flex-direction:column;padding:18px 24px 22px;overflow:auto}
|
|
400
|
+
@media(max-width:520px){.cl{padding:0}.cl-sheet{max-width:none;height:100svh;border-radius:0}}
|
|
401
|
+
.cl-top{flex:none;display:flex;justify-content:space-between;align-items:center;font-size:14px;color:var(--ink-3)}.cl-back{background:none;border:0;font:14px var(--sans);color:#3A3935;padding:10px 0;cursor:pointer;min-height:44px}
|
|
402
|
+
.cl-prog{flex:none;display:flex;gap:6px;margin-top:4px}.cl-prog i{flex:1;height:4px;border-radius:2px;background:rgba(11,11,10,.12)}.cl-prog i.on{background:var(--ink)}
|
|
403
|
+
.cl-h{flex:none;margin:24px 0 0;font-size:34px;line-height:1.02;letter-spacing:-.045em;font-weight:650}
|
|
404
|
+
.cl-p{flex:none;margin:10px 0 0;font-size:15px;line-height:1.45;color:#3A3935}
|
|
405
|
+
.cl-hcard{flex:none;margin-top:22px;background:var(--ink);color:#F4F1EA;border-radius:22px;padding:20px;position:relative;overflow:hidden}
|
|
406
|
+
.cl-hcard:before{content:"";position:absolute;right:-10px;top:-10px;width:170px;height:120px;background-image:radial-gradient(circle,rgba(244,241,234,.9) 0 2.6px,transparent 3px);background-size:12px 12px;-webkit-mask-image:radial-gradient(circle at 80% 20%,#000 10%,transparent 70%);mask-image:radial-gradient(circle at 80% 20%,#000 10%,transparent 70%)}
|
|
407
|
+
.cl-hcard label{position:relative;font-size:13px;opacity:.7}
|
|
408
|
+
.cl-hrow{position:relative;display:flex;align-items:baseline;margin-top:4px}.cl-hrow span{font-size:40px;font-weight:650;letter-spacing:-.05em}
|
|
409
|
+
.cl-hrow input{flex:1;min-width:0;border:0;outline:none;background:transparent;color:#F4F1EA;font:650 40px var(--sans);letter-spacing:-.05em;padding:0}
|
|
410
|
+
.cl-hst{position:relative;margin-top:8px;font-size:14px;min-height:20px;opacity:.85}
|
|
411
|
+
.cl-f{flex:none;display:flex;flex-direction:column;gap:6px;margin-top:14px;font-size:14px;font-weight:500}
|
|
412
|
+
.cl-f input{height:48px;padding:0 14px;border-radius:12px;border:1px solid rgba(11,11,10,.14);background:#fff;font:16px var(--sans);color:var(--ink)}
|
|
413
|
+
.cl-fine{flex:none;margin:10px 0 0;font-size:13px;line-height:1.45;color:var(--ink-3)}.cl-fine.c{text-align:center}
|
|
414
|
+
.cl-err{flex:none;margin-top:12px;font-size:14px;color:#B3261E;font-weight:500}.cl-err:empty{display:none}
|
|
415
|
+
.cl-sp{flex:1 0 16px}
|
|
416
|
+
.cl-go{flex:none;height:56px;border-radius:999px;border:0;background:var(--ink);color:var(--paper);font:600 17px var(--sans);cursor:pointer;width:100%}.cl-go[disabled]{opacity:.5;cursor:wait}
|
|
417
|
+
.cl-skip{flex:none;display:block;width:100%;height:44px;background:none;border:0;font:14px var(--sans);color:var(--ink-3);cursor:pointer}
|
|
418
|
+
.cl-lw{position:relative;margin-top:16px;flex:1 1 auto;min-height:180px;display:flex;flex-direction:column}
|
|
419
|
+
.cl-list{flex:1;overflow:auto;display:flex;flex-direction:column;gap:12px;padding:3px 3px 48px}
|
|
420
|
+
.cl-pc{flex:none;border-radius:20px;padding:16px;background:#fff;color:var(--ink);box-shadow:inset 0 0 0 1px rgba(11,11,10,.12);cursor:pointer}
|
|
421
|
+
.cl-pc.on{box-shadow:0 0 0 3px var(--ink)}
|
|
422
|
+
.cl-pch{display:flex;justify-content:space-between;align-items:center;gap:8px;font-size:12px;font-weight:500}.cl-pch span:first-child{opacity:.8}
|
|
423
|
+
.cl-pick{flex:none;border-radius:999px;padding:5px 11px;box-shadow:inset 0 0 0 1px rgba(11,11,10,.22)}.cl-pc.on .cl-pick{background:var(--ink);color:#fff;box-shadow:none}
|
|
424
|
+
.cl-pc p{margin:10px 0 0;font-size:16px;line-height:1.32;letter-spacing:-.015em;font-weight:500;display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden;white-space:pre-line}
|
|
425
|
+
.cl-pc textarea{display:block;width:100%;margin-top:10px;min-height:180px;border:0;border-radius:12px;padding:10px 12px;font:15px/1.45 var(--sans);background:rgba(255,255,255,.94);color:var(--ink);resize:vertical}
|
|
426
|
+
.cl-edit{margin-top:6px;background:none;border:0;padding:8px 0;font:600 13px var(--sans);color:inherit;text-decoration:underline;cursor:pointer}
|
|
427
|
+
.hid{background:var(--ink);color:var(--ink);border-radius:5px;padding:0 4px}
|
|
428
|
+
.cl-more{position:absolute;left:0;right:0;bottom:0;height:90px;background:linear-gradient(180deg,rgba(247,243,236,0),var(--paper) 72%);display:flex;align-items:flex-end;justify-content:center;pointer-events:none;padding-bottom:4px}.cl-more[hidden]{display:none}
|
|
429
|
+
.cl-more span{display:inline-flex;align-items:center;height:32px;padding:0 14px;border-radius:999px;background:#fff;box-shadow:0 4px 14px rgba(11,11,10,.08),inset 0 0 0 1px rgba(11,11,10,.1);font-size:13px;font-weight:500;color:#3A3935}
|
|
430
|
+
.cl-opt{flex:none;display:block;border-radius:20px;padding:18px;margin-top:12px;cursor:pointer;background:#fff;box-shadow:inset 0 0 0 1px rgba(11,11,10,.1)}.cl-opt.dk{background:var(--ink);color:#F4F1EA;box-shadow:none}
|
|
431
|
+
.cl-oh{display:flex;justify-content:space-between;align-items:center;gap:12px;font-size:18px;font-weight:650;letter-spacing:-.02em}
|
|
432
|
+
.cl-oh input{width:24px;height:24px;flex:none;accent-color:var(--ink)}.cl-opt.dk .cl-oh input{accent-color:#B0ACFE}
|
|
433
|
+
.cl-ot{display:block;margin-top:8px;font-size:15px;line-height:1.4;color:#3A3935}.cl-opt.dk .cl-ot{color:#F4F1EA;opacity:.9}.cl-opt.dk .cl-ot.s{font-size:13px;opacity:.65}
|
|
434
|
+
.cl-ul{margin:10px 0 0;padding:0;list-style:none;display:flex;flex-direction:column;gap:6px;font-size:15px;line-height:1.4;color:#3A3935}.cl-ul li{display:flex;gap:8px}.cl-ul li:before{content:"";flex:none;width:6px;height:6px;border-radius:50%;background:var(--dot);margin-top:8px}
|
|
435
|
+
.cl-h3{flex:none;margin:26px 0 10px;font-size:18px;letter-spacing:-.02em}
|
|
436
|
+
.cl-two{flex:none;display:grid;grid-template-columns:1fr 1fr;gap:10px}.cl-two div{background:#fff;border-radius:16px;padding:14px;box-shadow:inset 0 0 0 1px rgba(11,11,10,.1);font-size:14px;line-height:1.35}.cl-two b{display:flex;align-items:center;gap:7px;font-size:13px;margin-bottom:4px}.cl-two b i{width:9px;height:9px;border-radius:50%}.cl-two span{display:block;margin-top:6px}
|
|
437
|
+
.cl-link{flex:none;align-self:flex-start;background:none;border:0;padding:10px 0;font:13px var(--sans);color:var(--ink-3);text-decoration:underline;cursor:pointer}
|
|
438
|
+
.cl pre{flex:none;background:var(--ink);color:#F4F1EA;border-radius:14px;padding:14px;font:12px/1.5 var(--mono);white-space:pre-wrap;word-break:break-word;max-height:260px;overflow:auto;margin:0 0 12px}
|
|
439
|
+
.cl-done{flex:none;margin-top:28px;background:var(--ink);color:#F4F1EA;border-radius:24px;padding:28px 24px;position:relative;overflow:hidden}
|
|
440
|
+
.cl-done:before{content:"";position:absolute;inset:0 0 auto auto;width:100%;height:120px;background-image:radial-gradient(circle,rgba(244,241,234,.9) 0 2.6px,transparent 3px);background-size:12px 12px;-webkit-mask-image:linear-gradient(200deg,#000,transparent 70%);mask-image:linear-gradient(200deg,#000,transparent 70%)}
|
|
441
|
+
.cl-done h2{position:relative;margin:90px 0 0;font-size:44px;line-height:.95;letter-spacing:-.05em;font-weight:650}
|
|
442
|
+
.cl-done a.u{position:relative;display:block;margin-top:16px;font:15px var(--mono);color:#F4F1EA;word-break:break-all}
|
|
443
|
+
.cl-done p{position:relative;margin:14px 0 0;font-size:14px;line-height:1.45;opacity:.8}
|
|
444
|
+
.cl-row{flex:none;display:flex;gap:10px;margin-top:16px}.cl-row a,.cl-row button{flex:1;display:flex;align-items:center;justify-content:center;height:52px;border-radius:999px;font:600 16px var(--sans);text-decoration:none;cursor:pointer;border:0}
|
|
445
|
+
.cl-row a{background:var(--ink);color:var(--paper)}.cl-row button{background:#fff;color:var(--ink);box-shadow:inset 0 0 0 1px rgba(11,11,10,.14)}
|
|
446
|
+
.cl-g2{margin-top:22px}.cl-g2>*+*{margin-top:14px}
|
|
447
|
+
.cl-foot{flex:none;display:flex;flex-direction:column-reverse;gap:8px;margin-top:14px}.cl-foot .cl-fine{margin:0;text-align:center}
|
|
448
|
+
.cl-tip{margin-top:10px;font-size:12.5px;font-weight:600;opacity:.8}
|
|
449
|
+
.cl-toks{margin-top:8px;font-size:15.5px;line-height:1.6;max-height:300px;overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;background:rgba(255,255,255,.95);color:var(--ink);border-radius:12px;padding:10px 12px;cursor:auto}
|
|
450
|
+
.tok{font:inherit;color:inherit;background:none;border:0;padding:0 1px;margin:0;border-radius:4px;cursor:pointer}.tok:hover{background:rgba(11,11,10,.1)}.tok.on{background:var(--ink);color:var(--ink)}
|
|
451
|
+
.cl-pc.ed{cursor:default}.cl-narrow{width:100%;max-width:560px;margin:0 auto}
|
|
452
|
+
@media(min-width:900px){.cl-sheet{max-width:1040px;height:min(820px,calc(100svh - 48px));padding:26px 40px 30px}.cl-h{font-size:44px}
|
|
453
|
+
.cl-g2{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:28px;align-items:start}.cl-g2>*+*{margin-top:0}.cl-g2>.cl-hcard{margin-top:0}
|
|
454
|
+
.cl-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));align-content:start;align-items:start}
|
|
455
|
+
.cl-foot{flex-direction:row;justify-content:space-between;align-items:center;gap:20px}.cl-foot .cl-fine{text-align:left}.cl-foot .cl-go{width:auto;min-width:300px;padding:0 34px}.cl-foot .cl-skip{width:auto}}
|
|
456
|
+
`;
|
|
457
|
+
const CLAIM_HTML = `<div class="cl" id="cl" hidden role="dialog" aria-modal="true" aria-label="Claim your handle"><div class="cl-sheet" id="cls"></div></div>`;
|
|
458
|
+
const CLAIM_JS = String.raw`
|
|
459
|
+
var CS={hide:(D.examples||[]).map(function(){return new Set()}),step:1,handle:D.suggest||"",name:"",email:"",picked:[],texts:(D.examples||[]).map(function(e){return e.text}),editing:-1,pub:false,roles:false,feature:false,busy:false,err:"",data:false,done:null};
|
|
460
|
+
var HANDLE_RE=/^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){2,23}$/,EMAIL_RE=/^[^\s@<>"]+@[^\s@<>"]+\.[^\s@<>"]+$/;
|
|
461
|
+
var PCOL=[["#DBA004","#0B0B0A"],["#8B5FD9","#FFFFFF"],["#2B5FE3","#FFFFFF"]];
|
|
462
|
+
function hidHTML(t){return esc(t).replace(/\[hidden\]/g,'<span class="hid">hidden</span>')}
|
|
463
|
+
function claimEl(){var d=document.createElement("article");d.className="card claimc";d.style.setProperty("--bg","#0B0B0A");d.style.setProperty("--fg","#F4F1EA");d.style.setProperty("--bs","15cqw");
|
|
464
|
+
var h=CS.handle||"you";d.innerHTML='<div class="in"><div class="ask">One more thing</div><div class="cdots"></div><div class="big">Claim your handle.</div><div class="pill"><span>usehour.com/</span><b class="grad">@'+esc(h)+'</b></div><div class="line">These cards become your profile. Handles are first come, first served.</div><button type="button" class="cta">Claim @'+esc(h)+'</button><div class="ft"><span class="b"><svg viewBox="0 0 64 64"><use href="#mark"/></svg>Hour</span><span class="u">usehour.com</span></div></div>';
|
|
465
|
+
d.querySelector(".cta").onclick=function(e){e.stopPropagation();openClaim()};return d}
|
|
466
|
+
function openClaim(){document.getElementById("cl").hidden=false;document.body.style.overflow="hidden";renderClaim()}
|
|
467
|
+
function closeClaim(){document.getElementById("cl").hidden=true;document.body.style.overflow=""}
|
|
468
|
+
function payload(){return{v:2,handle:CS.handle,name:CS.name.trim(),email:CS.email.trim(),linkedin:"",public:CS.pub,roles:CS.roles,feature:CS.feature,prompts:CS.picked.map(function(i){return{text:promptOut(i),date:D.examples[i].date,hidden:CS.hide[i].size}}).filter(function(p){return p.text&&p.text!=="[hidden]"}),wrapped:D.m}}
|
|
469
|
+
function prog(n){return'<div class="cl-top"><button type="button" class="cl-back" data-a="back">'+(n===1?"Close":"Back")+'</button><span>'+n+' of 3</span></div><div class="cl-prog">'+[1,2,3].map(function(i){return'<i'+(i<=n?' class="on"':'')+'></i>'}).join("")+'</div>'}
|
|
470
|
+
function handleNote(){var h=CS.handle;if(!h)return"Letters, numbers and dashes.";if(!HANDLE_RE.test(h))return"3 to 24 letters, numbers or single dashes.";return"Press Continue to check it's free."}
|
|
471
|
+
function toks(i){return CS.texts[i].split(/(\s+)/)}
|
|
472
|
+
function isAuto(w){return w.indexOf("[hidden]")>=0}
|
|
473
|
+
function promptOut(i){var h=CS.hide[i];return toks(i).map(function(w,k){return h.has(k)?"[hidden]":w}).join("").replace(/\[hidden\](\s+\[hidden\])+/g,"[hidden]").trim()}
|
|
474
|
+
function hidCount(i){return (CS.texts[i].match(/\[hidden\]/g)||[]).length+CS.hide[i].size}
|
|
475
|
+
function viewHTML(i){var h=CS.hide[i];return toks(i).map(function(w,k){if(/^\s+$/.test(w))return w;if(h.has(k))return'<span class="hid">'+esc(w)+'</span>';return hidHTML(w)}).join("")}
|
|
476
|
+
function editHTML(i){var h=CS.hide[i];return toks(i).map(function(w,k){if(/^\s+$/.test(w))return w;if(isAuto(w))return hidHTML(w);return'<button type="button" class="tok'+(h.has(k)?" on":"")+'" data-a="tok" data-i="'+i+'" data-k="'+k+'">'+esc(w)+'</button>'}).join("")}
|
|
477
|
+
function renderClaim(){var s=document.getElementById("cls"),H="",n=CS.step;
|
|
478
|
+
if(n===1){H=prog(1)+'<h2 class="cl-h">Pick your handle.</h2><p class="cl-p">It’s the address of your profile. Once it’s yours, nobody else can have it.</p>'
|
|
479
|
+
+'<div class="cl-g2"><div class="cl-hcard"><label for="clh">usehour.com/</label><div class="cl-hrow"><span class="grad">@</span><input id="clh" autocomplete="off" autocapitalize="off" spellcheck="false" maxlength="24" value="'+esc(CS.handle)+'"></div><div class="cl-hst" id="clhst">'+esc(handleNote())+'</div></div>'
|
|
480
|
+
+'<div><label class="cl-f" style="margin-top:0">Your name<input id="cln" autocomplete="name" maxlength="80" value="'+esc(CS.name)+'"></label>'
|
|
481
|
+
+'<label class="cl-f">Email, to keep your handle<input id="cle" type="email" autocomplete="email" maxlength="200" value="'+esc(CS.email)+'"></label>'
|
|
482
|
+
+'<p class="cl-fine">We email you a link to your profile. That’s how you get back in. No password.</p></div></div>'
|
|
483
|
+
+'<div class="cl-err">'+esc(CS.err)+'</div><div class="cl-sp"></div>'
|
|
484
|
+
+'<div class="cl-foot"><p class="cl-fine">Continue checks the name with usehour.com. Nothing else is sent.</p><button type="button" class="cl-go" data-a="next1"'+(CS.busy?" disabled":"")+'>'+(CS.busy?"Checking…":"Continue")+'</button></div>'}
|
|
485
|
+
else if(n===2){var k=CS.picked.length;H=prog(2)+'<h2 class="cl-h">Show how you brief an agent.</h2><p class="cl-p">Your strongest prompts, found in your logs. Pick up to 3. You can black out words, but not add or change any.</p>'
|
|
486
|
+
+'<div class="cl-lw"><div class="cl-list" id="cll">'+D.examples.map(function(e,i){var at=CS.picked.indexOf(i),on=at>=0,c=on?PCOL[at%3]:null,ed=CS.editing===i,w=CS.texts[i].split(/\s+/).filter(Boolean).length,hn=hidCount(i);
|
|
487
|
+
return'<div class="cl-pc'+(on?" on":"")+(ed?" ed":"")+'" data-a="pick" data-i="'+i+'"'+(c?' style="background:'+c[0]+';color:'+c[1]+'"':'')+'><div class="cl-pch"><span>'+esc(e.date?new Date(e.date+"T12:00:00Z").toLocaleString("en",{month:"short",day:"numeric",timeZone:"UTC"})+" · ":"")+w+' words'+(hn?" · "+hn+" hidden":"")+'</span><span class="cl-pick">'+(on?"✓ Picked":"Pick")+'</span></div>'
|
|
488
|
+
+(ed?'<div class="cl-tip">Tap words to black them out. Tap again to bring them back.</div><div class="cl-toks">'+editHTML(i)+'</div>':'<p>“'+viewHTML(i)+'”</p>')
|
|
489
|
+
+'<button type="button" class="cl-edit" data-a="edit" data-i="'+i+'">'+(ed?"Done":"Black out words")+'</button></div>'}).join("")+'</div><div class="cl-more" id="clm" hidden><span>'+(D.examples.length>4?"More prompts below":"More below")+'</span></div></div>'
|
|
490
|
+
+'<div class="cl-err">'+esc(CS.err)+'</div><div class="cl-foot" style="margin-top:14px">'+(k?'<button type="button" class="cl-skip" data-a="skip2">Skip, don’t share any prompts</button>':'<span></span>')+'<button type="button" class="cl-go" data-a="next2">'+(k?"Continue with "+k+" prompt"+(k>1?"s":""):"Continue without prompts")+'</button></div>'}
|
|
491
|
+
else if(n===3){var p=payload(),hh=esc(CS.handle);H=prog(3)+'<h2 class="cl-h">Make it work for you.</h2><p class="cl-p">All optional. Change any of it later from your profile.</p>'
|
|
492
|
+
+'<div class="cl-g2"><div>'
|
|
493
|
+
+'<label class="cl-opt dk" style="margin-top:0"><span class="cl-oh"><span>Make <span class="grad">@'+hh+'</span> public</span><input type="checkbox" data-o="pub"'+(CS.pub?" checked":"")+'></span><span class="cl-ot">A link for your LinkedIn, GitHub and email signature. It shows how you work, which a CV can’t.</span><span class="cl-ot s">Off: your handle stays reserved, and only people with your private link can see the profile.</span></label>'
|
|
494
|
+
+'<label class="cl-opt"><span class="cl-oh"><span>Let companies come to you</span><input type="checkbox" data-o="roles"'+(CS.roles?" checked":"")+'></span><ul class="cl-ul" style="--dot:#2B5FE3"><li>Teams hiring AI builders see your profile and ask for an intro. No applying, no cover letters.</li><li>You say yes or no to each one. Your email stays hidden until you say yes.</li></ul></label>'
|
|
495
|
+
+'<label class="cl-opt"><span class="cl-oh"><span>Get featured by Hour</span><input type="checkbox" data-o="feature"'+(CS.feature?" checked":"")+'></span><ul class="cl-ul" style="--dot:#F2601F"><li>We post standout cards in our “builders to watch” series, tagged to you.</li><li>Your work in front of founders and hiring teams who follow Hour.</li></ul></label>'
|
|
496
|
+
+'</div><div><h3 class="cl-h3" style="margin-top:0">What goes to Hour</h3><div class="cl-two"><div><b><i style="background:#2B5FE3"></i>Sent</b><span>Your card numbers</span><span>'+(p.prompts.length?p.prompts.length+" prompt"+(p.prompts.length>1?"s":"")+" you picked":"No prompts")+'</span><span>Handle, name, email</span></div><div><b><i style="background:#F2601F"></i>Stays with you</b><span>Your logs and code</span><span>Every other prompt</span><span>File and project names</span></div></div>'
|
|
497
|
+
+'<button type="button" class="cl-link" data-a="data">'+(CS.data?"Hide the exact data":"See the exact data")+'</button>'+(CS.data?'<pre>'+esc(JSON.stringify(p,null,2))+'</pre>':'')+'</div></div>'
|
|
498
|
+
+'<div class="cl-err">'+esc(CS.err)+'</div><div class="cl-sp"></div><div class="cl-foot"><p class="cl-fine">Delete it any time, with one link.</p><button type="button" class="cl-go" data-a="create"'+(CS.busy?" disabled":"")+'>'+(CS.busy?"Creating…":"Create @"+hh)+'</button></div>'}
|
|
499
|
+
else{var d=CS.done,u=d.publicUrl||d.url;H='<div class="cl-top"><span></span><button type="button" class="cl-back" data-a="close">Close</button></div><div class="cl-narrow"><div class="cl-done"><h2>@'+esc(d.handle)+'<br>is yours.</h2><a class="u" href="'+esc(u)+'" target="_blank" rel="noopener">'+esc(u.replace("https://",""))+'</a><p>'+(d.publicUrl?"Your profile is public. Put the link anywhere you like.":"Your profile is private. Only people you send this link to can see it.")+'</p></div>'
|
|
500
|
+
+'<div class="cl-row"><a href="'+esc(u)+'" target="_blank" rel="noopener">Open my profile</a><button type="button" data-a="copy">Copy link</button></div><p class="cl-fine c">We emailed you this link, plus a private link to change your settings or delete your profile.</p></div>'}
|
|
501
|
+
s.innerHTML=H;s.scrollTop=0;
|
|
502
|
+
if(n===2){var L=document.getElementById("cll"),M=document.getElementById("clm"),f=function(){M.hidden=L.scrollHeight-L.clientHeight-L.scrollTop<24};L.addEventListener("scroll",f);f()}}
|
|
503
|
+
document.getElementById("cls").addEventListener("input",function(e){var t=e.target;
|
|
504
|
+
if(t.id==="clh"){var v=t.value.toLowerCase().replace(/[^a-z0-9-]/g,"");if(v!==t.value)t.value=v;CS.handle=v;CS.err="";document.getElementById("clhst").textContent=handleNote()}
|
|
505
|
+
else if(t.id==="cln")CS.name=t.value;else if(t.id==="cle")CS.email=t.value;
|
|
506
|
+
else if(t.tagName==="TEXTAREA")CS.texts[+t.getAttribute("data-i")]=t.value});
|
|
507
|
+
document.getElementById("cls").addEventListener("change",function(e){var o=e.target.getAttribute("data-o");if(o){CS[o]=e.target.checked;if(CS.data)renderClaim()}});
|
|
508
|
+
document.getElementById("cls").addEventListener("click",function(e){var b=e.target.closest("[data-a]");if(!b||e.target.tagName==="TEXTAREA")return;var a=b.getAttribute("data-a"),i=+b.getAttribute("data-i");
|
|
509
|
+
if(a==="back"){CS.err="";if(CS.step===1)closeClaim();else{CS.step=CS.step===3&&!D.examples.length?1:CS.step-1;renderClaim()}}
|
|
510
|
+
else if(a==="close"){closeClaim()}
|
|
511
|
+
else if(a==="next1"){if(CS.busy)return;if(!HANDLE_RE.test(CS.handle)){CS.err="Handles are 3 to 24 letters, numbers or single dashes.";return renderClaim()}if(!CS.name.trim()){CS.err="Add your name.";return renderClaim()}if(!EMAIL_RE.test(CS.email.trim())){CS.err="Add an email address, so the handle stays yours.";return renderClaim()}
|
|
512
|
+
CS.busy=true;CS.err="";renderClaim();fetch(D.api+"/handles/"+encodeURIComponent(CS.handle)).then(function(r){return r.json()}).then(function(j){CS.busy=false;if(j.available){CS.step=D.examples.length?2:3}else CS.err="@"+CS.handle+(j.reason==="already taken"?" is taken. Try another.":": "+j.reason+".");renderClaim()}).catch(function(){CS.busy=false;CS.err="Couldn’t reach usehour.com. Check your connection and try again.";renderClaim()})}
|
|
513
|
+
else if(a==="edit"){e.stopPropagation();CS.editing=CS.editing===i?-1:i;var y0=document.getElementById("cll").scrollTop;renderClaim();document.getElementById("cll").scrollTop=y0}
|
|
514
|
+
else if(a==="tok"){e.stopPropagation();var kk=+b.getAttribute("data-k"),hs=CS.hide[i];if(hs.has(kk))hs.delete(kk);else hs.add(kk);b.classList.toggle("on");var mt=b.closest(".cl-pc").querySelector(".cl-pch span");mt.textContent=mt.textContent.replace(/ \u00B7 \d+ hidden$/,"")+(hidCount(i)?" \u00B7 "+hidCount(i)+" hidden":"")}
|
|
515
|
+
else if(a==="pick"){if(CS.editing===i)return;var at=CS.picked.indexOf(i);CS.err="";if(at>=0)CS.picked.splice(at,1);else if(CS.picked.length>=3)CS.err="Pick up to 3. Untick one first.";else CS.picked.push(i);var y=document.getElementById("cll").scrollTop;renderClaim();document.getElementById("cll").scrollTop=y}
|
|
516
|
+
else if(a==="next2"){CS.err="";CS.editing=-1;CS.step=3;renderClaim()}
|
|
517
|
+
else if(a==="skip2"){CS.picked=[];CS.err="";CS.editing=-1;CS.step=3;renderClaim()}
|
|
518
|
+
else if(a==="data"){CS.data=!CS.data;renderClaim()}
|
|
519
|
+
else if(a==="copy"){var u=CS.done.publicUrl||CS.done.url;(navigator.clipboard?navigator.clipboard.writeText(u):Promise.reject()).then(function(){b.textContent="Copied"},function(){window.prompt("Copy this link",u)})}
|
|
520
|
+
else if(a==="create"){if(CS.busy)return;CS.busy=true;CS.err="";renderClaim();
|
|
521
|
+
fetch(D.api+"/profiles",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload())}).then(function(r){return r.json().catch(function(){return{}}).then(function(j){return{s:r.status,j:j}})}).then(function(x){CS.busy=false;
|
|
522
|
+
if(x.s===201&&x.j.url){CS.done=x.j;CS.step=4;document.getElementById("topnote").textContent="Your profile is on usehour.com. Your logs and other prompts stayed on this computer.";document.getElementById("claimbar").hidden=true}
|
|
523
|
+
else if(x.s===409){CS.step=1;CS.err="Someone just took @"+CS.handle+". Pick another."}
|
|
524
|
+
else CS.err="That didn’t go through ("+(x.j.error||"error "+x.s)+"). Nothing was saved. Try again in a minute.";renderClaim()}).catch(function(){CS.busy=false;CS.err="Couldn’t reach usehour.com. Nothing was saved. Try again.";renderClaim()})}});
|
|
525
|
+
document.getElementById("claimb").onclick=openClaim;
|
|
526
|
+
(function(){var h=document.getElementById("clhandle"),b=document.getElementById("claimb");if(CS.handle){h.textContent=CS.handle;b.textContent="Claim @"+CS.handle}})();
|
|
527
|
+
`;
|
|
528
|
+
|
|
281
529
|
/* --------------------------------------------------------------- the page */
|
|
282
|
-
function pageHTML(m, C) {
|
|
283
|
-
const
|
|
530
|
+
function pageHTML(m, C, o = {}) {
|
|
531
|
+
const api = o.api || null, examples = api ? o.examples || [] : [];
|
|
532
|
+
const data = JSON.stringify({ hours: m.hours, months: m.months.map((x) => x.prompts), cards: C, m: api ? m : null, examples, api, suggest: api ? o.suggest || "" : "" }).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
533
|
+
/* The page may talk to exactly one place: Hour's profile endpoint, and only
|
|
534
|
+
when you press the button. With --no-share it may talk to nothing. */
|
|
535
|
+
const connect = api ? new URL(api).origin : "'none'";
|
|
536
|
+
const escH = (t) => String(t).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
537
|
+
const meta = { period: `${monthYear(m.window.first)} – ${monthYear(m.window.last)}`, sessions: m.sessions.total.toLocaleString("en"), prompts: m.prompts.total.toLocaleString("en") };
|
|
538
|
+
const LOCK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></svg>';
|
|
539
|
+
const DL = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 4v11M7 10l5 5 5-5M5 20h14"/></svg>';
|
|
540
|
+
const csp = `default-src 'none'; img-src data: blob:; font-src data:; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src ${connect}; form-action 'none'; base-uri 'none'`;
|
|
284
541
|
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
542
|
+
<meta http-equiv="Content-Security-Policy" content="${csp}">
|
|
285
543
|
<title>Your AI Wrapped · Hour</title>
|
|
286
544
|
<style>
|
|
287
545
|
@font-face{font-family:"Geist";src:url(data:font/woff2;base64,${FONT_GEIST}) format("woff2");font-weight:100 900}
|
|
288
546
|
:root{--paper:#F7F3EC;--ink:#0B0B0A;--ink-3:#6C6A64;--hair:rgba(11,11,10,.1);--surface:#fff;--sans:"Geist",ui-sans-serif,system-ui,-apple-system,"Helvetica Neue",Arial,sans-serif;--mono:"Geist Mono",ui-monospace,"SF Mono",Menlo,monospace}
|
|
289
547
|
*{box-sizing:border-box}body{background:var(--paper);color:var(--ink);font-family:var(--sans);margin:0;-webkit-font-smoothing:antialiased}
|
|
290
|
-
.wrap{max-width:1180px;margin:0 auto;padding:24px clamp(16px,4vw,
|
|
291
|
-
.top{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap
|
|
292
|
-
.brand{display:flex;align-items:center;gap:9px;font-weight:600}.brand svg{width:22px;height:22px}
|
|
293
|
-
.note{font-size:14px;color:var(--ink-3);
|
|
294
|
-
.
|
|
295
|
-
.
|
|
296
|
-
.
|
|
297
|
-
.
|
|
298
|
-
|
|
299
|
-
.
|
|
300
|
-
.
|
|
548
|
+
.wrap{max-width:1180px;margin:0 auto;padding:24px clamp(16px,4vw,48px) 64px}
|
|
549
|
+
.top{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap}
|
|
550
|
+
.brand{display:flex;align-items:center;gap:9px;font-weight:600;font-size:17px}.brand svg{width:22px;height:22px}
|
|
551
|
+
.note{display:flex;align-items:center;gap:8px;font-size:14px;color:var(--ink-3);line-height:1.5}.note svg{flex:none;width:15px;height:15px}
|
|
552
|
+
.btn{display:inline-flex;align-items:center;justify-content:center;gap:9px;height:52px;padding:0 22px;border-radius:999px;border:0;background:var(--ink);color:var(--paper);font:600 16px var(--sans);cursor:pointer;text-decoration:none}
|
|
553
|
+
.btn.ghost{background:#fff;color:var(--ink);font-weight:500;box-shadow:inset 0 0 0 1px rgba(11,11,10,.14)}.btn svg{width:17px;height:17px;flex:none}.btn[disabled]{opacity:.6;cursor:wait}
|
|
554
|
+
.hero{margin-top:40px;display:grid;grid-template-columns:minmax(0,420px) minmax(0,1fr);gap:clamp(28px,5vw,56px);align-items:stretch}
|
|
555
|
+
.hero-r{display:flex;flex-direction:column}
|
|
556
|
+
.hero h1{margin:4px 0 0;font-size:clamp(48px,6.4vw,76px);line-height:.9;letter-spacing:-.06em;font-weight:650}
|
|
557
|
+
.hero p.sub{margin:16px 0 0;font-size:19px;line-height:1.4;color:#3A3935}
|
|
558
|
+
.acts{margin-top:22px;display:flex;gap:10px;flex-wrap:wrap}
|
|
559
|
+
.claimbox{margin-top:auto;border-radius:26px;background:#8B5FD9;color:#fff;padding:28px 28px 26px;position:relative;overflow:hidden}
|
|
560
|
+
.claimbox:before{content:"";position:absolute;right:-20px;top:-20px;width:300px;height:190px;background-image:radial-gradient(circle,rgba(255,255,255,.9) 0 3px,transparent 3.4px);background-size:14px 14px;-webkit-mask-image:radial-gradient(circle at 80% 20%,#000 10%,transparent 68%);mask-image:radial-gradient(circle at 80% 20%,#000 10%,transparent 68%)}
|
|
561
|
+
.claimbox>*{position:relative}.claimbox .k{font-size:15px;font-weight:500;opacity:.9}.claimbox .h{margin-top:8px;font-size:clamp(32px,3.6vw,44px);line-height:.95;letter-spacing:-.05em;font-weight:650}
|
|
562
|
+
.claimbox .pill{display:inline-flex;margin-top:14px;padding:9px 15px;border-radius:999px;background:rgba(11,11,10,.22);font:16px var(--mono)}.claimbox .pill span{opacity:.7}
|
|
563
|
+
.claimbox p{margin:12px 0 0;max-width:460px;font-size:15.5px;line-height:1.42}
|
|
564
|
+
.claimbox button{margin-top:18px;height:52px;padding:0 24px;border-radius:999px;border:0;background:#fff;color:var(--ink);font:600 16px var(--sans);cursor:pointer}
|
|
565
|
+
h2.sec{margin:64px 0 18px;font-size:30px;letter-spacing:-.04em;font-weight:650}
|
|
566
|
+
.grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:16px}@media(max-width:1000px){.grid{grid-template-columns:repeat(3,minmax(0,1fr))}}@media(max-width:640px){.grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}}
|
|
567
|
+
.grid .card{cursor:pointer;transition:transform .15s}.grid .card:hover{transform:translateY(-3px)}
|
|
568
|
+
.save{display:flex;align-items:center;justify-content:center;gap:7px;width:100%;height:40px;margin-top:6px;border:0;background:none;border-radius:999px;font:500 13px var(--sans);color:#3A3935;cursor:pointer}.save:hover{background:rgba(11,11,10,.05)}.save svg{width:14px;height:14px}
|
|
569
|
+
.tips{margin-top:36px;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px}.tips div{border-radius:18px;background:#fff;box-shadow:inset 0 0 0 1px rgba(11,11,10,.08);padding:20px 22px}.tips b{display:block;font-size:16px}.tips span{display:block;margin-top:4px;font-size:14px;line-height:1.45;color:#3A3935}
|
|
570
|
+
@media(max-width:760px){.hero{grid-template-columns:1fr}.hero-r{order:-1}.claimbox{margin-top:24px}.tips{grid-template-columns:1fr}h2.sec{margin-top:48px;font-size:24px}}
|
|
571
|
+
.intro{position:fixed;inset:0;z-index:40;background:var(--ink);color:#F4F1EA;display:flex;flex-direction:column;padding:26px clamp(20px,4vw,48px);overflow:auto}.intro[hidden],.story[hidden]{display:none}
|
|
572
|
+
.intro:before{content:"";position:absolute;inset:0;background-image:radial-gradient(circle,rgba(244,241,234,.85) 0 3px,transparent 3.4px);background-size:18px 18px;-webkit-mask-image:radial-gradient(ellipse at 78% 40%,#000 0%,transparent 55%);mask-image:radial-gradient(ellipse at 78% 40%,#000 0%,transparent 55%);pointer-events:none}
|
|
573
|
+
.intro>*{position:relative}.intro .top .note{color:#F4F1EA;opacity:.75}
|
|
574
|
+
.intro .mid{margin:auto 0;max-width:720px;padding:40px 0}.intro .meta{font-size:clamp(15px,1.6vw,18px);font-weight:500;opacity:.8}
|
|
575
|
+
.intro h1{margin:18px 0 0;font-size:clamp(56px,9vw,112px);line-height:.88;letter-spacing:-.065em;font-weight:650}
|
|
576
|
+
.intro p{margin:24px 0 0;font-size:clamp(18px,2vw,22px);line-height:1.35;opacity:.9;max-width:560px}
|
|
577
|
+
.intro .go{margin-top:34px;display:flex;gap:12px;flex-wrap:wrap;align-items:center}
|
|
578
|
+
.intro .play{display:inline-flex;align-items:center;gap:12px;height:62px;padding:0 30px 0 24px;border-radius:999px;border:0;background:#F4F1EA;color:var(--ink);font:600 19px var(--sans);cursor:pointer}.intro .play svg{width:20px;height:20px}
|
|
579
|
+
.intro .skip{height:62px;padding:0 20px;border:0;background:none;color:#F4F1EA;opacity:.8;font:500 16px var(--sans);cursor:pointer}
|
|
580
|
+
.intro .tip{font-size:13px;opacity:.55}
|
|
581
|
+
.story{position:fixed;inset:0;z-index:40;background:#161614;color:#F4F1EA;display:flex;flex-direction:column;align-items:center;padding:18px clamp(12px,3vw,32px) 20px}
|
|
582
|
+
.story .top{align-self:stretch}.story .cnt{display:flex;align-items:center;gap:14px;font-size:14px}.story .cnt span{opacity:.6}
|
|
583
|
+
.rb{display:inline-flex;align-items:center;justify-content:center;width:44px;height:44px;border-radius:50%;border:0;background:rgba(244,241,234,.1);color:#F4F1EA;cursor:pointer;flex:none}.rb svg{width:18px;height:18px}
|
|
584
|
+
.st-row{flex:1;min-height:0;width:100%;display:flex;align-items:center;justify-content:center;gap:clamp(8px,3vw,36px)}
|
|
585
|
+
.st-col{--w:min(480px,88vw,calc((100svh - 190px) * .8));width:var(--w);display:flex;flex-direction:column;gap:10px}
|
|
586
|
+
.st-bars{display:flex;gap:4px}.st-bars i{flex:1;height:3px;border-radius:2px;background:rgba(244,241,234,.25);overflow:hidden}.st-bars i b{display:block;height:100%;width:0;background:#F4F1EA}
|
|
587
|
+
.st-slot{position:relative}.st-slot .card{box-shadow:0 40px 100px rgba(0,0,0,.5);border-radius:28px/22px}
|
|
588
|
+
.st-hit{position:absolute;top:0;bottom:0;z-index:4;background:transparent;border:0;cursor:pointer}.st-hit.l{left:0;width:32%}.st-hit.r{right:0;width:68%}
|
|
589
|
+
.nb{width:56px;height:56px}.nb.fw{background:#F4F1EA;color:var(--ink)}.nb svg{width:22px;height:22px}
|
|
590
|
+
@media(max-width:640px){.nb{display:none}}
|
|
591
|
+
.st-acts{margin-top:14px;display:flex;gap:10px}.st-acts button{display:inline-flex;align-items:center;gap:8px;height:44px;padding:0 18px;border-radius:999px;border:0;background:rgba(244,241,234,.1);color:#F4F1EA;font:500 14px var(--sans);cursor:pointer}.st-acts svg{width:16px;height:16px}
|
|
301
592
|
.card{container-type:inline-size;aspect-ratio:4/5;width:100%;border-radius:5.5%/4.4%;overflow:hidden;position:relative;background:var(--bg);color:var(--fg);isolation:isolate}
|
|
302
593
|
.card canvas{position:absolute;inset:0;width:100%;height:100%}
|
|
303
594
|
.card .in{position:absolute;inset:0;padding:9cqw 8cqw 7cqw;display:flex;flex-direction:column;z-index:1}
|
|
@@ -307,22 +598,43 @@ h2{font-size:24px;letter-spacing:-.03em;margin:0 0 14px}
|
|
|
307
598
|
.ft{display:flex;justify-content:space-between;align-items:center;margin-top:6cqw;font-size:3.4cqw;opacity:.8}.ft .b{display:flex;align-items:center;gap:1.6cqw;font-weight:600}.ft svg{width:4.6cqw;height:4.6cqw}.ft .u{font-family:var(--mono)}
|
|
308
599
|
.sum{margin-top:auto;display:grid;grid-template-columns:1fr 1fr;gap:4.5cqw 5cqw}.sum div{border-top:.35cqw solid currentColor;padding-top:2cqw}.sum small{display:block;font-size:3.4cqw;opacity:.75;margin-bottom:.6cqw}.sum b{font-size:6.2cqw;letter-spacing:-.035em;font-weight:650;line-height:1}
|
|
309
600
|
.arch{font-size:11.5cqw;line-height:.92;letter-spacing:-.05em;font-weight:650;margin:2cqw 0 6cqw}
|
|
601
|
+
.rows{margin-top:7cqw;display:grid;gap:3.4cqw}.rows div{display:grid;grid-template-columns:1fr auto 10cqw;align-items:center;gap:3cqw;font-size:4.3cqw;font-weight:500}.rows em{display:flex;gap:1.1cqw}.rows u{width:2.7cqw;height:2.7cqw;border-radius:50%;background:currentColor;opacity:.14}.rows u.on{opacity:1;background:#8B5FD9}.rows b{text-align:right;font-weight:600}
|
|
602
|
+
${api ? CLAIM_CSS : ""}
|
|
310
603
|
.grad{background:linear-gradient(100deg,#4F88FF 0%,#B0ACFE 38%,#FB65C2 72%,#FD2980 100%);-webkit-background-clip:text;background-clip:text;color:transparent}
|
|
311
604
|
</style></head><body>
|
|
312
605
|
<svg width="0" height="0" style="position:absolute" aria-hidden="true"><defs><linearGradient id="ob" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#4F88FF"/><stop offset="38%" stop-color="#B0ACFE"/><stop offset="72%" stop-color="#FB65C2"/><stop offset="100%" stop-color="#FD2980"/></linearGradient><symbol id="mark" viewBox="0 0 64 64"><path fill="url(#ob)" d="${MARK_PATH}"/></symbol></defs></svg>
|
|
606
|
+
<div class="intro" id="intro">
|
|
607
|
+
<div class="top"><div class="brand"><svg viewBox="0 0 64 64"><use href="#mark"/></svg>Hour</div><div class="note">${LOCK}Made on this computer. Nothing has been sent.</div></div>
|
|
608
|
+
<div class="mid"><div class="meta">${escH(meta.period)} · ${meta.sessions} sessions · ${meta.prompts} prompts</div>
|
|
609
|
+
<h1>Your year with <span class="grad">AI</span> is ready.</h1>
|
|
610
|
+
<p>${C.length} cards about how you build: your type, your hours, how you brief the agent. About a minute.</p>
|
|
611
|
+
<div class="go"><button type="button" class="play" id="play"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7 4.5v15l13-7.5z"/></svg>Play my year</button><button type="button" class="skip" id="skip">Skip to all cards</button></div></div>
|
|
612
|
+
<div class="tip">Tip: use the arrow keys, or tap the left and right of a card. Space pauses.</div>
|
|
613
|
+
</div>
|
|
614
|
+
<div class="story" id="story" hidden role="dialog" aria-modal="true" aria-label="Your year with AI">
|
|
615
|
+
<div class="top"><div class="brand"><svg viewBox="0 0 64 64"><use href="#mark"/></svg>Your year with AI</div><div class="cnt"><span id="stn"></span><button type="button" class="rb" id="stx" aria-label="Close"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg></button></div></div>
|
|
616
|
+
<div class="st-row"><button type="button" class="rb nb" id="stp" aria-label="Previous card"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 5l-7 7 7 7"/></svg></button>
|
|
617
|
+
<div class="st-col"><div class="st-bars" id="bars"></div><div class="st-slot"><div id="slot"></div><button type="button" class="st-hit l" id="hl" aria-label="Previous"></button><button type="button" class="st-hit r" id="hr" aria-label="Next"></button></div></div>
|
|
618
|
+
<button type="button" class="rb nb fw" id="stf" aria-label="Next card"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5l7 7-7 7"/></svg></button></div>
|
|
619
|
+
<div class="st-acts" id="sta"><button type="button" id="sts">${DL}Save this card</button><button type="button" id="stz"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/></svg><span>Pause</span></button></div>
|
|
620
|
+
</div>
|
|
313
621
|
<div class="wrap">
|
|
314
|
-
<div class="top"><div class="brand"><svg viewBox="0 0 64 64"><use href="#mark"/></svg>Hour</div><div class="note"
|
|
315
|
-
<
|
|
316
|
-
<
|
|
317
|
-
<
|
|
318
|
-
|
|
622
|
+
<div class="top"><div class="brand"><svg viewBox="0 0 64 64"><use href="#mark"/></svg>Hour</div><div class="note" id="topnote">${LOCK}Made on this computer. Nothing has been sent.</div></div>
|
|
623
|
+
<section class="hero"><div id="sumcard"></div><div class="hero-r">
|
|
624
|
+
<h1>Your year with AI</h1><p class="sub">${C.length} cards from ${meta.sessions} sessions. Save the ones you like and post them.</p>
|
|
625
|
+
<div class="acts"><button type="button" class="btn" id="saveall">${DL}Save all cards</button><button type="button" class="btn ghost" id="replay"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7 4.5v15l13-7.5z"/></svg>Play again</button></div>
|
|
626
|
+
${api ? `<div class="claimbox" id="claimbar"><div class="k">Next: make it a profile</div><div class="h">Claim your handle.</div><div class="pill"><span>usehour.com/</span>@<b id="clhandle">you</b></div><p>A profile companies hiring AI builders actually want to read. First come, first served. Private until you say otherwise.</p><button type="button" id="claimb">Claim your handle</button></div>` : ""}
|
|
627
|
+
</div></section>
|
|
628
|
+
<h2 class="sec">All your cards</h2><div class="grid" id="grid"></div>
|
|
629
|
+
<div class="tips"><div><b>What to post</b><span>Your summary card or your builder type, with one line on what surprised you.</span></div><div><b>Where they go</b><span>Saved cards land in your Downloads, sized for LinkedIn and X.</span></div><div><b>Come back any time</b><span>This page lives in the hour-wrapped folder where you ran the command. The numbers are in wrapped.json.</span></div></div>
|
|
630
|
+
${api ? CLAIM_HTML : ""}
|
|
319
631
|
</div>
|
|
320
632
|
<script>
|
|
321
633
|
const D=${data};
|
|
322
634
|
const esc=s=>String(s).replace(/[&<>"]/g,c=>({"&":"&","<":"<",">":">",'"':"""}[c]));
|
|
323
635
|
function el(c){const d=document.createElement("article");d.className="card";d.style.setProperty("--bg",c.bg);d.style.setProperty("--fg",c.fg);if(c.bs)d.style.setProperty("--bs",c.bs);
|
|
324
636
|
const ft='<div class="ft"><span class="b"><svg viewBox="0 0 64 64"><use href="#mark"/></svg>Hour</span><span class="u">usehour.com</span></div>';
|
|
325
|
-
d.innerHTML='<canvas aria-hidden="true"></canvas><div class="in">'+(c.summary?('<div class="ask">'+esc(c.who?c.who+" \\u00B7 ":"")+esc(c.period)+'</div><div class="arch grad">'+esc(c.type)+'</div><div class="sum">'+c.stats.map(s=>'<div><small>'+esc(s[0])+'</small><b>'+esc(s[1])+'</b></div>').join("")+'</div>'):('<div class="ask">'+esc(c.ask)+'</div><div class="big'+(c.grad?" grad":"")+'">'+esc(c.big)+'</div><div class="line">'+esc(c.line)+'</div>'))+ft+'</div>';
|
|
637
|
+
d.innerHTML='<canvas aria-hidden="true"></canvas><div class="in">'+(c.summary?('<div class="ask">'+esc(c.who?c.who+" \\u00B7 ":"")+esc(c.period)+'</div><div class="arch grad">'+esc(c.type)+'</div><div class="sum">'+c.stats.map(s=>'<div><small>'+esc(s[0])+'</small><b>'+esc(s[1])+'</b></div>').join("")+'</div>'):('<div class="ask">'+esc(c.ask)+'</div>'+(c.rows?'<div class="rows">'+c.rows.map(w=>'<div><span>'+esc(w[0])+'</span><em>'+Array.from({length:10},(_,k)=>'<u'+(k<Math.round(w[1]*10)?' class="on"':'')+'></u>').join("")+'</em><b>'+esc(w[2])+'</b></div>').join("")+'</div>':'')+'<div class="big'+(c.grad?" grad":"")+'">'+esc(c.big)+'</div><div class="line">'+esc(c.line)+'</div>'))+ft+'</div>';
|
|
326
638
|
requestAnimationFrame(()=>draw(d,c));return d}
|
|
327
639
|
function draw(card,c){const cv=card.querySelector("canvas"),r=card.getBoundingClientRect(),dpr=Math.min(devicePixelRatio||1,2),W=Math.max(1,r.width),H=Math.max(1,r.height);cv.width=W*dpr;cv.height=H*dpr;const g=cv.getContext("2d");g.scale(dpr,dpr);drawOn(g,W,H,c)}
|
|
328
640
|
function drawOn(x,W,H,c){if(!c.pat)return;
|
|
@@ -349,13 +661,32 @@ c.stats.forEach((s,k)=>{const x=P+(k%2)*(cw+W*.05),yy=top+Math.floor(k/2)*rowH;g
|
|
|
349
661
|
g.font="650 "+W*.115+"px "+F;g.fillStyle=grad();g.fillText(c.type,P,W*.33);g.fillStyle=c.fg;g.globalAlpha=.85;g.font="500 "+W*.044+"px "+F;g.fillText((c.who?c.who+" · ":"")+c.period,P,W*.16);g.globalAlpha=1;return}
|
|
350
662
|
g.font="450 "+W*.056+"px "+F;const L=wrapText(g,c.line,(W-2*P)*.92);L.reverse().forEach((l,i)=>g.fillText(l,P,y-i*W*.07));y-=L.length*W*.07+W*.034;
|
|
351
663
|
let bs=parseFloat(c.bs||"19")/100*W;g.font="650 "+bs+"px "+F;let Bl=wrapText(g,c.big,W-2*P);while(Bl.length>1&&Bl[Bl.length-1].length<=2&&bs>W*.08){bs*=.92;g.font="650 "+bs+"px "+F;Bl=wrapText(g,c.big,W-2*P)}g.fillStyle=c.grad?grad():c.fg;Bl.reverse().forEach((l,i)=>g.fillText(l,P,y-i*bs*.9));
|
|
352
|
-
g.fillStyle=c.fg;g.globalAlpha=.85;g.font="500 "+W*.044+"px "+F;wrapText(g,c.ask,(W-2*P)*.62).forEach((l,i)=>g.fillText(l,P,W*.16+i*W*.055));g.globalAlpha=1
|
|
664
|
+
g.fillStyle=c.fg;g.globalAlpha=.85;g.font="500 "+W*.044+"px "+F;wrapText(g,c.ask,(W-2*P)*.62).forEach((l,i)=>g.fillText(l,P,W*.16+i*W*.055));g.globalAlpha=1;
|
|
665
|
+
if(c.rows){const d=W*.027,gap=W*.011,pw=W*.1,dx=W-P-pw-10*d-9*gap;c.rows.forEach((w,k)=>{const yy=W*.3+k*W*.1;g.fillStyle=c.fg;g.font="500 "+W*.043+"px "+F;g.fillText(w[0],P,yy);g.font="600 "+W*.043+"px "+F;g.textAlign="right";g.fillText(w[2],W-P,yy);g.textAlign="left";for(let q=0;q<10;q++){const on=q<Math.round(w[1]*10);g.globalAlpha=on?1:.14;g.fillStyle=on?"#8B5FD9":c.fg;g.beginPath();g.arc(dx+q*(d+gap)+d/2,yy-W*.015,d/2,0,7);g.fill()}g.globalAlpha=1})}}
|
|
353
666
|
async function save(c){try{await document.fonts.ready}catch(e){}const cv=document.createElement("canvas");cv.width=1080;cv.height=1350;paint(cv.getContext("2d"),1080,1350,c);const a=document.createElement("a");a.download="hour-"+c.id+".png";a.href=cv.toDataURL("image/png");document.body.appendChild(a);a.click();a.remove()}
|
|
354
|
-
let cur=0
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
667
|
+
let cur=0,timer=null,t0=0,paused=false;const DUR=6000;
|
|
668
|
+
const $=id=>document.getElementById(id),slot=$("slot"),bars=$("bars"),grid=$("grid");
|
|
669
|
+
const N=D.cards.length+(D.api?1:0);for(let k=0;k<N;k++){const i=document.createElement("i");i.appendChild(document.createElement("b"));bars.appendChild(i)}
|
|
670
|
+
function fill(){[...bars.children].forEach((b,k)=>{b.firstChild.style.width=k<cur?"100%":k>cur?"0":b.firstChild.style.width})}
|
|
671
|
+
function tick(){if(paused||$("story").hidden)return;const p=Math.min(1,(performance.now()-t0)/DUR);bars.children[cur].firstChild.style.width=(p*100)+"%";if(p>=1){go(cur+1);return}timer=requestAnimationFrame(tick)}
|
|
672
|
+
function show(n){cancelAnimationFrame(timer);cur=Math.max(0,n);const cl=cur>=D.cards.length;slot.innerHTML="";slot.appendChild(cl?claimEl():el(D.cards[cur]));
|
|
673
|
+
$("hl").style.display=$("hr").style.display=cl?"none":"";$("sts").style.visibility=cl?"hidden":"";$("stz").style.visibility=cl?"hidden":"";
|
|
674
|
+
$("stn").textContent=(cur+1)+" of "+N;[...bars.children].forEach((b,k)=>{b.firstChild.style.width=k<cur?"100%":"0"});
|
|
675
|
+
if(cl){bars.children[cur].firstChild.style.width="100%";return}t0=performance.now();if(!paused)timer=requestAnimationFrame(tick)}
|
|
676
|
+
function go(n){if(n>=N){closeStory();return}show(n)}
|
|
677
|
+
function openStory(n){$("intro").hidden=true;$("story").hidden=false;document.body.style.overflow="hidden";paused=false;setPause();show(n||0)}
|
|
678
|
+
function closeStory(){cancelAnimationFrame(timer);$("story").hidden=true;$("intro").hidden=true;document.body.style.overflow=""}
|
|
679
|
+
function setPause(){$("stz").querySelector("span").textContent=paused?"Play":"Pause"}
|
|
680
|
+
function togglePause(){if(cur>=D.cards.length)return;paused=!paused;setPause();if(!paused){const w=parseFloat(bars.children[cur].firstChild.style.width)||0;t0=performance.now()-w/100*DUR;timer=requestAnimationFrame(tick)}else cancelAnimationFrame(timer)}
|
|
681
|
+
$("play").onclick=()=>openStory(0);$("skip").onclick=closeStory;$("replay").onclick=()=>openStory(0);$("stx").onclick=closeStory;
|
|
682
|
+
$("stp").onclick=$("hl").onclick=()=>show(cur-1);$("stf").onclick=$("hr").onclick=()=>go(cur+1);$("stz").onclick=togglePause;$("sts").onclick=()=>save(D.cards[cur]);
|
|
683
|
+
addEventListener("keydown",e=>{const o=$("cl");if(o&&!o.hidden)return;if($("story").hidden){if(!$("intro").hidden&&(e.key==="Enter"||e.key===" ")){e.preventDefault();openStory(0)}return}
|
|
684
|
+
if(e.key==="ArrowRight")go(cur+1);else if(e.key==="ArrowLeft")show(cur-1);else if(e.key===" "){e.preventDefault();togglePause()}else if(e.key==="Escape")closeStory()});
|
|
685
|
+
const SUM=D.cards.find(c=>c.summary);if(SUM){const e=el(SUM);e.style.cursor="pointer";e.onclick=()=>openStory(D.cards.indexOf(SUM));$("sumcard").appendChild(e)}
|
|
686
|
+
const DLI='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 4v11M7 10l5 5 5-5M5 20h14"/></svg>';
|
|
687
|
+
D.cards.forEach((c,k)=>{const w=document.createElement("div");const e=el(c);e.onclick=()=>openStory(k);const s=document.createElement("button");s.type="button";s.className="save";s.innerHTML=DLI+"Save";s.onclick=()=>save(c);w.append(e,s);grid.appendChild(w)});
|
|
688
|
+
$("saveall").onclick=async()=>{const b=$("saveall"),t=b.innerHTML;b.disabled=true;for(let i=0;i<D.cards.length;i++){b.textContent="Saving "+(i+1)+" of "+D.cards.length+"…";await save(D.cards[i]);await new Promise(r=>setTimeout(r,350))}b.innerHTML=t;b.disabled=false};
|
|
689
|
+
${api ? CLAIM_JS : ""}
|
|
359
690
|
</script></body></html>`;
|
|
360
691
|
}
|
|
361
692
|
|
|
@@ -380,12 +711,10 @@ function openFile(f) {
|
|
|
380
711
|
const args = process.platform === "win32" ? ["/c", "start", "", f] : [f];
|
|
381
712
|
try { spawn(cmd, args, { stdio: "ignore", detached: true }).unref(); } catch { /* the path is printed anyway */ }
|
|
382
713
|
}
|
|
383
|
-
/* Sharing stays off until usehour.com can store profiles. */
|
|
384
|
-
const SHARE_OPEN = false;
|
|
385
714
|
async function main() {
|
|
386
715
|
const argv = process.argv.slice(2), flag = (n) => argv.includes(n), opt = (n) => { const i = argv.indexOf(n); return i >= 0 ? argv[i + 1] : null; };
|
|
387
716
|
if (flag("--help") || flag("-h")) {
|
|
388
|
-
console.log(`Hour Wrapped ${VERSION}\n\n npx usehour [--yes] [--no-open] [--out DIR] [--claude-dir DIR] [--codex-dir DIR]
|
|
717
|
+
console.log(`Hour Wrapped ${VERSION}\n\n npx usehour [--yes] [--no-open] [--no-share] [--out DIR] [--claude-dir DIR] [--codex-dir DIR]\n\n --yes skip the first question (reading still happens only on this machine)\n --no-open don't open the browser\n --no-share cards only: no example prompts, and the cards page cannot connect anywhere\n --out DIR where to write hour-wrapped/ (default: current folder)\n`);
|
|
389
718
|
return;
|
|
390
719
|
}
|
|
391
720
|
const home = os.homedir();
|
|
@@ -394,6 +723,7 @@ async function main() {
|
|
|
394
723
|
{ src: "codex", label: "Codex", dir: opt("--codex-dir") || path.join(process.env.CODEX_HOME || path.join(home, ".codex"), "sessions") },
|
|
395
724
|
].map((s) => ({ ...s, files: walk(s.dir) }));
|
|
396
725
|
const found = sources.filter((s) => s.files.length);
|
|
726
|
+
const share = !flag("--no-share");
|
|
397
727
|
console.log(`\n ${B("Hour Wrapped")} ${DIM("v" + VERSION)} Your year of building with AI.\n`);
|
|
398
728
|
if (!found.length) {
|
|
399
729
|
console.log(` No Claude Code or Codex sessions found.\n Looked in:\n${sources.map((s) => " " + s.dir).join("\n")}\n\n If yours live somewhere else, run with --claude-dir or --codex-dir.\n`);
|
|
@@ -402,8 +732,8 @@ async function main() {
|
|
|
402
732
|
const bytes = found.reduce((a, s) => a + s.files.reduce((b, f) => { try { return b + fs.statSync(f).size; } catch { return b; } }, 0), 0);
|
|
403
733
|
console.log(" This reads your session logs, on this machine only:");
|
|
404
734
|
for (const s of found) console.log(` ${s.label.padEnd(12)} ${s.files.length} files ${DIM(s.dir)}`);
|
|
405
|
-
console.log(`\n It keeps counts
|
|
406
|
-
console.log(`
|
|
735
|
+
console.log(`\n It keeps counts${share ? ", plus a few of your best prompts for you to choose from" : ""}.`);
|
|
736
|
+
console.log(` This script never connects to the internet.\n`);
|
|
407
737
|
if (!flag("--yes")) {
|
|
408
738
|
if (!process.stdin.isTTY) { console.log(" Run this in a terminal (or add --yes)."); process.exitCode = 1; return; }
|
|
409
739
|
const a = await ask(` Read ${(bytes / 1e9).toFixed(1)} GB of logs and make your cards? [y/N] `);
|
|
@@ -421,7 +751,7 @@ async function main() {
|
|
|
421
751
|
if (next >= jobs.length) { w.terminate(); if (--active === 0) resolve(); return; }
|
|
422
752
|
const job = jobs[next++];
|
|
423
753
|
w.once("message", (r) => { if (r.ok) summaries.push(r.s); done++; doneBytes += job.size; tick(); feed(w); });
|
|
424
|
-
w.postMessage({ file: job.file, src: job.src });
|
|
754
|
+
w.postMessage({ file: job.file, src: job.src, examples: share });
|
|
425
755
|
};
|
|
426
756
|
workers.forEach((w) => { active++; feed(w); });
|
|
427
757
|
});
|
|
@@ -434,35 +764,18 @@ async function main() {
|
|
|
434
764
|
fs.mkdirSync(outDir, { recursive: true });
|
|
435
765
|
fs.writeFileSync(path.join(outDir, "wrapped.json"), JSON.stringify(m, null, 2));
|
|
436
766
|
const page = path.join(outDir, "index.html");
|
|
437
|
-
|
|
767
|
+
const examples = share ? pickExamples(summaries, 10) : [];
|
|
768
|
+
const suggest = (() => { try { const u = os.userInfo().username.toLowerCase().replace(/[^a-z0-9-]/g, "").replace(/^-+|-+$/g, "").slice(0, 24); return ["root", "admin", "user", "ubuntu", "runner", "administrator", "guest", "owner"].includes(u) ? "" : u; } catch { return ""; } })();
|
|
769
|
+
fs.writeFileSync(page, pageHTML(m, C, { api: share ? (opt("--api") || API_DEFAULT).replace(/\/+$/, "") : null, examples, suggest: suggest.length >= 3 ? suggest : "" }));
|
|
438
770
|
const type = TYPES[builderType(m)].name;
|
|
439
771
|
console.log(`\n ${B("You're " + type + ".")} ${m.sessions.total} sessions, ${m.prompts.total.toLocaleString("en")} prompts, busiest at ${hourLabel(m.busiestHour)}.`);
|
|
440
772
|
console.log(` Your cards: ${page}\n`);
|
|
441
773
|
if (!flag("--no-open")) openFile(page);
|
|
442
|
-
if (
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
if (!yes(a1)) { console.log("\n Done. Nothing was sent.\n"); return; }
|
|
448
|
-
const email = await ask(" Your email (so we can send you the link): ");
|
|
449
|
-
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) { console.log("\n That doesn't look like an email. Nothing was sent.\n"); return; }
|
|
450
|
-
const name = await ask(" Your name, as it should appear on the page: ");
|
|
451
|
-
const linkedin = await ask(` LinkedIn URL ${DIM("(optional, press Enter to skip)")}: `);
|
|
452
|
-
const pub = yes(await ask(" Make your page public so you can share the link? [y/N] "));
|
|
453
|
-
const feature = yes(await ask(" OK for Hour to feature your cards in posts about great AI builders? [y/N] "));
|
|
454
|
-
const roles = yes(await ask(" Open to hearing about roles where this matters? [y/N] "));
|
|
455
|
-
const payload = { email, name, linkedin, public: pub, feature, roles, wrapped: m };
|
|
456
|
-
console.log(`\n This is exactly what will be sent to ${opt("--share-url") || SHARE_URL_DEFAULT}:\n`);
|
|
457
|
-
console.log(JSON.stringify(payload, null, 2).split("\n").map((l) => " " + l).join("\n"));
|
|
458
|
-
if (!yes(await ask("\n Send it? [y/N] "))) { console.log("\n Not sent. Your cards are still on your machine.\n"); return; }
|
|
459
|
-
const body = new URLSearchParams({ "form-name": "wrapped", email, name, linkedin, public: String(pub), feature: String(feature), roles: String(roles), data: JSON.stringify(m) });
|
|
460
|
-
try {
|
|
461
|
-
const r = await fetch(opt("--share-url") || SHARE_URL_DEFAULT, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body, redirect: "manual" });
|
|
462
|
-
if (r.status >= 200 && r.status < 400) console.log(`\n Sent. We'll email ${email} your link within a day.\n`);
|
|
463
|
-
else console.log(`\n The upload didn't go through (${r.status}). Email wrapped.json to lush@usehour.com instead.\n`);
|
|
464
|
-
} catch (e) {
|
|
465
|
-
console.log(`\n Couldn't reach usehour.com (${e.message}). Email wrapped.json to lush@usehour.com instead.\n`);
|
|
774
|
+
if (share) {
|
|
775
|
+
console.log(` Want a profile? The last card lets you claim your handle at usehour.com/@you.`);
|
|
776
|
+
console.log(` Nothing is sent unless you press the buttons there. You see exactly what goes first.`);
|
|
777
|
+
if (examples.length) console.log(DIM(` (The cards page holds ${examples.length} of your prompts so you can choose. It stays on this computer.)`));
|
|
778
|
+
console.log("");
|
|
466
779
|
}
|
|
467
780
|
}
|
|
468
781
|
/* ------------------------------------------------------------- assets
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "usehour",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Your year of building with AI, as Wrapped-style cards. Reads your Claude Code and Codex logs locally.
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Your year of building with AI, as Wrapped-style cards, and an optional private profile. Reads your Claude Code and Codex logs locally.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": { "usehour": "hour.mjs" },
|
|
7
7
|
"files": ["hour.mjs"],
|