usehour 0.3.1 → 0.4.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 +2 -1
- package/hour.mjs +443 -113
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Your year of building with AI, as Wrapped-style cards.
|
|
4
4
|
|
|
5
5
|
```
|
|
6
|
-
npx usehour@0.
|
|
6
|
+
npx usehour@0.4.0
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
- Asks before it reads anything, and lists the folders it will read.
|
|
@@ -11,6 +11,7 @@ npx usehour@0.3.1
|
|
|
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
|
+
- Counts only sessions you started yourself: sub-agents and runs started by scripts or SDKs are left out. `npx usehour@0.4.0 --explain` shows what was counted and what was left out, in your terminal only.
|
|
14
15
|
- No dependencies, nothing installed.
|
|
15
16
|
|
|
16
17
|
It's one file: read [hour.mjs](https://usehour.com/hour.mjs) before you run it.
|
package/hour.mjs
CHANGED
|
@@ -8,10 +8,11 @@
|
|
|
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. Counts: sessions, hours, models, how you brief and steer the agent
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
3. Counts: sessions, hours, models, how you brief and steer the agent,
|
|
12
|
+
the file types your agents edited, commits, tools used. Counts and
|
|
13
|
+
labels only: no prompt you wrote, no code, no file contents. The one
|
|
14
|
+
exception is your go-to phrase, if it is three words or fewer (like
|
|
15
|
+
"please proceed"), which you see before anything is sent.
|
|
15
16
|
4. Writes ./hour-wrapped/index.html (your cards) and wrapped.json (the
|
|
16
17
|
counts only), then opens the cards in your browser.
|
|
17
18
|
5. This script never uses the network. The cards page can make one request,
|
|
@@ -30,7 +31,7 @@ import { spawn } from "node:child_process";
|
|
|
30
31
|
import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
|
|
31
32
|
import { fileURLToPath } from "node:url";
|
|
32
33
|
|
|
33
|
-
const VERSION = "0.
|
|
34
|
+
const VERSION = "0.4.0";
|
|
34
35
|
const API_DEFAULT = "https://usehour.com/api";
|
|
35
36
|
|
|
36
37
|
/* ------------------------------------------------------------------ patterns */
|
|
@@ -105,9 +106,21 @@ function promptFacts(ts, text) {
|
|
|
105
106
|
spec: SPECIFIC.test(t),
|
|
106
107
|
explains: kind === "correct" && words >= 12 && EXPLAIN.test(t),
|
|
107
108
|
pasted: PASTED.test(t),
|
|
109
|
+
work: words >= 5 ? workKind(t) : null,
|
|
108
110
|
};
|
|
109
111
|
return f;
|
|
110
112
|
}
|
|
113
|
+
/* What a prompt is mostly about, as one label. First match wins, so "fix the
|
|
114
|
+
failing test" is fixing, not testing. Only the label leaves this function. */
|
|
115
|
+
const WORK = [
|
|
116
|
+
["fix", /\b(fix|fixes|bug|bugs|error|errors|broken|breaks|fails?|failing|crash\w*|exception|stack ?trace|not working|doesn'?t work|regression)\b/i],
|
|
117
|
+
["tidy", /\b(refactor\w*|clean ?up|rename|simplify|restructure|reorgani[sz]e|tidy|dedupe|split (it|this|the)|extract)\b/i],
|
|
118
|
+
["test", /\b(write (a |the )?tests?|unit tests?|e2e|coverage|test suite|add tests?)\b/i],
|
|
119
|
+
["understand", /\b(explain|what does|how does|why does|walk me through|understand|investigate|look into|find out|figure out)\b/i],
|
|
120
|
+
["write", /\b(readme|docs?|documentation|write[- ]?up|blog|article|post|email|copy)\b/i],
|
|
121
|
+
["build", /\b(add|build|create|implement|make|new|feature|set ?up|scaffold|generate|design)\b/i],
|
|
122
|
+
];
|
|
123
|
+
function workKind(t) { for (const [k, re] of WORK) if (re.test(t)) return k; return null; }
|
|
111
124
|
/* How good an example a prompt is: a clear brief with context, limits, a
|
|
112
125
|
finish line, steps, specifics and a request for proof. Long pastes of code
|
|
113
126
|
or logs are not examples of prompting, so they are skipped. */
|
|
@@ -130,12 +143,21 @@ function cleanPrompt(t) {
|
|
|
130
143
|
if (!t || t.startsWith("<") || t.startsWith("# AGENTS.md") || t.startsWith("Caveat:") || t.startsWith("[Request interrupted") || t.startsWith("# Files mentioned")) return null;
|
|
131
144
|
return t;
|
|
132
145
|
}
|
|
146
|
+
/* A short fingerprint of a prompt, used only to spot the same message logged
|
|
147
|
+
in two files (a resumed or forked session). The words themselves never
|
|
148
|
+
leave the file parser. */
|
|
149
|
+
function hashText(t) { let h = 0x811c9dc5; for (let i = 0; i < t.length; i++) { h ^= t.charCodeAt(i); h = Math.imul(h, 0x01000193); } return (h >>> 0).toString(36); }
|
|
133
150
|
let KEEP_EXAMPLES = true;
|
|
134
151
|
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"/;
|
|
135
152
|
const KEEP_CLAUDE = /"type":\s*"(user|assistant)"/;
|
|
136
153
|
async function parseFile(file, src) {
|
|
137
|
-
const
|
|
138
|
-
const
|
|
154
|
+
const sub = file.includes(`${path.sep}subagents${path.sep}`);
|
|
155
|
+
const s = { src, sub, how: "typed", via: null, id: path.basename(file, ".jsonl"), parent: sub ? path.basename(path.dirname(path.dirname(file))) : null, start: null, end: null, cwd: null, models: {}, prompts: [], ids: [], examples: [], patches: 0, cmds: 0, tests: 0, aborts: 0, tokens: 0, exts: {}, commits: 0, prs: 0, mcp: {}, skills: {} };
|
|
156
|
+
const seenMsg = new Set(); const seenText = new Map(); let waiting = false, curModel = null, agentTs = null;
|
|
157
|
+
/* Each prompt is "being worked on" from when it was sent until the agent's
|
|
158
|
+
last step before the next prompt. Only the two times are kept. */
|
|
159
|
+
const addPrompt = (f) => { closeLast(); s.prompts.push(f); agentTs = null; };
|
|
160
|
+
const closeLast = () => { const l = s.prompts[s.prompts.length - 1]; if (l && agentTs && agentTs > (l.ts || "")) l.end = agentTs; };
|
|
139
161
|
const rl = readline.createInterface({ input: fs.createReadStream(file, { encoding: "utf8" }), crlfDelay: Infinity });
|
|
140
162
|
for await (const line of rl) {
|
|
141
163
|
if (line.length > 4_000_000) continue;
|
|
@@ -147,55 +169,93 @@ async function parseFile(file, src) {
|
|
|
147
169
|
if (src === "claude") {
|
|
148
170
|
if (o.isSidechain) s.sub = true;
|
|
149
171
|
s.cwd = s.cwd || o.cwd || null;
|
|
172
|
+
/* Started by a script or the Agent SDK (claude -p, an app built on it),
|
|
173
|
+
not typed by the person: kept apart from their own sessions. */
|
|
174
|
+
if (typeof o.entrypoint === "string") { s.via = s.via || o.entrypoint; if (/^sdk/.test(o.entrypoint)) s.how = "script"; }
|
|
150
175
|
const m = o.message && typeof o.message === "object" ? o.message : {};
|
|
151
176
|
if (o.type === "user") {
|
|
152
177
|
const c = m.content;
|
|
153
178
|
let text = typeof c === "string" ? c : Array.isArray(c) && !c.some((x) => x && x.type === "tool_result") ? c.filter((x) => x && x.type === "text").map((x) => x.text).join("\n") : null;
|
|
154
179
|
if (text && text.trim().startsWith("[Request interrupted")) s.aborts++;
|
|
155
180
|
if (o.isMeta) continue;
|
|
181
|
+
/* Messages another agent or a background task put in the user's turn. */
|
|
182
|
+
if (o.origin && typeof o.origin === "object" && o.origin.kind && o.origin.kind !== "human") continue;
|
|
156
183
|
const u = cleanPrompt(text);
|
|
157
|
-
if (u) { const f = promptFacts(ts, u); keepExample(s, f, u);
|
|
184
|
+
if (u) { const f = promptFacts(ts, u); keepExample(s, f, u); addPrompt(f); s.ids.push(o.uuid || null); waiting = true; }
|
|
158
185
|
} else if (o.type === "assistant") {
|
|
186
|
+
if (ts) agentTs = ts;
|
|
159
187
|
if (m.id && !seenMsg.has(m.id)) {
|
|
160
188
|
seenMsg.add(m.id);
|
|
161
|
-
|
|
189
|
+
/* One count per prompt the person sent: the model that answered it. */
|
|
190
|
+
if (waiting && m.model && m.model !== "<synthetic>") { s.models[m.model] = (s.models[m.model] || 0) + 1; waiting = false; }
|
|
162
191
|
const u = m.usage || {};
|
|
163
192
|
s.tokens += (u.input_tokens || 0) + (u.output_tokens || 0) + (u.cache_read_input_tokens || 0) + (u.cache_creation_input_tokens || 0);
|
|
164
193
|
}
|
|
165
194
|
for (const x of m.content || []) {
|
|
166
195
|
if (!x || x.type !== "tool_use") continue;
|
|
167
|
-
|
|
168
|
-
|
|
196
|
+
const inp = x.input || {};
|
|
197
|
+
if (["Edit", "Write", "MultiEdit", "NotebookEdit"].includes(x.name)) { s.patches++; addExt(s, inp.file_path || inp.notebook_path); }
|
|
198
|
+
else if (x.name === "Bash") { s.cmds++; shellFacts(s, inp.command || ""); }
|
|
199
|
+
else if (x.name === "Skill" && typeof inp.skill === "string") tally(s.skills, inp.skill.replace(/^.*:/, ""));
|
|
200
|
+
else if (/^mcp__/.test(x.name)) tally(s.mcp, x.name.split("__")[1]);
|
|
169
201
|
}
|
|
170
202
|
}
|
|
171
203
|
} else {
|
|
172
204
|
const p = o.payload || {};
|
|
173
205
|
if (o.type === "session_meta") {
|
|
174
206
|
s.cwd = p.cwd || null;
|
|
207
|
+
s.via = typeof p.source === "string" ? p.source : p.source && p.source.subagent ? "subagent" : null;
|
|
175
208
|
if ((p.source && typeof p.source === "object" && p.source.subagent) || p.parent_thread_id) s.sub = true;
|
|
209
|
+
/* codex exec, the Codex SDK and MCP runs are started by a program. */
|
|
210
|
+
if (p.source === "exec" || p.source === "mcp" || /sdk|exec/i.test(p.originator || "")) s.how = "script";
|
|
211
|
+
if (p.id) s.id = p.id;
|
|
212
|
+
const pm = JSON.stringify(p.source || {}).match(/"parent_thread_id":"([^"]+)"/);
|
|
213
|
+
s.parent = p.parent_thread_id || (pm && pm[1]) || null;
|
|
176
214
|
} else if (o.type === "turn_context") {
|
|
177
|
-
if (p.model)
|
|
215
|
+
if (p.model) curModel = p.model;
|
|
178
216
|
} else if (o.type === "event_msg" && p.type === "turn_aborted") s.aborts++;
|
|
179
217
|
else if (o.type === "event_msg" && p.type === "token_count") {
|
|
180
218
|
const t = p.info && p.info.total_token_usage && p.info.total_token_usage.total_tokens;
|
|
181
219
|
if (t) s.tokens = Math.max(s.tokens, t);
|
|
220
|
+
if (ts) agentTs = ts;
|
|
182
221
|
}
|
|
183
222
|
const ut = codexUserText(p);
|
|
184
223
|
if (ut !== null && (o.type === "event_msg" || o.type === "response_item")) {
|
|
185
224
|
const u = cleanPrompt(ut);
|
|
186
|
-
|
|
225
|
+
/* Codex logs each message twice (as the item and as the event), a
|
|
226
|
+
moment apart. The same words said again later still count. */
|
|
227
|
+
const t0 = Date.parse(ts || "") || 0, prev = u ? seenText.get(u) : undefined;
|
|
228
|
+
if (u && (prev === undefined || Math.abs(t0 - prev) > 5000)) { seenText.set(u, t0); const f = promptFacts(ts, u); keepExample(s, f, u); addPrompt(f); s.ids.push(hashText(u) + "|" + (ts || "").slice(0, 19)); if (curModel) s.models[curModel] = (s.models[curModel] || 0) + 1; }
|
|
187
229
|
}
|
|
188
230
|
if (o.type === "response_item" && ["function_call", "custom_tool_call", "local_shell_call"].includes(p.type)) {
|
|
189
231
|
const name = p.name || "";
|
|
190
232
|
let arg = p.arguments || p.input || (p.action ? JSON.stringify(p.action) : "");
|
|
191
233
|
if (typeof arg !== "string") arg = JSON.stringify(arg);
|
|
192
|
-
if (
|
|
193
|
-
|
|
234
|
+
if (ts) agentTs = ts;
|
|
235
|
+
if (name === "apply_patch" || arg.includes("*** Begin Patch")) { s.patches++; for (const mm of arg.matchAll(/\*\*\* (?:Update|Add) File: ([^\n\\"]+)/g)) addExt(s, mm[1]); }
|
|
236
|
+
else if (/^mcp__/.test(name)) tally(s.mcp, name.split("__")[1]);
|
|
237
|
+
else { s.cmds++; shellFacts(s, arg); }
|
|
194
238
|
}
|
|
195
239
|
}
|
|
196
240
|
}
|
|
241
|
+
closeLast();
|
|
197
242
|
return s;
|
|
198
243
|
}
|
|
244
|
+
/* What a session touched, as labels only: a file's extension (or a few
|
|
245
|
+
telling file names), whether a command ran tests, committed or opened a pull
|
|
246
|
+
request, and which tools (MCP servers, skills) were used. */
|
|
247
|
+
function tally(o, k) { if (k) o[k] = (o[k] || 0) + 1; }
|
|
248
|
+
function addExt(s, f) {
|
|
249
|
+
if (typeof f !== "string" || !f) return;
|
|
250
|
+
const b = f.split(/[\/\\]/).pop().toLowerCase();
|
|
251
|
+
const k = /^dockerfile|^docker-compose/.test(b) ? "docker" : b === "package.json" ? "package.json" : /\.github\/workflows\//.test(f) ? "ci" : (b.match(/\.([a-z0-9]{1,8})$/) || [])[1];
|
|
252
|
+
tally(s.exts, k);
|
|
253
|
+
}
|
|
254
|
+
function shellFacts(s, c) {
|
|
255
|
+
if (TEST.test(c)) s.tests++;
|
|
256
|
+
if (/\bgit\s+(?:-C\s+\S+\s+)?commit\b/.test(c)) s.commits++;
|
|
257
|
+
if (/\bgh\s+pr\s+create\b/.test(c)) s.prs++;
|
|
258
|
+
}
|
|
199
259
|
/* Keeps the best three example prompts of a file, secrets hidden. */
|
|
200
260
|
function keepExample(s, f, text) {
|
|
201
261
|
if (!KEEP_EXAMPLES) return;
|
|
@@ -235,15 +295,20 @@ function modelName(id) {
|
|
|
235
295
|
const pretty = m.replace(/^claude-/, "Claude ").replace(/-(\d+)-(\d+)(-\d{8})?$/, " $1.$2").replace(/-(\d+)(-\d{8})?$/, " $1").replace(/^gpt/, "GPT");
|
|
236
296
|
return pretty.replace(/\b(opus|sonnet|haiku|fable)\b/g, (w) => w[0].toUpperCase() + w.slice(1));
|
|
237
297
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
298
|
+
/* Only sessions the person started count: sub-agents and runs started by a
|
|
299
|
+
script or an SDK are left out of every number (the counts of them are kept
|
|
300
|
+
so --explain can show what was left out). { all: true } counts them too,
|
|
301
|
+
the way versions before 0.4.0 did; --explain uses it to compare. */
|
|
302
|
+
const MAX_AT_ONCE = 24;
|
|
303
|
+
function compute(summaries, opts = {}) {
|
|
304
|
+
const seen = new Set(), rank = (s) => (s.sub ? 2 : s.how === "script" ? 1 : 0);
|
|
305
|
+
/* The same prompt in two files keeps its place in the person's own session. */
|
|
306
|
+
for (const s of [...summaries].sort((a, b) => rank(a) - rank(b))) {
|
|
242
307
|
const keep = [];
|
|
243
308
|
s.prompts.forEach((p, i) => { const k = s.ids[i] || `${p.ts}|${p.words}`; if (!seen.has(k)) { seen.add(k); keep.push(p); } });
|
|
244
309
|
s.prompts = keep;
|
|
245
310
|
}
|
|
246
|
-
const main = summaries.filter((s) => !s.sub && s.prompts.length);
|
|
311
|
+
const main = summaries.filter((s) => !s.sub && (opts.all || s.how !== "script") && s.prompts.length);
|
|
247
312
|
const P = [];
|
|
248
313
|
main.forEach((s, si) => { [...s.prompts].sort((a, b) => (a.ts || "").localeCompare(b.ts || "")).forEach((p, i) => { if (p.ts) P.push({ ...p, d: new Date(p.ts), si, i }); }); });
|
|
249
314
|
if (!P.length) return null;
|
|
@@ -262,13 +327,30 @@ function compute(summaries) {
|
|
|
262
327
|
const nf = follows.length || 1;
|
|
263
328
|
const verifySessions = main.filter((s) => s.prompts.some((p) => p.verify)).length;
|
|
264
329
|
const edit = main.filter((s) => s.patches > 0), editChecked = edit.filter((s) => s.tests > 0).length;
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
330
|
+
const peak = peakAtOnce(main);
|
|
331
|
+
/* More than a person could run by hand means something else started them:
|
|
332
|
+
say nothing rather than a wrong number. */
|
|
333
|
+
const atOnce = opts.all || peak.max <= MAX_AT_ONCE ? peak.max : 0;
|
|
334
|
+
/* Everything the agents did for you: your sessions and the sub-agents they
|
|
335
|
+
started (not the sub-agents of scripted runs). */
|
|
336
|
+
const scriptIds = new Set(summaries.filter((s) => !s.sub && s.how === "script").map((s) => s.id));
|
|
337
|
+
const work = opts.all ? summaries : summaries.filter((s) => (s.sub ? !scriptIds.has(s.parent) : s.how !== "script"));
|
|
338
|
+
const sum = (arr, f) => arr.reduce((a, s) => a + f(s), 0);
|
|
339
|
+
const merged = (k) => { const o = {}; work.forEach((s) => { for (const [x, c] of Object.entries(s[k] || {})) o[x] = (o[x] || 0) + c; }); return o; };
|
|
340
|
+
const exts = merged("exts"), edits = Object.values(exts).reduce((a, b) => a + b, 0);
|
|
341
|
+
const shares = (o, n, skip = []) => { const t = Object.entries(o).filter(([k]) => !skip.includes(k)); const tot = t.reduce((a, [, c]) => a + c, 0) || 1; return t.sort((a, b) => b[1] - a[1]).slice(0, n).map(([name, c]) => ({ name, share: c / tot })); };
|
|
342
|
+
const byLabel = (map) => { const o = {}; for (const [e, c] of Object.entries(exts)) { const l = map[e]; if (l) o[l] = (o[l] || 0) + c; } return o; };
|
|
343
|
+
const stack = edits >= 20 ? shares(byLabel(LANG), 5, ["Config", "Markdown"]) : [];
|
|
344
|
+
const areas = edits >= 20 ? shares(byLabel(AREA), 3) : [];
|
|
345
|
+
const wk = {}; P.forEach((p) => { if (p.work) wk[p.work] = (wk[p.work] || 0) + 1; });
|
|
346
|
+
const workMix = Object.values(wk).reduce((a, b) => a + b, 0) >= 20 ? shares(wk, 6).map((x) => ({ key: x.name, share: x.share })) : [];
|
|
347
|
+
const clean = (o, n) => shares(Object.fromEntries(Object.entries(o).filter(([k]) => /^[A-Za-z][\w .-]{1,39}$/.test(k) && !/^ccd_/.test(k))), n).map((x) => x.name);
|
|
348
|
+
const children = new Set(summaries.filter((s) => s.sub && s.parent).map((s) => s.parent));
|
|
349
|
+
const dayList = [...new Set(P.map((p) => p.d.toISOString().slice(0, 10)))].sort();
|
|
350
|
+
let streak = dayList.length ? 1 : 0, run = 1;
|
|
351
|
+
for (let i = 1; i < dayList.length; i++) { run = Date.parse(dayList[i]) - Date.parse(dayList[i - 1]) === 864e5 ? run + 1 : 1; streak = Math.max(streak, run); }
|
|
352
|
+
/* Longest focused stretch: one session worked on with no gap over 20 minutes. */
|
|
353
|
+
const longest = Math.max(0, ...main.map((s) => { let best = 0, a = 0, b = 0; s.prompts.filter((p) => p.ts).map((p) => [Date.parse(p.ts), Math.min(Date.parse(p.end || p.ts), Date.parse(p.ts) + 3 * 3600e3)]).sort((x, y) => x[0] - y[0]).forEach(([x, y]) => { if (x - b > 20 * 60e3) { a = x; b = y; } else b = Math.max(b, y); best = Math.max(best, b - a); }); return best / 60e3; }));
|
|
272
354
|
const shorts = {}; P.forEach((p) => { if (p.short) shorts[p.short] = (shorts[p.short] || 0) + 1; });
|
|
273
355
|
const goto = Object.entries(shorts).filter(([, c]) => c >= 5).sort((a, b) => b[1] - a[1])[0] || null;
|
|
274
356
|
const days = new Set(P.map((p) => p.d.toDateString())).size;
|
|
@@ -292,26 +374,56 @@ function compute(summaries) {
|
|
|
292
374
|
version: VERSION,
|
|
293
375
|
generated: new Date().toISOString().slice(0, 10),
|
|
294
376
|
window: { first: first.toISOString().slice(0, 10), last: last.toISOString().slice(0, 10), activeDays: days },
|
|
295
|
-
sessions: { total: main.length, claudeCode: main.filter((s) => s.src === "claude").length, codex: main.filter((s) => s.src === "codex").length, subagentThreads: summaries.filter((s) => s.sub).length },
|
|
377
|
+
sessions: { total: main.length, claudeCode: main.filter((s) => s.src === "claude").length, codex: main.filter((s) => s.src === "codex").length, subagentThreads: summaries.filter((s) => s.sub).length, scripted: summaries.filter((s) => !s.sub && s.how === "script" && s.prompts.length).length },
|
|
296
378
|
prompts: { total: P.length, medianWords: median(P.map((p) => p.words)), firstPromptMedianWords: median(firsts), followupMedianWords: median(follows.map((p) => p.words)) },
|
|
297
379
|
followups: { total: follows.length, question: mix.question / nf, correction: mix.correct / nf, goAhead: mix.go / nf, instruction: mix.direct / nf },
|
|
298
380
|
proof: { sessionsAskedToVerify: verifySessions, codingSessions: edit.length, codingSessionsWithTests: editChecked },
|
|
299
|
-
parallelMax:
|
|
381
|
+
parallelMax: atOnce,
|
|
382
|
+
atOnceAt: atOnce ? peak.at : null,
|
|
300
383
|
hours, busiestHour: busiest, weekendShare: P.filter((p) => [0, 6].includes(p.d.getDay())).length / P.length,
|
|
301
384
|
months: months.map((m) => ({ month: m.key, label: m.label, prompts: m.n })), busiestMonth: busiestMonth.key,
|
|
302
385
|
models: models.slice(0, 5), anthropicShare: anthropic,
|
|
303
386
|
projects, stops: main.reduce((a, s) => a + s.aborts, 0),
|
|
304
387
|
goToPrompt: goto ? { text: goto[0], count: goto[1] } : null,
|
|
305
388
|
dontKnow: P.filter((p) => p.dontknow).length,
|
|
306
|
-
tokens: summaries.reduce((a, s) => a + s.tokens, 0),
|
|
389
|
+
tokens: summaries.filter((s) => opts.all || s.sub || s.how !== "script").reduce((a, s) => a + s.tokens, 0),
|
|
307
390
|
promptProfile,
|
|
391
|
+
stack, areas, workMix,
|
|
392
|
+
shipped: { commits: sum(work, (s) => s.commits || 0), prs: sum(work, (s) => s.prs || 0) },
|
|
393
|
+
automations: { runs: scriptIds.size && !opts.all ? summaries.filter((s) => !s.sub && s.how === "script" && s.prompts.length).length : 0, projects: new Set(summaries.filter((s) => !s.sub && s.how === "script" && s.cwd).map((s) => s.cwd)).size },
|
|
394
|
+
subagents: { threads: work.filter((s) => s.sub).length, sessionsUsing: main.filter((s) => children.has(s.id)).length },
|
|
395
|
+
actionsPerPrompt: P.length ? Math.round(sum(work, (s) => s.cmds + s.patches) / P.length * 10) / 10 : 0,
|
|
396
|
+
tools: { mcp: clean(merged("mcp"), 6), skills: clean(merged("skills"), 5) },
|
|
397
|
+
style: { longestStretchMin: Math.round(longest), streakDays: streak, nightShare: P.filter((p) => p.d.getHours() >= 22 || p.d.getHours() < 5).length / P.length, earlyShare: P.filter((p) => p.d.getHours() >= 5 && p.d.getHours() < 9).length / P.length },
|
|
308
398
|
};
|
|
309
399
|
}
|
|
400
|
+
/* The most agents at work at the same moment. A prompt keeps its session busy
|
|
401
|
+
from when it was sent until the agent's last step before the next prompt
|
|
402
|
+
(at most three hours; a minute if the agent never answered). */
|
|
403
|
+
function peakAtOnce(sessions) {
|
|
404
|
+
const ev = [], spans = new Map();
|
|
405
|
+
for (const s of sessions) {
|
|
406
|
+
const iv = s.prompts.filter((p) => p.ts).map((p) => { const a = Date.parse(p.ts); let b = p.end ? Date.parse(p.end) : a + 60e3; if (!(b > a)) b = a + 60e3; return [a, Math.min(b, a + 3 * 3600e3)]; }).sort((x, y) => x[0] - y[0]);
|
|
407
|
+
const mg = []; for (const [a, b] of iv) { const l = mg[mg.length - 1]; if (l && a <= l[1]) l[1] = Math.max(l[1], b); else mg.push([a, b]); }
|
|
408
|
+
spans.set(s, mg); for (const [a, b] of mg) ev.push([a, 1], [b, -1]);
|
|
409
|
+
}
|
|
410
|
+
ev.sort((x, y) => x[0] - y[0] || x[1] - y[1]);
|
|
411
|
+
let cur = 0, max = 0, at = 0; for (const [t, d] of ev) { cur += d; if (cur > max) { max = cur; at = t; } }
|
|
412
|
+
const active = sessions.filter((s) => spans.get(s).some(([a, b]) => a <= at && at < b));
|
|
413
|
+
const d = new Date(at), two = (n) => String(n).padStart(2, "0");
|
|
414
|
+
return { max, at: max ? `${d.getFullYear()}-${two(d.getMonth() + 1)}-${two(d.getDate())}T${two(d.getHours())}:${two(d.getMinutes())}` : null, active };
|
|
415
|
+
}
|
|
416
|
+
/* File types, as languages and as the kind of thing being built. */
|
|
417
|
+
const LANG = {}, AREA = {};
|
|
418
|
+
[["TypeScript", "ts tsx mts cts"], ["JavaScript", "js jsx mjs cjs"], ["Python", "py"], ["Jupyter", "ipynb"], ["Go", "go"], ["Rust", "rs"], ["Java", "java"], ["Kotlin", "kt kts"], ["Swift", "swift"], ["Ruby", "rb"], ["PHP", "php"], ["C#", "cs"], ["C++", "cpp cc cxx hpp hh"], ["C", "c h"], ["SQL", "sql"], ["CSS", "css scss sass less"], ["HTML", "html htm"], ["Astro", "astro"], ["Vue", "vue"], ["Svelte", "svelte"], ["Dart", "dart"], ["Shell", "sh bash zsh"], ["Markdown", "md mdx"], ["Elixir", "ex exs"], ["Scala", "scala"], ["R", "r"], ["Lua", "lua"], ["Terraform", "tf"], ["Config", "json yaml yml toml ini env docker ci package.json"]]
|
|
419
|
+
.forEach(([l, e]) => e.split(" ").forEach((x) => { LANG[x] = l; }));
|
|
420
|
+
[["Front end", "tsx jsx vue svelte astro css scss sass less html htm"], ["Back end", "ts mts cts js mjs cjs py go rs java rb php cs ex exs scala sql cpp cc c h"], ["Data & ML", "ipynb r"], ["Infra & DevOps", "tf docker ci sh bash zsh yaml yml toml"], ["Mobile", "swift kt kts dart"], ["Docs & writing", "md mdx txt"]]
|
|
421
|
+
.forEach(([l, e]) => e.split(" ").forEach((x) => { AREA[x] = l; }));
|
|
310
422
|
/* The strongest example prompts across all sessions: no repeats, at most two
|
|
311
423
|
from any one project, best first. They stay on this computer unless you
|
|
312
424
|
choose some for your profile. */
|
|
313
425
|
function pickExamples(summaries, n = 12) {
|
|
314
|
-
const all = summaries.filter((s) => !s.sub).flatMap((s) => (s.examples || []).map((e) => ({ ...e, cwd: s.cwd || "" })));
|
|
426
|
+
const all = summaries.filter((s) => !s.sub && s.how !== "script").flatMap((s) => (s.examples || []).map((e) => ({ ...e, cwd: s.cwd || "" })));
|
|
315
427
|
all.sort((a, b) => b.q - a.q);
|
|
316
428
|
const seen = new Set(), per = {}, out = [];
|
|
317
429
|
for (const e of all) {
|
|
@@ -324,11 +436,14 @@ function pickExamples(summaries, n = 12) {
|
|
|
324
436
|
return out;
|
|
325
437
|
}
|
|
326
438
|
|
|
439
|
+
/* @shared:start ---------------------------------------------------------
|
|
440
|
+
From here to @shared:end is also the site's netlify/lib/cards-page.mjs
|
|
441
|
+
(the profile shows the same cards): node site/scripts/sync-cards.mjs */
|
|
327
442
|
/* Builder types. Each signal is scored against the level where it becomes
|
|
328
443
|
notable; the highest score wins, and below 1 everywhere you are a Pair
|
|
329
444
|
Programmer. The rules are public so anyone can check their own. */
|
|
330
445
|
const TYPES = {
|
|
331
|
-
conductor: { name: "The Conductor", line: "
|
|
446
|
+
conductor: { name: "The Conductor", line: "Many sessions going in parallel. Long briefs, then you let them run." },
|
|
332
447
|
verifier: { name: "The Verifier", line: "Nothing ships until the agent has shown it works." },
|
|
333
448
|
architect: { name: "The Architect", line: "You brief first and in detail. The agent gets the plan before the work." },
|
|
334
449
|
interrogator: { name: "The Interrogator", line: "You ask more than you tell. Questions steer the work." },
|
|
@@ -339,7 +454,7 @@ function builderType(m) {
|
|
|
339
454
|
const verify = m.sessions.total ? m.proof.sessionsAskedToVerify / m.sessions.total : 0;
|
|
340
455
|
const tested = m.proof.codingSessions ? m.proof.codingSessionsWithTests / m.proof.codingSessions : 0;
|
|
341
456
|
const scores = {
|
|
342
|
-
conductor: m.parallelMax / 6,
|
|
457
|
+
conductor: (m.parallelMax || 0) / (m.atOnceAt !== undefined ? 4 : 6),
|
|
343
458
|
verifier: Math.max(verify / 0.45, tested / 0.75),
|
|
344
459
|
architect: m.prompts.firstPromptMedianWords / 80,
|
|
345
460
|
interrogator: m.followups.question / 0.3,
|
|
@@ -354,8 +469,39 @@ const compact = (n) => n >= 1e9 ? (n / 1e9).toFixed(1).replace(/\.0$/, "") + "B"
|
|
|
354
469
|
/* Dots for a habit: reaching the notable level fills 7 of 10, the last 3 are
|
|
355
470
|
for going beyond it. */
|
|
356
471
|
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)));
|
|
472
|
+
/* The Hour score, 0 to 100 (beta). Each part is scored the same way as the
|
|
473
|
+
habit bars: reaching the "good" level gives 70, the rest is for going past
|
|
474
|
+
it. The score is the plain average of the parts. Needs at least 4 parts. */
|
|
475
|
+
function hourScore(m) {
|
|
476
|
+
const parts = (m.promptProfile || []).map((h) => ({ key: h.key, label: h.label, points: Math.round(100 * habitFill(h)) }));
|
|
477
|
+
if (m.proof && m.proof.codingSessions >= 5) parts.push({ key: "tests", label: "Running tests", points: Math.round(100 * habitFill({ value: m.proof.codingSessionsWithTests / m.proof.codingSessions, notable: 0.75 })) });
|
|
478
|
+
if (m.atOnceAt && m.parallelMax >= 1) parts.push({ key: "parallel", label: "Agents at once", points: Math.round(100 * Math.min(1, 0.7 * Math.min(1, m.parallelMax / 4) + 0.3 * Math.max(0, (m.parallelMax - 4) / 8))) });
|
|
479
|
+
if (parts.length < 4) return null;
|
|
480
|
+
return { total: Math.round(parts.reduce((a, x) => a + x.points, 0) / parts.length), parts };
|
|
481
|
+
}
|
|
482
|
+
const WORK_LABEL = { build: ["Building new things", "building new things"], fix: ["Fixing bugs", "fixing bugs"], understand: ["Understanding code", "understanding code"], write: ["Writing and docs", "writing and docs"], tidy: ["Cleaning up code", "cleaning up code"], test: ["Writing tests", "writing tests"] };
|
|
483
|
+
const atOnceWhen = (at) => { if (!at) return ""; const d = new Date(at + ":00Z"); return `${at.slice(11, 16)} on ${d.getUTCDate()} ${d.toLocaleString("en", { month: "short", timeZone: "UTC" })}`; };
|
|
484
|
+
/* The headlines: the four strongest things about how someone works, in plain
|
|
485
|
+
sentences, built only from the numbers. who: null for "you", else a name. */
|
|
486
|
+
function headlines(m, who) {
|
|
487
|
+
const Y = who || "You", v = (you, they) => (who ? they : you), yr = who ? who + "'s" : "Your", H = [];
|
|
488
|
+
const add = (w, t) => H.push([w, t]);
|
|
489
|
+
const hp = (k) => (m.promptProfile || []).find((h) => h.key === k);
|
|
490
|
+
const proof = m.sessions.total ? m.proof.sessionsAskedToVerify / m.sessions.total : 0;
|
|
491
|
+
if (m.prompts.firstPromptMedianWords >= 80) add(m.prompts.firstPromptMedianWords / 80, `${Y} ${v("write", "writes")} detailed briefs: a typical first prompt is ${Math.round(m.prompts.firstPromptMedianWords)} words.`);
|
|
492
|
+
if (proof >= 0.45) add(proof / 0.45, `${Y} ${v("make", "makes")} the agent prove its work in ${pct(proof)}% of sessions.`);
|
|
493
|
+
if (m.atOnceAt && m.parallelMax >= 3) add(m.parallelMax / 3, `${Y} ${v("run", "runs")} up to ${m.parallelMax} agents at the same time.`);
|
|
494
|
+
if (m.stack && m.stack.length) add(1, `${Y} ${v("work", "works")} mostly in ${m.stack[0].name}${m.stack[1] ? " and " + m.stack[1].name : ""}.`);
|
|
495
|
+
if (m.shipped && m.shipped.commits >= 20) add(Math.min(1.6, m.shipped.commits / 100), `${yr} agents made ${m.shipped.commits.toLocaleString("en")} commits${m.shipped.prs ? ` and opened ${m.shipped.prs} pull requests` : ""}.`);
|
|
496
|
+
if (m.automations && m.automations.runs >= 10) add(Math.min(1.5, m.automations.runs / 20), `${Y} ${v("build", "builds")} with agents too: ${m.automations.runs} runs started by ${v("your", "their")} own code.`);
|
|
497
|
+
const st = hp("steps"); if (st && st.value >= st.notable) add(st.value / st.notable, `${Y} ${v("break", "breaks")} big tasks into steps.`);
|
|
498
|
+
if (m.followups.question >= 0.3) add(m.followups.question / 0.3, `${Y} ${v("steer", "steers")} by asking questions.`);
|
|
499
|
+
const rc = hp("recover"); if (rc && rc.value >= rc.notable) add(rc.value / rc.notable, `${Y} ${v("say", "says")} exactly what's wrong when correcting the agent.`);
|
|
500
|
+
if (m.workMix && m.workMix[0]) add(0.9, `Most prompts are about ${WORK_LABEL[m.workMix[0].key][1]}.`);
|
|
501
|
+
return H.sort((a, b) => b[0] - a[0]).slice(0, 4).map((x) => x[1]);
|
|
502
|
+
}
|
|
357
503
|
function cards(m, name) {
|
|
358
|
-
const
|
|
504
|
+
const tk = builderType(m), type = TYPES[tk];
|
|
359
505
|
const tools = [m.sessions.claudeCode && "Claude Code", m.sessions.codex && "Codex"].filter(Boolean).join(" and ");
|
|
360
506
|
const verify = m.sessions.total ? m.proof.sessionsAskedToVerify / m.sessions.total : 0;
|
|
361
507
|
const tested = m.proof.codingSessions ? m.proof.codingSessionsWithTests / m.proof.codingSessions : 0;
|
|
@@ -363,14 +509,22 @@ function cards(m, name) {
|
|
|
363
509
|
const busy = m.months.find((x) => x.month === m.busiestMonth);
|
|
364
510
|
const C = [];
|
|
365
511
|
C.push({ id: "intro", bg: "#0B0B0A", fg: "#F4F1EA", dot: "#F4F1EA", pat: "months", ask: "Your year with AI", big: m.sessions.total.toLocaleString("en"), line: `sessions in ${tools}. ${m.prompts.total.toLocaleString("en")} prompts. Busiest month: ${busy ? busy.label : ""}.`, bs: m.sessions.total >= 1000 ? "26cqw" : "32cqw" });
|
|
366
|
-
C.push({ id: "type", bg: "#
|
|
512
|
+
C.push({ id: "type", bg: "#0B0B0A", fg: "#F4F1EA", av: tk, ask: "Your builder type", big: type.name, line: type.line, bs: "15cqw", grad: true });
|
|
513
|
+
const sc = hourScore(m);
|
|
514
|
+
if (sc) C.push({ id: "score", bg: "#F1ECE3", fg: "#0B0B0A", rc: "#8B5FD9", rows: sc.parts.map((x) => [x.label, x.points / 100, String(x.points)]), ask: "Your Hour score", big: String(sc.total), line: `out of 100: your average across ${sc.parts.length} habits.`, bs: "18cqw" });
|
|
367
515
|
if (m.goToPrompt) C.push({ id: "prompt", bg: "#DBA004", fg: "#0B0B0A", dot: "#0B0B0A", pat: "rain", ask: "Your go-to prompt", big: `“${m.goToPrompt.text}”`, line: `You said it ${m.goToPrompt.count} times.`, bs: m.goToPrompt.text.length > 12 ? "14cqw" : "20cqw" });
|
|
368
516
|
const hs = [...m.hours].map((v, h) => [h, v]).sort((a, b) => b[1] - a[1]);
|
|
369
517
|
C.push({ id: "hour", bg: "#2B5FE3", fg: "#FFFFFF", dot: "#FFFFFF", pat: "hours", ask: "Your busiest hour", big: hourLabel(m.busiestHour), line: `${hourLabel(hs[1][0])} is a close second.`, bs: "26cqw" });
|
|
370
|
-
if (m.parallelMax >= 2) C.push({ id: "parallel", bg: "#F2601F", fg: "#0B0B0A", dot: "#0B0B0A", pat: "lanes", n: Math.min(13, m.parallelMax), ask: "
|
|
518
|
+
if (m.parallelMax >= 2) C.push({ id: "parallel", bg: "#F2601F", fg: "#0B0B0A", dot: "#0B0B0A", pat: "lanes", n: Math.min(13, m.parallelMax), ask: "Agents at once", big: String(m.parallelMax), line: m.atOnceAt ? `At ${atOnceWhen(m.atOnceAt)}, ${m.parallelMax} agents were working on your tasks at the same time.` : "sessions going inside twenty minutes.", bs: "34cqw" });
|
|
371
519
|
C.push({ id: "proof", bg: "#8B5FD9", fg: "#FFFFFF", dot: "#FFFFFF", pat: "ring", pct: verify, ask: "How often you made the agent prove it", big: `${pct(verify)}%`, line: "of sessions, you asked it to test, verify or screenshot.", bs: "30cqw" });
|
|
372
520
|
C.push({ id: "steer", bg: "#F1ECE3", fg: "#0B0B0A", dot: "#8B5FD9", pat: "share", frac: m.followups.question, ask: "How you steer", big: `${pct(m.followups.question)}%`, line: `of your follow-ups are questions. ${pct(m.followups.correction)}% are corrections.`, bs: "30cqw" });
|
|
373
|
-
if (
|
|
521
|
+
if (m.stack && m.stack.length >= 2) C.push({ id: "stack", bg: "#F7F3EC", fg: "#0B0B0A", rc: "#2B5FE3", rows: m.stack.map((x) => [x.name, x.share, pct(x.share) + "%"]), ask: "Your stack", big: m.stack[0].name, line: m.areas && m.areas.length ? `By files edited. Mostly ${m.areas[0].name.toLowerCase()}${m.areas[1] ? ", then " + m.areas[1].name.toLowerCase() : ""}.` : "By files your agents edited.", bs: m.stack[0].name.length > 8 ? "12cqw" : "16cqw" });
|
|
522
|
+
if (m.workMix && m.workMix.length >= 3) C.push({ id: "work", bg: "#F7F3EC", fg: "#0B0B0A", rc: "#F2601F", rows: m.workMix.slice(0, 5).map((x) => [WORK_LABEL[x.key][0], x.share, pct(x.share) + "%"]), ask: "What you spend it on", big: WORK_LABEL[m.workMix[0].key][0], line: "Judged from the words in your prompts.", bs: "10cqw" });
|
|
523
|
+
if (m.shipped && m.shipped.commits >= 5) C.push({ id: "ship", bg: "#DBA004", fg: "#0B0B0A", dot: "#0B0B0A", pat: "field", ask: "What you shipped", big: m.shipped.commits.toLocaleString("en"), line: `commits${m.shipped.prs ? ` and ${m.shipped.prs} pull requests` : ""}, made by your agents.`, bs: m.shipped.commits >= 1000 ? "26cqw" : "32cqw" });
|
|
524
|
+
if (m.actionsPerPrompt >= 2) C.push({ id: "actions", bg: "#FB65C2", fg: "#0B0B0A", dot: "#0B0B0A", pat: "rain", ask: "Actions per prompt", big: String(m.actionsPerPrompt), line: `agent actions per prompt: commands and file edits.${m.stops ? ` You stopped the agent ${m.stops} times.` : ""}`, bs: "30cqw" });
|
|
525
|
+
if (m.automations && (m.automations.runs >= 5 || (m.subagents && m.subagents.sessionsUsing >= 5))) { const su = m.subagents && m.sessions.total ? m.subagents.sessionsUsing / m.sessions.total : 0; C.push(m.automations.runs >= 5 ? { id: "auto", bg: "#0B0B0A", fg: "#F4F1EA", dot: "#F2601F", pat: "lanes", n: 9, ask: "Built with agents", big: String(m.automations.runs), line: `runs started by your own code, not typed.${su >= 0.05 ? ` Your agents handed work to helpers in ${pct(su)}% of sessions.` : ""}`, bs: "30cqw" } : { id: "auto", bg: "#0B0B0A", fg: "#F4F1EA", dot: "#F2601F", pat: "lanes", n: 9, ask: "Your agents delegate", big: `${pct(su)}%`, line: "of your sessions handed work to helper agents.", bs: "30cqw" }); }
|
|
526
|
+
if (m.style && m.projects) C.push({ id: "range", bg: "#8B5FD9", fg: "#FFFFFF", dot: "#FFFFFF", pat: "months", ask: "Your range", big: String(m.projects), line: `projects, ${m.window.activeDays} active days, a ${m.style.streakDays}-day streak. Longest focused stretch: ${Math.floor(m.style.longestStretchMin / 60)}h ${m.style.longestStretchMin % 60}m.`, bs: "30cqw" });
|
|
527
|
+
if (top) C.push({ id: "model", bg: "#0B0B0A", fg: "#F4F1EA", dot: "#F2601F", pat: "stripes", shares: m.models.map((x) => x.share), ask: "Your model", big: top.name, line: `${pct(top.share)}% of your prompts.` + (m.anthropicShare > 0 && m.anthropicShare < 1 ? ` ${pct(m.anthropicShare)}% Anthropic overall.` : ""), bs: top.name.length > 11 ? "11.5cqw" : "16cqw" });
|
|
374
528
|
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" });
|
|
375
529
|
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" });
|
|
376
530
|
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" });
|
|
@@ -378,8 +532,8 @@ function cards(m, name) {
|
|
|
378
532
|
const hp = m.promptProfile, best = [...hp].sort((a, b) => b.value / b.notable - a.value / a.notable)[0];
|
|
379
533
|
C.push({ id: "prompting", bg: "#F7F3EC", fg: "#0B0B0A", bs: "8.6cqw", rows: hp.map((h) => [h.label, habitFill(h), `${pct(h.value)}%`]), ask: "How you prompt", big: best.label, line: `Your strongest habit.` });
|
|
380
534
|
}
|
|
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,
|
|
382
|
-
stats: [["Sessions", m.sessions.total.toLocaleString("en")], ["
|
|
535
|
+
C.push({ id: "summary", bg: "#0B0B0A", fg: "#F4F1EA", av: tk, summary: true, who: name || "", period: `${monthYear(m.window.first)} – ${monthYear(m.window.last)}`, type: type.name,
|
|
536
|
+
stats: [["Sessions", m.sessions.total.toLocaleString("en")], ["Hour score", sc ? String(sc.total) : "–"], ["Agents at once", m.parallelMax >= 2 ? String(m.parallelMax) : "–"], ["Made it prove it", `${pct(verify)}%`], [m.stack && m.stack.length ? "Stack" : "Busiest hour", m.stack && m.stack.length ? m.stack[0].name : hourLabel(m.busiestHour)], ["Go-to prompt", m.goToPrompt ? `“${m.goToPrompt.text}”` : "–"]] });
|
|
383
537
|
return C;
|
|
384
538
|
}
|
|
385
539
|
|
|
@@ -406,7 +560,53 @@ const CLAIM_CSS = String.raw`
|
|
|
406
560
|
.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
561
|
.cl-hcard label{position:relative;font-size:13px;opacity:.7}
|
|
408
562
|
.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
|
|
563
|
+
.cl-hrow{align-items:center!important;gap:6px;margin-top:10px!important;padding:6px 8px 6px 14px;border-radius:16px;background:rgba(244,241,234,.08);box-shadow:inset 0 0 0 1.5px rgba(244,241,234,.3);transition:box-shadow .15s,background .15s}
|
|
564
|
+
.cl-hrow:focus-within{background:rgba(244,241,234,.12);box-shadow:inset 0 0 0 2px #B0ACFE}
|
|
565
|
+
.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;caret-color:#FB65C2}
|
|
566
|
+
.cl-edit-h{flex:none;display:inline-flex;align-items:center;gap:6px;height:36px;padding:0 13px;border-radius:999px;border:0;background:#F4F1EA;color:var(--ink);font:600 14px var(--sans);cursor:pointer}.cl-edit-h svg{width:15px;height:15px}
|
|
567
|
+
.cl-hst{position:relative;margin-top:10px;font-size:14px;opacity:.8}
|
|
568
|
+
.claimbox .pill em{font-style:normal;margin-left:10px;padding:2px 8px;border-radius:99px;background:rgba(255,255,255,.18);font:500 12px var(--sans)}
|
|
569
|
+
.pk-row{flex:none;display:grid;gap:12px;margin-top:22px}
|
|
570
|
+
.pk{display:flex;flex-direction:column;border-radius:20px;padding:18px 18px 14px;background:#EDE8DE;color:var(--ink);cursor:pointer;transition:background .35s,color .35s}.pk:hover{background:#E7E1D5}
|
|
571
|
+
.pk-pub{--on:#0B0B0A;--fg:#F4F1EA}.pk-roles{--on:#2B5FE3;--fg:#FFFFFF}.pk-feature{--on:#F2601F;--fg:#0B0B0A}
|
|
572
|
+
.pk.on,.pk.on:hover{background:var(--on);color:var(--fg)}
|
|
573
|
+
.pk-l{font-size:13px;font-weight:500;opacity:.75}
|
|
574
|
+
.pk .art{display:block;width:100%;height:auto;margin:14px 0 0;fill:currentColor;opacity:.28;transition:opacity .35s}.pk.on .art{opacity:1}
|
|
575
|
+
.pk .art circle.l{transform-box:fill-box;transform-origin:center}.pk.on .art circle.l{animation:pk-pop .55s cubic-bezier(.2,.8,.3,1.3) var(--d) both}
|
|
576
|
+
@keyframes pk-pop{from{transform:scale(0)}}
|
|
577
|
+
.pk-b{margin-top:14px;font-size:28px;line-height:1.02;letter-spacing:-.045em;font-weight:650}
|
|
578
|
+
.pk-pub.on .hd{background:linear-gradient(100deg,#4F88FF 0%,#B0ACFE 38%,#FB65C2 72%,#FD2980 100%);-webkit-background-clip:text;background-clip:text;color:transparent}
|
|
579
|
+
.pk-t{flex:1;margin-top:8px;font-size:14px;line-height:1.42;opacity:.85}
|
|
580
|
+
.pk-f{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-top:14px;padding-top:12px;box-shadow:inset 0 1px 0 color-mix(in srgb,currentColor 18%,transparent);font-size:13px;font-weight:600}
|
|
581
|
+
.pk .o1,.pk.on .o0{display:none}.pk.on .o1{display:inline}
|
|
582
|
+
.pk .sw i{background:rgba(11,11,10,.2)}.pk.on .sw i{background:var(--fg)}.pk.on .sw i:after{background:var(--on);box-shadow:none}.pk .sw input:focus-visible+i{outline-color:currentColor}
|
|
583
|
+
.gv{flex:none;display:grid;gap:18px;margin-top:12px;border-radius:20px;padding:16px 18px;box-shadow:inset 0 0 0 1px rgba(11,11,10,.12)}
|
|
584
|
+
.gv-h{display:flex;flex-direction:column;align-items:flex-start}.gv-b{display:block;margin-top:8px;font-size:22px;line-height:1.05;letter-spacing:-.035em;font-weight:650}.gv .cl-link{padding:12px 0 0}
|
|
585
|
+
.gv ul{list-style:none;margin:10px 0 0;padding:0;display:flex;flex-direction:column;gap:7px;font-size:14px;line-height:1.35}
|
|
586
|
+
.gv li{display:flex;gap:9px}.gv li:before{content:"";flex:none;width:8px;height:8px;margin-top:5px;border-radius:50%;background:var(--ink)}.gv .st li:before{background:none;box-shadow:inset 0 0 0 1.5px var(--ink)}
|
|
587
|
+
.gv .gv-pre{grid-column:1/-1;margin:0}
|
|
588
|
+
@media(min-width:900px){.pk-row{grid-template-columns:repeat(3,minmax(0,1fr))}.gv{grid-template-columns:1.15fr 1fr 1fr;padding:18px 24px}.pk-row{margin-top:18px}}
|
|
589
|
+
.pk.fl,.pk.fl:hover,.pk.fl.on{background:none;padding:0;perspective:1400px}
|
|
590
|
+
.pk-flip{position:relative;flex:1;display:grid;transform-style:preserve-3d;transition:transform .8s cubic-bezier(.3,.9,.25,1)}.pk.fl.on .pk-flip{transform:rotateY(180deg)}
|
|
591
|
+
.pk-face{grid-area:1/1;display:flex;flex-direction:column;border-radius:20px;padding:18px 18px 14px;-webkit-backface-visibility:hidden;backface-visibility:hidden;background:#EDE8DE;color:var(--ink);transition:background .2s}
|
|
592
|
+
.pk.fl:hover .pk-face.fr{background:#E7E1D5}.pk-face.fr .pk-t{flex:1}.pk-face.bk{transform:rotateY(180deg);background:var(--on);color:var(--fg)}
|
|
593
|
+
.pk.fl.on .pk-face.fr .sw i{background:rgba(11,11,10,.2)}.pk.fl.on .pk-face.fr .sw i:after{background:#fff}
|
|
594
|
+
.sw.on-v i{background:var(--fg)}.sw.on-v i:after{transform:translateX(20px);background:var(--on);box-shadow:none}
|
|
595
|
+
.pk-l2{display:flex;justify-content:space-between;align-items:center;gap:8px;font-size:13px;font-weight:500}.pk-l2 span{opacity:.8}.pk-l2 em{font-style:normal;font-size:10.5px;font-weight:650;letter-spacing:.06em;text-transform:uppercase;padding:3px 8px;border-radius:99px;box-shadow:inset 0 0 0 1px color-mix(in srgb,currentColor 50%,transparent)}
|
|
596
|
+
.ib{flex:1;display:flex;flex-direction:column;margin-top:12px;border-radius:16px;padding:14px;background:color-mix(in srgb,var(--fg) 13%,transparent)}
|
|
597
|
+
.ib-h{display:flex;align-items:center;gap:7px;font-size:12px;font-weight:600;opacity:.85}.ib-h:before{content:"";width:7px;height:7px;border-radius:50%;background:currentColor;animation:ib-p 1.6s ease-in-out infinite}
|
|
598
|
+
@keyframes ib-p{50%{opacity:.25}}
|
|
599
|
+
.ib-t{margin-top:8px;font-size:19px;line-height:1.1;letter-spacing:-.03em;font-weight:650}
|
|
600
|
+
.ib-s{margin-top:12px;font-size:12px;opacity:.75}.ib-c{display:flex;flex-wrap:wrap;gap:5px;margin-top:6px}.ib-c i{font-style:normal;font-size:12px;font-weight:600;padding:4px 9px;border-radius:99px;background:color-mix(in srgb,var(--fg) 18%,transparent)}
|
|
601
|
+
.ib-a{display:flex;gap:8px;margin-top:auto;padding-top:12px}.ib-a b{flex:1;text-align:center;padding:8px 0;border-radius:99px;background:var(--fg);color:var(--on);font-size:13px}.ib-a b.o{background:none;color:var(--fg);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--fg) 50%,transparent)}
|
|
602
|
+
.fp{flex:1;display:flex;flex-direction:column;margin-top:12px;border-radius:16px;padding:12px;background:#F4F1EA;color:#0B0B0A}
|
|
603
|
+
.fp-h{display:flex;align-items:center;gap:6px;font-size:13px;font-weight:650}.fp-h svg{width:16px;height:16px}.fp-h span{font-weight:400;opacity:.6}
|
|
604
|
+
.fp-b{flex:1;display:flex;gap:12px;align-items:flex-end;margin-top:10px}.pk-thumb{flex:none;width:104px}.pk-thumb .card{border-radius:7px}
|
|
605
|
+
.fp-t{display:flex;flex-direction:column;gap:4px;font-size:12.5px;line-height:1.35;padding-bottom:2px}.fp-t b{font-size:16px;letter-spacing:-.02em}
|
|
606
|
+
.fp-c{display:flex;flex-wrap:wrap;gap:5px;margin-top:10px}.fp-c i{font-style:normal;font-size:12px;font-weight:600;padding:4px 9px;border-radius:99px;background:rgba(11,11,10,.14)}
|
|
607
|
+
@media(prefers-reduced-motion:reduce){.pk-flip{transition:none}.ib-h:before{animation:none}}
|
|
608
|
+
.sw{position:relative;flex:none}.sw input{position:absolute;opacity:0;width:1px;height:1px}.sw i{display:block;width:48px;height:28px;border-radius:999px;background:rgba(11,11,10,.16);position:relative;transition:background .15s}.sw i:after{content:"";position:absolute;left:3px;top:3px;width:22px;height:22px;border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.25);transition:transform .15s}
|
|
609
|
+
.cl-opt.dk .sw i{background:rgba(244,241,234,.25)}.sw input:checked+i{background:var(--acc)}.sw input:checked+i:after{transform:translateX(20px)}.sw input:focus-visible+i{outline:2px solid var(--acc);outline-offset:2px}
|
|
410
610
|
.cl-hst{position:relative;margin-top:8px;font-size:14px;min-height:20px;opacity:.85}
|
|
411
611
|
.cl-f{flex:none;display:flex;flex-direction:column;gap:6px;margin-top:14px;font-size:14px;font-weight:500}
|
|
412
612
|
.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)}
|
|
@@ -441,8 +641,12 @@ const CLAIM_CSS = String.raw`
|
|
|
441
641
|
.cl-done h2{position:relative;margin:90px 0 0;font-size:44px;line-height:.95;letter-spacing:-.05em;font-weight:650}
|
|
442
642
|
.cl-done a.u{position:relative;display:block;margin-top:16px;font:15px var(--mono);color:#F4F1EA;word-break:break-all}
|
|
443
643
|
.cl-done p{position:relative;margin:14px 0 0;font-size:14px;line-height:1.45;opacity:.8}
|
|
644
|
+
.cl-done .u.off{position:relative;margin-top:16px;display:flex;align-items:center;gap:10px;flex-wrap:wrap;font:15px var(--mono);color:rgba(244,241,234,.6)}
|
|
645
|
+
.cl-done .u.off .tag{font:600 12px var(--sans);letter-spacing:.02em;color:#F4F1EA;background:rgba(244,241,234,.14);border-radius:999px;padding:4px 10px}
|
|
646
|
+
.cl-done a.u.s{margin-top:8px;font-size:13px;opacity:.85}
|
|
444
647
|
.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)}
|
|
648
|
+
.cl-row a{background:var(--ink);color:var(--paper)}
|
|
649
|
+
.cl-row.sec a{background:#fff;color:var(--ink);box-shadow:inset 0 0 0 1px rgba(11,11,10,.14)}.cl-row button{background:#fff;color:var(--ink);box-shadow:inset 0 0 0 1px rgba(11,11,10,.14)}
|
|
446
650
|
.cl-g2{margin-top:22px}.cl-g2>*+*{margin-top:14px}
|
|
447
651
|
.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
652
|
.cl-tip{margin-top:10px;font-size:12.5px;font-weight:600;opacity:.8}
|
|
@@ -466,8 +670,24 @@ d.querySelector(".cta").onclick=function(e){e.stopPropagation();openClaim()};ret
|
|
|
466
670
|
function openClaim(){document.getElementById("cl").hidden=false;document.body.style.overflow="hidden";renderClaim()}
|
|
467
671
|
function closeClaim(){document.getElementById("cl").hidden=true;document.body.style.overflow=""}
|
|
468
672
|
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>'+
|
|
470
|
-
function
|
|
673
|
+
function prog(n){var T=D.examples.length?3:2,k=T===2&&n===3?2:n,all=T===3?[1,2,3]:[1,2];return'<div class="cl-top"><button type="button" class="cl-back" data-a="back">'+(n===1?"Close":"Back")+'</button><span>'+k+' of '+T+'</span></div><div class="cl-prog">'+all.map(function(i){return'<i'+(i<=k?' class="on"':'')+'></i>'}).join("")+'</div>'}
|
|
674
|
+
function perkType(){var t=(D.cards||[]).find(function(c){return c.id==="type"});return t?String(t.big).replace(/^The /,""):""}
|
|
675
|
+
function dotArt(k){var C=18,R=5,o="";for(var y=0;y<R;y++)for(var x=0;x<C;x++){var r;
|
|
676
|
+
if(k==="pub")r=Math.pow(x/(C-1),1.5)*(0.6+0.4*Math.sin((y+1)/(R+1)*Math.PI));
|
|
677
|
+
else if(k==="roles"){var hs={1:2,4:4,7:3,10:5,13:3,16:5};r=x%3===1&&y>=R-hs[x]?0.85:0}
|
|
678
|
+
else{var dx=(x-C*0.64)/1.7,dy=y-R*0.4;r=1.05-Math.sqrt(dx*dx+dy*dy)/4.4}
|
|
679
|
+
var lit=r>0.28;o+='<circle cx="'+(x*10+5)+'" cy="'+(y*10+5)+'" r="'+((lit?Math.min(1,r):0.17)*4.4).toFixed(2)+'"'+(lit?' class="l" style="--d:'+(x*24+y*10)+'ms"':'')+'/>'}
|
|
680
|
+
return'<svg class="art" viewBox="0 0 '+C*10+' '+R*10+'" aria-hidden="true">'+o+'</svg>'}
|
|
681
|
+
function pkTile(k,label,big,body,offT,onT,back){var on=!!CS[k],sw='<span class="sw"><input type="checkbox" data-o="'+k+'"'+(on?' checked':'')+'><i></i></span>',fr='<span class="pk-l">'+label+'</span>'+dotArt(k)+'<span class="pk-b">'+big+'</span><span class="pk-t">'+body+'</span>';
|
|
682
|
+
if(!back)return'<label class="pk pk-'+k+(on?' on':'')+'">'+fr+'<span class="pk-f"><span><span class="o0">'+offT+'</span><span class="o1">'+onT+'</span></span>'+sw+'</span></label>';
|
|
683
|
+
return'<label class="pk fl pk-'+k+(on?' on':'')+'"><span class="pk-flip"><span class="pk-face fr">'+fr+'<span class="pk-f"><span>'+offT+'</span>'+sw+'</span></span><span class="pk-face bk" aria-hidden="true">'+back+'<span class="pk-f"><span>'+onT+'</span><span class="sw on-v"><i></i></span></span></span></span></label>'}
|
|
684
|
+
function cardBy(id){return (D.cards||[]).find(function(c){return c.id===id})}
|
|
685
|
+
function introBack(hh){var m=D.m||{},t=cardBy("type"),sc=cardBy("score"),ch=[];if(t)ch.push(t.big);if(m.parallelMax>1&&m.parallelMax<=24)ch.push(m.parallelMax+" agents at once");if(m.shipped&&m.shipped.commits)ch.push(Number(m.shipped.commits).toLocaleString("en")+" commits");if(ch.length<3&&sc)ch.push("Hour score "+sc.big);
|
|
686
|
+
return'<span class="pk-l2"><span>What an intro looks like</span><em>Example</em></span><span class="ib"><span class="ib-h">Intro request</span><span class="ib-t">A team building AI agents wants to meet @'+hh+'.</span><span class="ib-s">What they saw</span><span class="ib-c">'+ch.slice(0,3).map(function(x){return'<i>'+esc(x)+'</i>'}).join("")+'</span><span class="ib-a"><b>Say yes</b><b class="o">Not now</b></span></span>'}
|
|
687
|
+
function featBack(hh){var t=cardBy("type"),ln=t&&t.line?String(t.line).split(/(?<=\.)\s/)[0]:"";
|
|
688
|
+
return'<span class="pk-l2"><span>Builders to watch</span><em>Example</em></span><span class="fp"><span class="fp-h"><svg viewBox="0 0 64 64" aria-hidden="true"><use href="#mark"/></svg>Hour<span>· this week</span></span><span class="fp-b"><span class="pk-thumb"></span><span class="fp-t"><b>@'+hh+'</b><span>'+esc((t?t.big+". ":"")+ln)+'</span></span></span></span><span class="fp-c"><i>X</i><i>LinkedIn</i><i>Hour newsletter</i></span>'}
|
|
689
|
+
function perkLine(){var t=(D.cards||[]).find(function(c){return c.id==="type"}),sc=(D.cards||[]).find(function(c){return c.id==="score"});return (t?t.big:"")+(sc?" · Hour score "+sc.big:"")}
|
|
690
|
+
function handleNote(){var h=CS.handle;if(!h)return"Letters, numbers and dashes.";if(D.suggest&&h===D.suggest)return"We suggested this from your computer’s name. Type any handle you like.";if(!HANDLE_RE.test(h))return"3 to 24 letters, numbers or single dashes.";return"Press Continue to check it's free."}
|
|
471
691
|
function toks(i){return CS.texts[i].split(/(\s+)/)}
|
|
472
692
|
function isAuto(w){return w.indexOf("[hidden]")>=0}
|
|
473
693
|
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()}
|
|
@@ -476,7 +696,7 @@ function viewHTML(i){var h=CS.hide[i];return toks(i).map(function(w,k){if(/^\s+$
|
|
|
476
696
|
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
697
|
function renderClaim(){var s=document.getElementById("cls"),H="",n=CS.step;
|
|
478
698
|
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>'
|
|
699
|
+
+'<div class="cl-g2"><div class="cl-hcard"><label for="clh">Your handle · 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)+'" aria-describedby="clhst"><button type="button" class="cl-edit-h" data-a="edith" aria-label="Edit your handle"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 20h4L19 9l-4-4L4 16v4z"/><path d="M13.5 6.5l4 4"/></svg>Edit</button></div><div class="cl-hst" id="clhst">'+esc(handleNote())+'</div></div>'
|
|
480
700
|
+'<div><label class="cl-f" style="margin-top:0">Your name<input id="cln" autocomplete="name" maxlength="80" value="'+esc(CS.name)+'"></label>'
|
|
481
701
|
+'<label class="cl-f">Email, to keep your handle<input id="cle" type="email" autocomplete="email" maxlength="200" value="'+esc(CS.email)+'"></label>'
|
|
482
702
|
+'<p class="cl-fine">We email you a link to your profile. That’s how you get back in. No password.</p></div></div>'
|
|
@@ -489,25 +709,32 @@ return'<div class="cl-pc'+(on?" on":"")+(ed?" ed":"")+'" data-a="pick" data-i="'
|
|
|
489
709
|
+'<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
710
|
+'<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
711
|
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="
|
|
493
|
-
+
|
|
494
|
-
+
|
|
495
|
-
+
|
|
496
|
-
+'</div><div><
|
|
497
|
-
+'<
|
|
712
|
+
+'<div class="pk-row">'
|
|
713
|
+
+pkTile("pub","Public profile",'<span class="hd">@'+hh+'</span>',"One link for LinkedIn, GitHub and your email signature. It shows how you work, which a CV can’t.","Private: only your secret link opens it","Live at usehour.com/@"+hh)
|
|
714
|
+
+pkTile("roles","Teams hiring","Get asked, not screened.","Teams hiring AI builders see how you work and ask to meet you. You say yes or no. Your email stays hidden.","Not open to intros","Open to intros, by email",introBack(hh))
|
|
715
|
+
+pkTile("feature","Hour picks","Get featured.","We share standout cards with the founders and hiring teams who follow Hour, tagged @"+hh+".","Not in the picks",esc(perkType())?"Your "+esc(perkType())+" card is in":"Your cards are in",featBack(hh))
|
|
716
|
+
+'</div><div class="gv"><div class="gv-h"><span class="pk-l">What goes to Hour</span><span class="gv-b">Your cards go.<br>Your prompts stay.</span><button type="button" class="cl-link" data-a="data">'+(CS.data?"Hide the exact data":"See the exact data")+'</button></div>'
|
|
717
|
+
+'<div class="gv-c"><span class="pk-l">Sent to Hour</span><ul><li>The numbers on your cards</li><li>Languages, tools and model names</li>'+(D.m&&D.m.goToPrompt?'<li>Your go-to phrase, “'+esc(D.m.goToPrompt.text)+'”</li>':'')+'<li>Handle, name and email</li></ul></div>'
|
|
718
|
+
+'<div class="gv-c st"><span class="pk-l">Stays on this computer</span><ul><li>Every prompt you wrote</li><li>Your logs and code</li><li>File and project names</li></ul></div>'
|
|
719
|
+
+(CS.data?'<pre class="gv-pre">'+esc(JSON.stringify(p,null,2))+'</pre>':'')+'</div>'
|
|
498
720
|
+'<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
|
|
500
|
-
+'<
|
|
501
|
-
|
|
721
|
+
else{var d=CS.done,pub=!!d.publicUrl,u=d.publicUrl||d.url,hu="usehour.com/@"+d.handle;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>'
|
|
722
|
+
+(pub?'<a class="u" href="'+esc(u)+'" target="_blank" rel="noopener">'+esc(hu)+'</a><p>Your profile is public. Put the link anywhere you like.</p>'
|
|
723
|
+
:'<div class="u off">'+esc(hu)+'<span class="tag">Not public yet</span></div><p>'+esc(hu)+' starts working when you make your profile public. Until then, only this private link opens it:</p><a class="u s" href="'+esc(d.url)+'" target="_blank" rel="noopener">'+esc(d.url.replace("https://",""))+'</a>')+'</div>'
|
|
724
|
+
+(pub||!d.key?'':'<button type="button" class="cl-go" style="width:100%;margin-top:16px" data-a="gopub"'+(CS.busy?" disabled":"")+'>'+(CS.busy?"Making it public…":"Make @"+esc(d.handle)+" public")+'</button>')
|
|
725
|
+
+'<div class="cl-err">'+esc(CS.err)+'</div>'
|
|
726
|
+
+'<div class="cl-row'+(pub?'':' sec')+'"><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>'}
|
|
727
|
+
s.innerHTML=H;s.scrollTop=0;if(n===3){var th=s.querySelector(".pk-thumb"),tc=cardBy("type");if(th&&tc)th.appendChild(el(tc))}if(n===1){var hi=document.getElementById("clh");if(hi&&D.suggest&&CS.handle===D.suggest){hi.focus();hi.select()}}
|
|
502
728
|
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
729
|
document.getElementById("cls").addEventListener("input",function(e){var t=e.target;
|
|
504
730
|
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
731
|
else if(t.id==="cln")CS.name=t.value;else if(t.id==="cle")CS.email=t.value;
|
|
506
732
|
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()}});
|
|
733
|
+
document.getElementById("cls").addEventListener("change",function(e){var o=e.target.getAttribute("data-o");if(o){CS[o]=e.target.checked;var pk=e.target.closest(".pk");if(pk)pk.classList.toggle("on",e.target.checked);if(CS.data)renderClaim()}});
|
|
508
734
|
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
735
|
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
736
|
else if(a==="close"){closeClaim()}
|
|
737
|
+
else if(a==="edith"){var hi=document.getElementById("clh");hi.focus();hi.select()}
|
|
511
738
|
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
739
|
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
740
|
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}
|
|
@@ -517,19 +744,61 @@ else if(a==="next2"){CS.err="";CS.editing=-1;CS.step=3;renderClaim()}
|
|
|
517
744
|
else if(a==="skip2"){CS.picked=[];CS.err="";CS.editing=-1;CS.step=3;renderClaim()}
|
|
518
745
|
else if(a==="data"){CS.data=!CS.data;renderClaim()}
|
|
519
746
|
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)})}
|
|
747
|
+
else if(a==="gopub"){if(CS.busy)return;CS.busy=true;CS.err="";renderClaim();
|
|
748
|
+
fetch(D.api+"/profiles/public",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:CS.done.id,key:CS.done.key})}).then(function(r){return r.json().catch(function(){return{}}).then(function(j){return{s:r.status,j:j}})}).then(function(x){CS.busy=false;
|
|
749
|
+
if(x.s===200&&x.j.publicUrl){CS.done.publicUrl=x.j.publicUrl;delete CS.done.key}else CS.err="That didn’t work ("+(x.j.error||"error "+x.s)+"). You can switch it on from the link in your email.";renderClaim()}).catch(function(){CS.busy=false;CS.err="Couldn’t reach usehour.com. Try again, or switch it on from the link in your email.";renderClaim()})}
|
|
520
750
|
else if(a==="create"){if(CS.busy)return;CS.busy=true;CS.err="";renderClaim();
|
|
521
751
|
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
752
|
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
753
|
else if(x.s===409){CS.step=1;CS.err="Someone just took @"+CS.handle+". Pick another."}
|
|
524
754
|
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
755
|
document.getElementById("claimb").onclick=openClaim;
|
|
526
|
-
(function(){var h=document.getElementById("clhandle")
|
|
756
|
+
(function(){var h=document.getElementById("clhandle");if(CS.handle){h.textContent=CS.handle;var sg=document.createElement("em");sg.textContent="suggested";h.parentNode.appendChild(sg)}})();
|
|
527
757
|
`;
|
|
528
758
|
|
|
529
759
|
/* --------------------------------------------------------------- the page */
|
|
760
|
+
/* The avatar engine (direction D, the living dot matrix). Source of truth is
|
|
761
|
+
cli/avatar-engine.js; this is the same code as a string, because the
|
|
762
|
+
published package is this one file. Same copy in site/netlify/lib/avatar.mjs. */
|
|
763
|
+
const AV_JS = "/* Hour avatars, direction D: the living dot matrix. One per builder type.\n No dependencies. window.HourAvatar.draw(ctx, size, t, type, seed, still)\n paints one frame; HourAvatar.add(canvas, type, seed) animates a canvas;\n HourAvatar.scan(root) picks up every canvas[data-av=\"type:seed\"]. Draw on\n a dark surface: the eyes are near-white. */\nwindow.HourAvatar = (function () {\n var STOPS = [[0, [79, 136, 255]], [0.38, [176, 172, 254]], [0.72, [251, 101, 194]], [1, [253, 41, 128]]];\n function grad(u) { u = Math.max(0, Math.min(1, u)); for (var i = 1; i < STOPS.length; i++) { if (u <= STOPS[i][0]) { var a = STOPS[i - 1], b = STOPS[i], k = (u - a[0]) / (b[0] - a[0]); return [a[1][0] + (b[1][0] - a[1][0]) * k, a[1][1] + (b[1][1] - a[1][1]) * k, a[1][2] + (b[1][2] - a[1][2]) * k]; } } return STOPS[3][1]; }\n function rgba(c, a) { return 'rgba(' + (c[0] | 0) + ',' + (c[1] | 0) + ',' + (c[2] | 0) + ',' + a + ')'; }\n function hash(n) { var x = Math.sin(n * 127.1 + 311.7) * 43758.5453; return x - Math.floor(x); }\n function ease(k) { k = Math.max(0, Math.min(1, k)); return k < .5 ? 4 * k * k * k : 1 - Math.pow(-2 * k + 2, 3) / 2; }\n function gaze(t, seed, bias) {\n var P = 2.1 + hash(seed) * 1.2, s = Math.floor((t + seed * 3) / P), f = ((t + seed * 3) % P) / P;\n function tgt(i) { var h = hash(i + seed * 17), h2 = hash(i * 3.3 + seed); return [(h < .35 ? 0 : (h2 * 2 - 1)) + (bias || 0), (hash(i * 7.7 + seed) * 2 - 1) * .5]; }\n var a = tgt(s - 1), b = tgt(s), k = ease(f / .16);\n return [a[0] + (b[0] - a[0]) * k, a[1] + (b[1] - a[1]) * k];\n }\n function blink(t, seed) {\n var P = 3.7 + hash(seed + 9) * 1.6, f = (t + seed * 5) % P, d = .16;\n if (f > d) { var f2 = f - .28; if (hash(Math.floor((t + seed * 5) / P) + seed) > .7 && f2 > 0 && f2 < d) return Math.abs(f2 / d * 2 - 1); return 1; }\n return Math.abs(f / d * 2 - 1);\n }\n function capsule(ctx, x, y, w, h) { var r = Math.min(w, h) / 2; ctx.moveTo(x - w / 2 + r, y - h / 2); ctx.lineTo(x + w / 2 - r, y - h / 2); ctx.arc(x + w / 2 - r, y - h / 2 + r, r, -Math.PI / 2, 0); ctx.lineTo(x + w / 2, y + h / 2 - r); ctx.arc(x + w / 2 - r, y + h / 2 - r, r, 0, Math.PI / 2); ctx.lineTo(x - w / 2 + r, y + h / 2); ctx.arc(x - w / 2 + r, y + h / 2 - r, r, Math.PI / 2, Math.PI); ctx.lineTo(x - w / 2, y - h / 2 + r); ctx.arc(x - w / 2 + r, y - h / 2 + r, r, Math.PI, Math.PI * 1.5); ctx.closePath(); }\n function draw(ctx, S, t, type, seed, still) {\n seed = seed || 1;\n var cx = S / 2, cy = S / 2, R = S * .44, small = S < 90, n = S < 50 ? 7 : S < 90 ? 9 : S < 200 ? 19 : 27, st = 2 * R / n;\n var fields = type === 'pair' ? [[cx - R * .36, cy + R * .04, R * .6, seed], [cx + R * .36, cy - R * .04, R * .62, seed + 4]] : [[cx, cy, R, seed]];\n var tilt = type === 'interrogator' ? Math.sin(t * .9) * .12 * (Math.sin(t * .45) > 0 ? 1 : .2) : 0;\n var eyes = fields.map(function (f, i) {\n var bias = type === 'sprinter' ? .7 : type === 'pair' ? (i ? -1 : 1) * (Math.sin(t * .5 + seed) > .3 ? .9 : 0) : 0;\n var g = still ? [type === 'sprinter' ? .7 : type === 'pair' ? (i ? -.6 : .6) : 0, 0] : gaze(t, f[3], bias), op = still ? 1 : blink(t, f[3]);\n var sq = 1; if (type === 'verifier') { var by = Math.sin(t * 1.3) * R * .75; sq = 1 - .55 * Math.exp(-Math.pow(by / (R * .18), 2)); }\n return { x: f[0], y: f[1], r: f[2], g: g, op: op * sq, by: type === 'verifier' ? f[1] + Math.sin(t * 1.3) * R * .75 : null };\n });\n var sats = type === 'conductor' ? [0, 1, 2].map(function (k) { var a = t * .6 + k * 2.094; return [cx + Math.cos(a) * R * .72, cy + Math.sin(a) * R * .72]; }) : [];\n for (var iy = 0; iy <= n; iy++) for (var ix = 0; ix <= n; ix++) {\n var px = cx - R + ix * st, py = cy - R + iy * st;\n if (type === 'sprinter') px -= ((t * 40) % st);\n var best = 0, inside = 0, bright = 0;\n eyes.forEach(function (e) {\n var dx = px - e.x, dy = py - e.y;\n if (tilt) { var c = Math.cos(tilt), s = Math.sin(tilt); var rx = dx * c - dy * s, ry = dx * s + dy * c; dx = rx; dy = ry; }\n var dd = type === 'architect' ? Math.max(Math.abs(dx), Math.abs(dy)) / (e.r * .86) : Math.hypot(dx, dy) / e.r;\n var edge = 1 - dd; if (edge > inside) inside = edge;\n for (var s2 = -1; s2 <= 1; s2 += 2) {\n var hx = e.x + s2 * e.r * .27 + e.g[0] * e.r * .13, hy = e.y - e.r * .02 + e.g[1] * e.r * .08;\n var raise = type === 'interrogator' && s2 === 1 ? 1.3 : 1;\n var qx = (px - hx) / Math.max(e.r * .115, small ? st * .55 : 0), qy = (py - hy) / Math.max(e.r * .23 * raise * Math.max(.12, e.op), small ? st * .6 * Math.max(.3, e.op) : 0);\n var v = Math.exp(-(qx * qx + qy * qy) * 1.3); if (v > bright) bright = v;\n }\n if (e.by !== null) { var band = Math.exp(-Math.pow((py - e.by) / (st * 1.2), 2)); best = Math.max(best, band); }\n });\n sats.forEach(function (sp) { var v = Math.exp(-Math.pow(Math.hypot(px - sp[0], py - sp[1]) / (st * 1.3), 2)); if (v > bright * .8) bright = Math.max(bright, v * .95); });\n if (inside <= 0 && !sats.length) continue;\n if (inside <= 0 && bright < .2) continue;\n var fade = Math.min(1, inside * 3.2);\n var wave = .5 + .5 * Math.sin((px + py) / S * 9 - t * 2.2 + seed);\n if (type === 'architect') { var row = (t * 3.2) % (n + 8); wave = Math.max(wave * .6, Math.exp(-Math.pow(iy - row, 2) / 3)); }\n var base = st * ((small ? .26 : .14) + .12 * wave * fade + .16 * best);\n var r = base + st * .34 * bright;\n var col = grad((px - (cx - R)) / (2 * R) * .75 + (py - (cy - R)) / (2 * R) * .25);\n if (bright > .35) col = [244 - 30 * (1 - bright), 241 - 30 * (1 - bright), 234];\n ctx.fillStyle = rgba(col, Math.min(1, ((small ? .22 : .35) + (small ? .25 : .6) * wave * fade + .5 * best) * fade + bright));\n ctx.beginPath();\n if (type === 'sprinter' && bright < .35) { var w = r * (1.6 + 1.4 * (1 - ix / n)); capsule(ctx, px, py, w * 2, r * 2); } else ctx.arc(px, py, Math.min(r, st * .48), 0, 6.283);\n ctx.fill();\n }\n }\n var list = [], raf = 0, t0 = 0;\n var still = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;\n function frame(now) {\n raf = 0; if (!t0) t0 = now; var t = (now - t0) / 1000;\n list = list.filter(function (a) { return a.c.isConnected; });\n list.forEach(function (a) {\n var S = a.c.clientWidth; if (!S) return;\n var dpr = Math.min(2, window.devicePixelRatio || 1), px = Math.round(S * dpr);\n if (a.c.width !== px) { a.c.width = px; a.c.height = px; }\n var ctx = a.c.getContext('2d'); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.clearRect(0, 0, S, S);\n draw(ctx, S, still ? 1.3 : t, a.type, a.seed, still);\n });\n if (list.length && !still) raf = requestAnimationFrame(frame);\n }\n function add(c, type, seed) { list.push({ c: c, type: type, seed: seed || 1 }); if (!raf) raf = requestAnimationFrame(frame); }\n function scan(root) { [].slice.call((root || document).querySelectorAll('canvas[data-av]')).forEach(function (c) { if (c.__av) return; c.__av = 1; var p = c.getAttribute('data-av').split(':'); add(c, p[0], +(p[1] || 1)); }); }\n return { draw: draw, add: add, scan: scan };\n})();\n";
|
|
764
|
+
|
|
765
|
+
/* Draws a card onto a canvas (the dot patterns and, for saving, the whole
|
|
766
|
+
card). Shared with the profile page, which paints its card carousel with it. */
|
|
767
|
+
const PAINT_JS = String.raw`function drawOn(x,W,H,c){if(!c.pat)return;
|
|
768
|
+
const cols=28,step=W/cols,rows=Math.ceil(H/step),top=H*.13,bot=H*.47;x.fillStyle=c.dot||"#fff";
|
|
769
|
+
const dot=(i,j,s)=>{if(s<=.04)return;x.globalAlpha=Math.min(1,.18+.82*s);x.beginPath();x.arc((i+.5)*step,(j+.5)*step,step*.42*Math.min(1,s),0,7);x.fill()};
|
|
770
|
+
const rand=(i,j)=>{const t=Math.sin(i*127.1+j*311.7+c.id.length*17.3)*43758.5453;return t-Math.floor(t)};
|
|
771
|
+
const band=[];for(let j=0;j<rows;j++){const y=(j+.5)*step;if(y>top&&y<bot)band.push(j)}const nb=band.length,j0=band[0]||0;
|
|
772
|
+
const colChart=v=>{const m=Math.max(1,...v),per=cols/v.length;for(let i=0;i<cols;i++){const h=Math.round(v[Math.min(v.length-1,Math.floor(i/per))]/m*nb);for(let k=0;k<nb;k++)dot(i,j0+nb-1-k,k<h?1:.12)}};
|
|
773
|
+
const ring=(p,a,b)=>{const cx=W/2,cy=(top+bot)/2,R=(bot-top)/2*.98;for(const j of band)for(let i=0;i<cols;i++){const px=(i+.5)*step-cx,py=(j+.5)*step-cy,d=Math.hypot(px,py);let an=Math.atan2(px,-py);if(an<0)an+=2*Math.PI;const t=an/(2*Math.PI),w=R*(a+(b-a)*t);if(d<=R&&d>=R-w)dot(i,j,t<=p?1:.16)}};
|
|
774
|
+
switch(c.pat){case"months":colChart(D.months);break;case"hours":colChart(D.hours);break;case"sweep":ring(1,.06,.5);break;case"ring":ring(c.pct||0,.1,.5);break;
|
|
775
|
+
case"lanes":for(let k=0;k<(c.n||5);k++){const i=Math.round((k+.5)*cols/(c.n||5)),s0=Math.floor(rand(k,3)*nb*.45),e0=nb-Math.floor(rand(k,7)*nb*.3);for(let q=0;q<nb;q++)dot(i,j0+q,q>=s0&&q<e0?1:.14)}break;
|
|
776
|
+
case"share":{const f=c.frac||0;let n=0;for(const j of band)for(let i=0;i<cols;i++){dot(i,j,rand(i,j)<f?1:.16);n++}break}
|
|
777
|
+
case"stripes":{let acc=0;const cut=(c.shares||[1]).map(v=>acc+=v);for(const j of band)for(let i=0;i<cols;i++){const f=(i+.5)/cols,k=cut.findIndex(v=>f<=v+1e-9);dot(i,j,[1,.62,.42,.3,.18][k<0?4:k])}break}
|
|
778
|
+
case"rain":for(const j of band)for(let i=0;i<cols;i++){const v=rand(i,j);dot(i,j,v>.58?.95:v>.3?.35:.1)}break;
|
|
779
|
+
case"field":for(const j of band)for(let i=0;i<cols;i++){const f=(i/cols+(j-j0)/nb)/2;dot(i,j,.15+.85*Math.pow(rand(i,j),1.6)*f*1.4)}break}x.globalAlpha=1}
|
|
780
|
+
/* Save a card as a 1080x1350 PNG. Drawn with canvas on this machine; nothing is uploaded. */
|
|
781
|
+
function wrapText(g,t,maxW){const words=String(t).split(" "),out=[];let l="";for(const w of words){const n=l?l+" "+w:w;if(g.measureText(n).width>maxW&&l){out.push(l);l=w}else l=n}if(l)out.push(l);return out}
|
|
782
|
+
function paint(g,W,H,c){g.fillStyle=c.bg;g.fillRect(0,0,W,H);drawOn(g,W,H,c);if(c.av){const s=c.summary?W*.22:W*.46;g.save();g.translate(c.summary?W*.73:W*.27,c.summary?W*.05:W*.18);HourAvatar.draw(g,s,1.3,c.av,1,true);g.restore()}const P=W*.08,F="Geist, ui-sans-serif, system-ui, -apple-system, Helvetica, Arial, sans-serif";g.fillStyle=c.fg;g.textBaseline="alphabetic";
|
|
783
|
+
const grad=()=>{const gr=g.createLinearGradient(P,0,W-P,0);[["#4F88FF",0],["#B0ACFE",.38],["#FB65C2",.72],["#FD2980",1]].forEach(([k,o])=>gr.addColorStop(o,k));return gr};
|
|
784
|
+
/* footer */const fs=W*.034,fy=H-W*.07;g.font="600 "+fs+"px "+F;const r=W*.019;if(D.mark){const s=r*2.3;g.save();g.translate(P-r*.15,fy-fs*.35-s/2);g.scale(s/64,s/64);const mg=g.createLinearGradient(0,0,64,64);[["#4F88FF",0],["#B0ACFE",.38],["#FB65C2",.72],["#FD2980",1]].forEach(([k,o])=>mg.addColorStop(o,k));g.fillStyle=mg;g.fill(new Path2D(D.mark));g.restore();g.fillStyle=c.fg}else{g.lineWidth=W*.008;g.strokeStyle=grad();g.beginPath();g.arc(P+r,fy-fs*.35,r,0,7);g.stroke()}g.globalAlpha=.8;g.fillText("Hour",P+r*2+W*.016,fy);g.font="400 "+fs+"px ui-monospace, SFMono-Regular, Menlo, monospace";g.textAlign="right";g.fillText("usehour.com",W-P,fy);g.textAlign="left";g.globalAlpha=1;
|
|
785
|
+
let y=fy-W*.085;
|
|
786
|
+
if(c.summary){const cw=(W-2*P-W*.06)/2,rowH=W*.22;const rows=Math.ceil(c.stats.length/2);let top=y-rows*rowH+W*.02;
|
|
787
|
+
c.stats.forEach((s,k)=>{const x=P+(k%2)*(cw+W*.06),yy=top+Math.floor(k/2)*rowH;g.fillStyle=c.fg;g.globalAlpha=1;g.fillRect(x,yy,cw,W*.0035);g.globalAlpha=.75;g.font="400 "+W*.034+"px "+F;g.fillText(s[0],x,yy+W*.05);g.globalAlpha=1;g.font="650 "+W*.062+"px "+F;wrapText(g,s[1],cw).slice(0,2).forEach((l,i)=>g.fillText(l,x,yy+W*.115+i*W*.062))});
|
|
788
|
+
g.font="650 "+W*.115+"px "+F;g.fillStyle=grad();g.fillText(c.type,P,c.av?W*.37:W*.33);g.fillStyle=c.fg;g.globalAlpha=.85;g.font="500 "+W*.04+"px "+F;g.fillText((c.who?c.who.split(" ")[0]+" · ":"")+c.period,P,W*.16,c.av?W*.6:W-2*P);g.globalAlpha=1;return}
|
|
789
|
+
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;
|
|
790
|
+
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));
|
|
791
|
+
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;
|
|
792
|
+
if(c.rows){const d=W*.023,gap=W*.009,pw=W*.135,dx=W-P-pw-10*d-9*gap;const rs=Math.min(W*.083,W*.42/c.rows.length);c.rows.forEach((w,k)=>{const yy=W*.29+k*rs;g.fillStyle=c.fg;g.font="500 "+W*.04+"px "+F;g.fillText(w[0],P,yy);g.font="600 "+W*.04+"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?(c.rc||"#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})}}
|
|
793
|
+
`;
|
|
530
794
|
function pageHTML(m, C, o = {}) {
|
|
531
|
-
const
|
|
532
|
-
|
|
795
|
+
const avk = (C.find((c) => c.av) || {}).av || "pair";
|
|
796
|
+
/* mode: "local" (the cards page on your computer), or on a profile:
|
|
797
|
+
"owner" (you, with Save) or "visitor" (read only, with "Make yours"). */
|
|
798
|
+
const mode = o.mode || "local", onSite = mode !== "local", first = String(o.who || "").split(" ")[0];
|
|
799
|
+
const api = onSite ? null : o.api || null, examples = api ? o.examples || [] : [];
|
|
800
|
+
const yours = mode === "visitor" ? `${first}'s` : "Your";
|
|
801
|
+
const data = JSON.stringify({ mark: MARK_PATH, hours: m.hours, months: m.months.map((x) => x.prompts), cards: C, m: api ? m : null, examples, api, canSave: mode !== "visitor", heads: headlines(m, mode === "visitor" ? first : null), suggest: api ? o.suggest || "" : "" }).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
533
802
|
/* The page may talk to exactly one place: Hour's profile endpoint, and only
|
|
534
803
|
when you press the button. With --no-share it may talk to nothing. */
|
|
535
804
|
const connect = api ? new URL(api).origin : "'none'";
|
|
@@ -537,6 +806,7 @@ function pageHTML(m, C, o = {}) {
|
|
|
537
806
|
const meta = { period: `${monthYear(m.window.first)} – ${monthYear(m.window.last)}`, sessions: m.sessions.total.toLocaleString("en"), prompts: m.prompts.total.toLocaleString("en") };
|
|
538
807
|
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
808
|
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>';
|
|
809
|
+
const NOTE = onSite ? `Counted on ${escH(first)}'s computer. No prompts shared.` : "Made on this computer. Nothing has been sent.";
|
|
540
810
|
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'`;
|
|
541
811
|
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
542
812
|
<meta http-equiv="Content-Security-Policy" content="${csp}">
|
|
@@ -551,14 +821,20 @@ function pageHTML(m, C, o = {}) {
|
|
|
551
821
|
.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
822
|
.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
823
|
.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:
|
|
824
|
+
.hero{margin-top:48px;display:grid;grid-template-columns:minmax(0,420px) minmax(0,1fr);gap:clamp(32px,6vw,80px);align-items:center}
|
|
555
825
|
.hero-r{display:flex;flex-direction:column}
|
|
556
826
|
.hero h1{margin:4px 0 0;font-size:clamp(48px,6.4vw,76px);line-height:.9;letter-spacing:-.06em;font-weight:650}
|
|
557
827
|
.hero p.sub{margin:16px 0 0;font-size:19px;line-height:1.4;color:#3A3935}
|
|
558
|
-
.acts{margin-top:
|
|
559
|
-
.
|
|
828
|
+
.acts{margin-top:32px;display:flex;gap:12px;flex-wrap:wrap}
|
|
829
|
+
.glance{margin-top:56px;display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:16px;align-items:stretch}.glance.solo{grid-template-columns:1fr}
|
|
830
|
+
.heads-card{border-radius:26px;background:#fff;box-shadow:inset 0 0 0 1px rgba(11,11,10,.08);padding:30px 32px}.heads-card .k{font-size:15px;font-weight:500;color:#6C6A64}
|
|
831
|
+
.claimbox{border-radius:26px;background:#8B5FD9;color:#fff;padding:28px 28px 26px;position:relative;overflow:hidden}
|
|
560
832
|
.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}
|
|
833
|
+
.claimbox>*{position:relative}
|
|
834
|
+
.claimbox a.cta{display:inline-flex;align-items:center;height:48px;margin-top:14px;padding:0 22px;border-radius:999px;background:#fff;color:var(--ink);font:600 15px var(--sans);text-decoration:none}
|
|
835
|
+
.heads{list-style:none;margin:18px 0 0;padding:0;display:grid;gap:16px}.heads li{position:relative;padding-left:24px;font-size:clamp(17px,1.6vw,20px);line-height:1.35;letter-spacing:-.015em;font-weight:500}.heads li:before{content:"";position:absolute;left:0;top:.45em;width:10px;height:10px;border-radius:50%;background:linear-gradient(135deg,#4F88FF,#B0ACFE 38%,#FB65C2 72%,#FD2980)}
|
|
836
|
+
.nosave .save,.nosave #sts{display:none}
|
|
837
|
+
a.btn{text-decoration:none;display:inline-flex;align-items:center}.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
838
|
.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
839
|
.claimbox p{margin:12px 0 0;max-width:460px;font-size:15.5px;line-height:1.42}
|
|
564
840
|
.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}
|
|
@@ -567,9 +843,9 @@ h2.sec{margin:64px 0 18px;font-size:30px;letter-spacing:-.04em;font-weight:650}
|
|
|
567
843
|
.grid .card{cursor:pointer;transition:transform .15s}.grid .card:hover{transform:translateY(-3px)}
|
|
568
844
|
.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
845
|
.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}.
|
|
846
|
+
@media(max-width:760px){.hero{grid-template-columns:1fr;margin-top:28px;gap:28px}.hero-r{order:-1}.glance{grid-template-columns:1fr;margin-top:28px}.heads-card{padding:24px 22px}.claimbox{padding:24px 22px}.tips{grid-template-columns:1fr}h2.sec{margin-top:48px;font-size:24px}}
|
|
571
847
|
.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(
|
|
848
|
+
.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(circle at 78% 50%,transparent 0 17vw,rgba(0,0,0,.55) 21vw,transparent 31vw);mask-image:radial-gradient(circle at 78% 50%,transparent 0 17vw,rgba(0,0,0,.55) 21vw,transparent 31vw);pointer-events:none}
|
|
573
849
|
.intro>*{position:relative}.intro .top .note{color:#F4F1EA;opacity:.75}
|
|
574
850
|
.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
851
|
.intro h1{margin:18px 0 0;font-size:clamp(56px,9vw,112px);line-height:.88;letter-spacing:-.065em;font-weight:650}
|
|
@@ -590,79 +866,58 @@ h2.sec{margin:64px 0 18px;font-size:30px;letter-spacing:-.04em;font-weight:650}
|
|
|
590
866
|
@media(max-width:640px){.nb{display:none}}
|
|
591
867
|
.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}
|
|
592
868
|
.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}
|
|
593
|
-
.card canvas{position:absolute;inset:0;width:100%;height:100%}
|
|
869
|
+
.card canvas{position:absolute;inset:0;width:100%;height:100%}.card canvas.av{inset:auto;left:27cqw;top:18cqw;width:46cqw;height:46cqw}.card canvas.av.s{left:auto;right:5cqw;top:5cqw;width:22cqw;height:22cqw}.card canvas.av.s~.in .ask{max-width:none;padding-right:23cqw;min-height:16cqw;font-size:3.7cqw;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.intro .iav{position:absolute;right:clamp(24px,6vw,96px);top:50%;transform:translateY(-50%);width:min(34vw,440px);height:min(34vw,440px);pointer-events:none}@media (max-width:860px){.intro .iav{position:static;transform:none;width:120px;height:120px;margin:8px 0 -8px}}
|
|
594
870
|
.card .in{position:absolute;inset:0;padding:9cqw 8cqw 7cqw;display:flex;flex-direction:column;z-index:1}
|
|
595
871
|
.ask{font-size:4.4cqw;font-weight:500;opacity:.85;margin-top:3cqw;max-width:62%}
|
|
596
872
|
.big{margin-top:auto;font-size:var(--bs,19cqw);line-height:.9;letter-spacing:-.055em;font-weight:650;text-wrap:balance}
|
|
597
873
|
.line{font-size:5.6cqw;line-height:1.25;letter-spacing:-.015em;margin-top:3.4cqw;max-width:92%;text-wrap:balance}
|
|
598
874
|
.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)}
|
|
599
|
-
.sum{margin-top:auto;display:grid;grid-template-columns:1fr 1fr;gap:
|
|
875
|
+
.sum{margin-top:auto;display:grid;grid-template-columns:1fr 1fr;gap:5.5cqw 6cqw}.sum div{border-top:.35cqw solid currentColor;padding-top:2.6cqw}.sum small{display:block;font-size:3.4cqw;opacity:.75;margin-bottom:1.4cqw}.sum b{font-size:6.2cqw;letter-spacing:-.035em;font-weight:650;line-height:1}
|
|
600
876
|
.arch{font-size:11.5cqw;line-height:.92;letter-spacing:-.05em;font-weight:650;margin:2cqw 0 6cqw}
|
|
601
|
-
.rows{margin-top:6cqw;display:grid;gap:3.2cqw}.rows div{display:grid;grid-template-columns:minmax(0,1fr) auto 9cqw;align-items:center;gap:2.6cqw;font-size:4cqw;font-weight:500;white-space:nowrap}.rows div>span{overflow:hidden;text-overflow:ellipsis}.rows em{display:flex;gap:.9cqw}.rows u{width:2.3cqw;height:2.3cqw;border-radius:50%;background:currentColor;opacity:.14}.rows u.on{opacity:1;background
|
|
877
|
+
.rows{margin-top:6cqw;display:grid;gap:3.2cqw}.rows div{display:grid;grid-template-columns:minmax(0,1fr) auto 9cqw;align-items:center;gap:2.6cqw;font-size:4cqw;font-weight:500;white-space:nowrap}.rows div>span{overflow:hidden;text-overflow:ellipsis}.rows em{display:flex;gap:.9cqw}.rows u{width:2.3cqw;height:2.3cqw;border-radius:50%;background:currentColor;opacity:.14}.rows u.on{opacity:1;background:var(--rc,#8B5FD9)}.rows.many{gap:1.5cqw;margin-top:4cqw}.rows b{text-align:right;font-weight:600}
|
|
602
878
|
${api ? CLAIM_CSS : ""}
|
|
603
879
|
.grad{background:linear-gradient(100deg,#4F88FF 0%,#B0ACFE 38%,#FB65C2 72%,#FD2980 100%);-webkit-background-clip:text;background-clip:text;color:transparent}
|
|
604
880
|
</style></head><body>
|
|
605
881
|
<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
882
|
<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}
|
|
608
|
-
<div class="mid"><div class="meta">${escH(meta.period)} · ${meta.sessions} sessions · ${meta.prompts} prompts</div>
|
|
609
|
-
<h1
|
|
610
|
-
<p>${C.length} cards about how you build:
|
|
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
|
|
883
|
+
<div class="top"><div class="brand"><svg viewBox="0 0 64 64"><use href="#mark"/></svg>Hour</div><div class="note">${LOCK}${NOTE}</div></div>
|
|
884
|
+
<canvas class="iav" data-av="${avk}:1" aria-hidden="true"></canvas><div class="mid"><div class="meta">${escH(meta.period)} · ${meta.sessions} sessions · ${meta.prompts} prompts</div>
|
|
885
|
+
<h1>${yours} year with <span class="grad">AI</span>${onSite ? "." : " is ready."}</h1>
|
|
886
|
+
<p>${C.length} cards about how ${mode === "visitor" ? escH(first) + " builds" : "you build"}: the type, the hours, how the agent gets briefed. About a minute.</p>
|
|
887
|
+
<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>${mode === "visitor" ? `Play ${escH(first)}'s year` : "Play my year"}</button><button type="button" class="skip" id="skip">Skip to all cards</button></div></div>
|
|
612
888
|
<div class="tip">Tip: use the arrow keys, or tap the left and right of a card. Space pauses.</div>
|
|
613
889
|
</div>
|
|
614
890
|
<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
|
|
891
|
+
<div class="top"><div class="brand"><svg viewBox="0 0 64 64"><use href="#mark"/></svg>${yours} 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
892
|
<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
893
|
<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
894
|
<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
895
|
<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
896
|
</div>
|
|
621
897
|
<div class="wrap">
|
|
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}
|
|
898
|
+
<div class="top"><div class="brand"><svg viewBox="0 0 64 64"><use href="#mark"/></svg>Hour</div><div class="note" id="topnote">${LOCK}${NOTE}</div></div>
|
|
623
899
|
<section class="hero"><div id="sumcard"></div><div class="hero-r">
|
|
624
|
-
<h1
|
|
625
|
-
<div class="acts"
|
|
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>` : ""}
|
|
900
|
+
<h1>${yours} year with AI</h1><p class="sub">${C.length} cards from ${meta.sessions} sessions.${mode === "visitor" ? "" : " Save the ones you like and post them."}</p>
|
|
901
|
+
<div class="acts">${mode === "visitor" ? "" : `<button type="button" class="btn" id="saveall">${DL}Save all cards</button>`}<button type="button" class="btn${mode === "visitor" ? "" : " ghost"}" id="replay"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7 4.5v15l13-7.5z"/></svg>${mode === "visitor" ? `Play ${escH(first)}'s year` : "Play again"}</button>${onSite && o.profileUrl ? `<a class="btn ghost" href="${escH(o.profileUrl)}">${mode === "visitor" ? `${escH(first)}'s profile` : "Back to my profile"}</a>` : ""}</div>
|
|
627
902
|
</div></section>
|
|
628
|
-
<
|
|
629
|
-
|
|
903
|
+
<section class="glance${mode === "owner" ? " solo" : ""}"><div class="heads-card"><div class="k">${mode === "visitor" ? `${escH(first)} in four lines` : "You in four lines"}</div><ul class="heads" id="heads"></ul></div>
|
|
904
|
+
${mode === "visitor" ? `<div class="claimbox"><div class="k">Made with Hour</div><div class="h">What does your year with AI look like?</div><p>Cards like these, from your own Claude Code and Codex logs. Counted on your computer.</p><a class="cta" href="${escH(o.makeUrl || "https://usehour.com/wrapped")}">Make yours</a></div>` : ""}
|
|
905
|
+
${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>` : ""}
|
|
906
|
+
</section>
|
|
907
|
+
<h2 class="sec">${mode === "visitor" ? `All of ${escH(first)}\u2019s cards` : "All your cards"}</h2><div class="grid" id="grid"></div>
|
|
908
|
+
${mode === "visitor" ? "" : `<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>${onSite ? "Your cards live on your profile now: open it and press Your cards." : "This page lives in the hour-wrapped folder where you ran the command, and on your profile once you claim one."}</span></div></div>`}
|
|
630
909
|
${api ? CLAIM_HTML : ""}
|
|
631
910
|
</div>
|
|
632
911
|
<script>
|
|
912
|
+
${AV_JS}
|
|
633
913
|
const D=${data};
|
|
634
914
|
const esc=s=>String(s).replace(/[&<>"]/g,c=>({"&":"&","<":"<",">":">",'"':"""}[c]));
|
|
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);
|
|
915
|
+
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);if(c.rc)d.style.setProperty("--rc",c.rc);
|
|
636
916
|
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>';
|
|
637
|
-
d.innerHTML='<canvas aria-hidden="true"></canvas
|
|
638
|
-
requestAnimationFrame(()=>draw(d,c));return d}
|
|
917
|
+
d.innerHTML='<canvas aria-hidden="true"></canvas>'+(c.av?'<canvas class="av'+(c.summary?' s':'')+'" aria-hidden="true"></canvas>':'')+'<div class="in">'+(c.summary?('<div class="ask">'+esc(c.who?c.who.split(" ")[0]+" \\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.length>5?' many':'')+'">'+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>';
|
|
918
|
+
requestAnimationFrame(()=>draw(d,c));if(c.av)HourAvatar.add(d.querySelector("canvas.av"),c.av,1);return d}
|
|
639
919
|
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)}
|
|
640
|
-
|
|
641
|
-
const cols=28,step=W/cols,rows=Math.ceil(H/step),top=H*.13,bot=H*.47;x.fillStyle=c.dot||"#fff";
|
|
642
|
-
const dot=(i,j,s)=>{if(s<=.04)return;x.globalAlpha=Math.min(1,.18+.82*s);x.beginPath();x.arc((i+.5)*step,(j+.5)*step,step*.42*Math.min(1,s),0,7);x.fill()};
|
|
643
|
-
const rand=(i,j)=>{const t=Math.sin(i*127.1+j*311.7+c.id.length*17.3)*43758.5453;return t-Math.floor(t)};
|
|
644
|
-
const band=[];for(let j=0;j<rows;j++){const y=(j+.5)*step;if(y>top&&y<bot)band.push(j)}const nb=band.length,j0=band[0]||0;
|
|
645
|
-
const colChart=v=>{const m=Math.max(1,...v),per=cols/v.length;for(let i=0;i<cols;i++){const h=Math.round(v[Math.min(v.length-1,Math.floor(i/per))]/m*nb);for(let k=0;k<nb;k++)dot(i,j0+nb-1-k,k<h?1:.12)}};
|
|
646
|
-
const ring=(p,a,b)=>{const cx=W/2,cy=(top+bot)/2,R=(bot-top)/2*.98;for(const j of band)for(let i=0;i<cols;i++){const px=(i+.5)*step-cx,py=(j+.5)*step-cy,d=Math.hypot(px,py);let an=Math.atan2(px,-py);if(an<0)an+=2*Math.PI;const t=an/(2*Math.PI),w=R*(a+(b-a)*t);if(d<=R&&d>=R-w)dot(i,j,t<=p?1:.16)}};
|
|
647
|
-
switch(c.pat){case"months":colChart(D.months);break;case"hours":colChart(D.hours);break;case"sweep":ring(1,.06,.5);break;case"ring":ring(c.pct||0,.1,.5);break;
|
|
648
|
-
case"lanes":for(let k=0;k<(c.n||5);k++){const i=Math.round((k+.5)*cols/(c.n||5)),s0=Math.floor(rand(k,3)*nb*.45),e0=nb-Math.floor(rand(k,7)*nb*.3);for(let q=0;q<nb;q++)dot(i,j0+q,q>=s0&&q<e0?1:.14)}break;
|
|
649
|
-
case"share":{const f=c.frac||0;let n=0;for(const j of band)for(let i=0;i<cols;i++){dot(i,j,rand(i,j)<f?1:.16);n++}break}
|
|
650
|
-
case"stripes":{let acc=0;const cut=(c.shares||[1]).map(v=>acc+=v);for(const j of band)for(let i=0;i<cols;i++){const f=(i+.5)/cols,k=cut.findIndex(v=>f<=v+1e-9);dot(i,j,[1,.62,.42,.3,.18][k<0?4:k])}break}
|
|
651
|
-
case"rain":for(const j of band)for(let i=0;i<cols;i++){const v=rand(i,j);dot(i,j,v>.58?.95:v>.3?.35:.1)}break;
|
|
652
|
-
case"field":for(const j of band)for(let i=0;i<cols;i++){const f=(i/cols+(j-j0)/nb)/2;dot(i,j,.15+.85*Math.pow(rand(i,j),1.6)*f*1.4)}break}x.globalAlpha=1}
|
|
653
|
-
/* Save a card as a 1080x1350 PNG. Drawn with canvas on this machine; nothing is uploaded. */
|
|
654
|
-
function wrapText(g,t,maxW){const words=String(t).split(" "),out=[];let l="";for(const w of words){const n=l?l+" "+w:w;if(g.measureText(n).width>maxW&&l){out.push(l);l=w}else l=n}if(l)out.push(l);return out}
|
|
655
|
-
function paint(g,W,H,c){g.fillStyle=c.bg;g.fillRect(0,0,W,H);drawOn(g,W,H,c);const P=W*.08,F="Geist, ui-sans-serif, system-ui, -apple-system, Helvetica, Arial, sans-serif";g.fillStyle=c.fg;g.textBaseline="alphabetic";
|
|
656
|
-
const grad=()=>{const gr=g.createLinearGradient(P,0,W-P,0);[["#4F88FF",0],["#B0ACFE",.38],["#FB65C2",.72],["#FD2980",1]].forEach(([k,o])=>gr.addColorStop(o,k));return gr};
|
|
657
|
-
/* footer */const fs=W*.034,fy=H-W*.07;g.globalAlpha=.8;g.font="600 "+fs+"px "+F;const r=W*.019;g.lineWidth=W*.008;g.strokeStyle=grad();g.beginPath();g.arc(P+r,fy-fs*.35,r,0,7);g.stroke();g.fillText("Hour",P+r*2+W*.016,fy);g.font="400 "+fs+"px ui-monospace, SFMono-Regular, Menlo, monospace";g.textAlign="right";g.fillText("usehour.com",W-P,fy);g.textAlign="left";g.globalAlpha=1;
|
|
658
|
-
let y=fy-W*.085;
|
|
659
|
-
if(c.summary){const cw=(W-2*P-W*.05)/2,rowH=W*.2;const rows=Math.ceil(c.stats.length/2);let top=y-rows*rowH+W*.02;
|
|
660
|
-
c.stats.forEach((s,k)=>{const x=P+(k%2)*(cw+W*.05),yy=top+Math.floor(k/2)*rowH;g.fillStyle=c.fg;g.globalAlpha=1;g.fillRect(x,yy,cw,W*.0035);g.globalAlpha=.75;g.font="400 "+W*.034+"px "+F;g.fillText(s[0],x,yy+W*.05);g.globalAlpha=1;g.font="650 "+W*.062+"px "+F;wrapText(g,s[1],cw).slice(0,2).forEach((l,i)=>g.fillText(l,x,yy+W*.115+i*W*.062))});
|
|
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}
|
|
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;
|
|
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));
|
|
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*.023,gap=W*.009,pw=W*.135,dx=W-P-pw-10*d-9*gap;c.rows.forEach((w,k)=>{const yy=W*.29+k*W*.083;g.fillStyle=c.fg;g.font="500 "+W*.04+"px "+F;g.fillText(w[0],P,yy);g.font="600 "+W*.04+"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})}}
|
|
920
|
+
${PAINT_JS}
|
|
666
921
|
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()}
|
|
667
922
|
let cur=0,timer=null,t0=0,paused=false;const DUR=6000;
|
|
668
923
|
const $=id=>document.getElementById(id),slot=$("slot"),bars=$("bars"),grid=$("grid");
|
|
@@ -682,13 +937,84 @@ $("play").onclick=()=>openStory(0);$("skip").onclick=closeStory;$("replay").oncl
|
|
|
682
937
|
$("stp").onclick=$("hl").onclick=()=>show(cur-1);$("stf").onclick=$("hr").onclick=()=>go(cur+1);$("stz").onclick=togglePause;$("sts").onclick=()=>save(D.cards[cur]);
|
|
683
938
|
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
939
|
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()});
|
|
940
|
+
(D.heads||[]).forEach(t=>{const li=document.createElement("li");li.textContent=t;$("heads").appendChild(li)});if(!D.canSave)document.body.classList.add("nosave");
|
|
685
941
|
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
942
|
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
943
|
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};
|
|
944
|
+
if($("saveall"))$("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
945
|
${api ? CLAIM_JS : ""}
|
|
946
|
+
/* From the profile carousel: usehour.com/@you/cards#card-3 opens that card. */
|
|
947
|
+
const HM=location.hash.match(/^#card-([0-9]+)$/);if(HM&&+HM[1]<D.cards.length)openStory(+HM[1]);
|
|
948
|
+
HourAvatar.scan(document);
|
|
690
949
|
</script></body></html>`;
|
|
691
950
|
}
|
|
951
|
+
/* @shared:end ----------------------------------------------------------- */
|
|
952
|
+
|
|
953
|
+
/* ------------------------------------------------------------ --explain
|
|
954
|
+
Prints, in this terminal only, what was counted and what was left out, and
|
|
955
|
+
the busiest twenty minutes the way versions before 0.4.0 counted them. No
|
|
956
|
+
prompt text is printed; project folders are shown by their last name only.
|
|
957
|
+
Nothing is saved or sent. */
|
|
958
|
+
function explain(summaries) {
|
|
959
|
+
const pad = (x, n) => { const t = String(x); return (t.length > n - 2 ? t.slice(0, n - 3) + "…" : t).padEnd(n); };
|
|
960
|
+
const tool = (s) => (s.src === "claude" ? "Claude Code" : "Codex");
|
|
961
|
+
const started = (s) => (s.sub ? "sub-agent" : s.how === "script" ? `program (${s.via || "?"})` : `you (${s.via || "?"})`);
|
|
962
|
+
const topModel = (s) => { const e = Object.entries(s.models).sort((a, b) => b[1] - a[1])[0]; return e ? e[0] : "–"; };
|
|
963
|
+
const folder = (s) => (s.cwd ? path.basename(s.cwd) : "–");
|
|
964
|
+
const withP = summaries.filter((s) => s.prompts.length);
|
|
965
|
+
const own = withP.filter((s) => !s.sub && s.how !== "script"), prog = withP.filter((s) => !s.sub && s.how === "script"), subs = withP.filter((s) => s.sub);
|
|
966
|
+
const by = (arr, f) => Object.entries(arr.reduce((a, s) => { const k = f(s); a[k] = (a[k] || 0) + 1; return a; }, {})).sort((a, b) => b[1] - a[1]).map(([k, v]) => `${k} ${v}`).join(", ");
|
|
967
|
+
const before = compute(summaries.map((s) => ({ ...s, prompts: [...s.prompts], ids: [...s.ids] })), { all: true });
|
|
968
|
+
const now = compute(summaries.map((s) => ({ ...s, prompts: [...s.prompts], ids: [...s.ids] })));
|
|
969
|
+
const L = (t = "") => console.log(" " + t);
|
|
970
|
+
L(); L(B("What Hour counted, and what it left out")); L(DIM("Printed here only. Nothing is saved or sent. No prompt text is shown.")); L();
|
|
971
|
+
L(B("Sessions with at least one prompt"));
|
|
972
|
+
L(` Started by you ${pad(own.length, 6)} ${DIM(by(own, (s) => tool(s) + " " + (s.via || "?")))}`);
|
|
973
|
+
L(` Started by a program ${pad(prog.length, 6)} ${DIM(by(prog, (s) => tool(s) + " " + (s.via || "?")))} ${prog.length ? "left out from 0.4.0" : ""}`);
|
|
974
|
+
L(` Sub-agents ${pad(subs.length, 6)} ${DIM("always left out")}`);
|
|
975
|
+
L();
|
|
976
|
+
const models = (arr) => { const mc = {}; arr.forEach((s) => { for (const [k, v] of Object.entries(s.models)) mc[k] = (mc[k] || 0) + v; }); const t = Object.values(mc).reduce((a, b) => a + b, 0) || 1; return Object.entries(mc).sort((a, b) => b[1] - a[1]).slice(0, 4).map(([k, v]) => `${k} ${pct(v / t)}% (${v})`).join(", ") || "–"; };
|
|
977
|
+
L(B("Models, one count per prompt"));
|
|
978
|
+
L(` Your sessions ${models(own)}`);
|
|
979
|
+
if (prog.length) L(` Program-started ${models(prog)}`);
|
|
980
|
+
L();
|
|
981
|
+
/* The most agents at work at one moment: counting everything (as before
|
|
982
|
+
0.4.0), and counting only your own sessions. */
|
|
983
|
+
const peakAll = peakAtOnce(withP.filter((s) => !s.sub)), peakOwn = peakAtOnce(own);
|
|
984
|
+
const rows = new Map(peakAll.active.map((s) => [s, s.prompts.length]));
|
|
985
|
+
const when = (p) => (p.at ? DIM(" at " + p.at.replace("T", " ")) : "");
|
|
986
|
+
/* What the 0.3.x card showed: sessions with a prompt inside any 20 minutes. */
|
|
987
|
+
const ev = []; withP.filter((s) => !s.sub).forEach((s) => s.prompts.forEach((p) => { const t = Date.parse(p.ts || ""); if (t) ev.push([t, s]); }));
|
|
988
|
+
ev.sort((a, b) => a[0] - b[0]); let old = 0; const win = new Map();
|
|
989
|
+
for (let i = 0, j = 0; i < ev.length; i++) { win.set(ev[i][1], (win.get(ev[i][1]) || 0) + 1); while (ev[i][0] - ev[j][0] > 20 * 60e3) { const k = ev[j][1], v = win.get(k) - 1; v ? win.set(k, v) : win.delete(k); j++; } old = Math.max(old, win.size); }
|
|
990
|
+
L(B("Agents at once: most agents working at the same moment"));
|
|
991
|
+
L(` The 0.3.x card said ${old}${DIM(" (sessions with a prompt inside 20 minutes, everything counted)")}`);
|
|
992
|
+
L(` Counting everything ${peakAll.max}${when(peakAll)}`);
|
|
993
|
+
L(` Only yours (0.4.0) ${peakOwn.max > MAX_AT_ONCE ? `hidden (above ${MAX_AT_ONCE})` : peakOwn.max}${when(peakOwn)}`);
|
|
994
|
+
L(DIM(" A session counts as working from your prompt until the agent's last step before your next one."));
|
|
995
|
+
if (rows.size) {
|
|
996
|
+
/* Sessions that look the same are grouped: 30 runs of one script are one row. */
|
|
997
|
+
const g = new Map();
|
|
998
|
+
rows.forEach((n, s) => { const k = [tool(s), started(s), folder(s), topModel(s)].join("\t"); const x = g.get(k) || { k, sessions: 0, prompts: 0 }; x.sessions++; x.prompts += n; g.set(k, x); });
|
|
999
|
+
const G = [...g.values()].sort((a, b) => b.sessions - a.sessions || b.prompts - a.prompts);
|
|
1000
|
+
L(); L(DIM(" Working at that moment, counting everything:"));
|
|
1001
|
+
L(DIM(" " + pad("Sessions", 10) + pad("Tool", 13) + pad("Started by", 28) + pad("Project folder", 24) + pad("Model", 24) + "Prompts"));
|
|
1002
|
+
G.slice(0, 25).forEach((x) => { const [t, st, f, mo] = x.k.split("\t"); L(" " + pad(x.sessions, 10) + pad(t, 13) + pad(st, 28) + pad(f, 24) + pad(mo, 24) + x.prompts); });
|
|
1003
|
+
if (G.length > 25) L(DIM(` …and ${G.length - 25} more rows`));
|
|
1004
|
+
}
|
|
1005
|
+
L();
|
|
1006
|
+
if (before && now) {
|
|
1007
|
+
const v = (m) => (m.sessions.total ? pct(m.proof.sessionsAskedToVerify / m.sessions.total) + "%" : "–");
|
|
1008
|
+
const h = (m, k) => { const x = m.promptProfile.find((y) => y.key === k); return x ? pct(x.value) + "%" : "–"; };
|
|
1009
|
+
const top = (m) => (m.models[0] ? m.models[0].name : "–");
|
|
1010
|
+
L(B("Your numbers, before 0.4.0 → now"));
|
|
1011
|
+
[["Sessions", before.sessions.total, now.sessions.total], ["Prompts", before.prompts.total, now.prompts.total], ["Agents at once", before.parallelMax, now.parallelMax || "hidden"], ["Hour score", (hourScore(before) || {}).total ?? "–", (hourScore(now) || {}).total ?? "–"], ["Made it prove it", v(before), v(now)], ["Briefing", h(before, "brief"), h(now, "brief")], ["Model", top(before), top(now)], ["Builder type", TYPES[builderType(before)].name, TYPES[builderType(now)].name]]
|
|
1012
|
+
.forEach(([k, a, b]) => L(` ${pad(k, 18)}${pad(a, 26)}→ ${b}`));
|
|
1013
|
+
L();
|
|
1014
|
+
}
|
|
1015
|
+
L(DIM("Copy what you're happy to share. Project folder names are yours to blank out."));
|
|
1016
|
+
L();
|
|
1017
|
+
}
|
|
692
1018
|
|
|
693
1019
|
/* ------------------------------------------------------------------ the run */
|
|
694
1020
|
const B = (s) => `\x1b[1m${s}\x1b[0m`, DIM = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
@@ -714,7 +1040,7 @@ function openFile(f) {
|
|
|
714
1040
|
async function main() {
|
|
715
1041
|
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; };
|
|
716
1042
|
if (flag("--help") || flag("-h")) {
|
|
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`);
|
|
1043
|
+
console.log(`Hour Wrapped ${VERSION}\n\n npx usehour [--yes] [--no-open] [--no-share] [--out DIR] [--claude-dir DIR] [--codex-dir DIR] [--explain]\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 --explain show, in this terminal only, what was counted and what was left out\n`);
|
|
718
1044
|
return;
|
|
719
1045
|
}
|
|
720
1046
|
const home = os.homedir();
|
|
@@ -732,8 +1058,9 @@ async function main() {
|
|
|
732
1058
|
const bytes = found.reduce((a, s) => a + s.files.reduce((b, f) => { try { return b + fs.statSync(f).size; } catch { return b; } }, 0), 0);
|
|
733
1059
|
console.log(" This reads your session logs, on this machine only:");
|
|
734
1060
|
for (const s of found) console.log(` ${s.label.padEnd(12)} ${s.files.length} files ${DIM(s.dir)}`);
|
|
735
|
-
console.log(`\n
|
|
736
|
-
console.log(`
|
|
1061
|
+
console.log(`\n ${B("Private by design.")} It keeps counts and labels only. No prompt you wrote,`);
|
|
1062
|
+
console.log(` no code and no file contents leave this computer, and this script never`);
|
|
1063
|
+
console.log(` connects to the internet. (Try it with Wi-Fi off: it still works.)\n`);
|
|
737
1064
|
if (!flag("--yes")) {
|
|
738
1065
|
if (!process.stdin.isTTY) { console.log(" Run this in a terminal (or add --yes)."); process.exitCode = 1; return; }
|
|
739
1066
|
const a = await ask(` Read ${(bytes / 1e9).toFixed(1)} GB of logs and make your cards? [y/N] `);
|
|
@@ -751,12 +1078,13 @@ async function main() {
|
|
|
751
1078
|
if (next >= jobs.length) { w.terminate(); if (--active === 0) resolve(); return; }
|
|
752
1079
|
const job = jobs[next++];
|
|
753
1080
|
w.once("message", (r) => { if (r.ok) summaries.push(r.s); done++; doneBytes += job.size; tick(); feed(w); });
|
|
754
|
-
w.postMessage({ file: job.file, src: job.src, examples:
|
|
1081
|
+
w.postMessage({ file: job.file, src: job.src, examples: false });
|
|
755
1082
|
};
|
|
756
1083
|
workers.forEach((w) => { active++; feed(w); });
|
|
757
1084
|
});
|
|
758
1085
|
if (process.stdout.isTTY) process.stdout.write("\r" + " ".repeat(60) + "\r");
|
|
759
1086
|
console.log(` Read ${jobs.length} files in ${Math.round((Date.now() - t0) / 1000)}s.`);
|
|
1087
|
+
if (flag("--explain")) { explain(summaries); return; }
|
|
760
1088
|
const m = compute(summaries);
|
|
761
1089
|
if (!m) { console.log("\n Found the logs but no prompts in them. Nothing to make cards from.\n"); return; }
|
|
762
1090
|
const C = cards(m, "");
|
|
@@ -764,7 +1092,9 @@ async function main() {
|
|
|
764
1092
|
fs.mkdirSync(outDir, { recursive: true });
|
|
765
1093
|
fs.writeFileSync(path.join(outDir, "wrapped.json"), JSON.stringify(m, null, 2));
|
|
766
1094
|
const page = path.join(outDir, "index.html");
|
|
767
|
-
|
|
1095
|
+
/* Since 0.4.0 no prompt text goes on a profile: only counts and labels.
|
|
1096
|
+
(pickExamples stays for anyone who wants to see their best prompts.) */
|
|
1097
|
+
const examples = [];
|
|
768
1098
|
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
1099
|
fs.writeFileSync(page, pageHTML(m, C, { api: share ? (opt("--api") || API_DEFAULT).replace(/\/+$/, "") : null, examples, suggest: suggest.length >= 3 ? suggest : "" }));
|
|
770
1100
|
const type = TYPES[builderType(m)].name;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "usehour",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
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" },
|