codex-snapshots 0.1.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.
@@ -0,0 +1,106 @@
1
+ const DEFAULT_API_URL = "http://127.0.0.1:8787";
2
+ const params = new URLSearchParams(window.location.search);
3
+ const shareId = params.get("id") || "";
4
+ const apiUrl = normalizeApiUrl(params.get("api") || localStorage.getItem("codex-snapshots.api") || DEFAULT_API_URL);
5
+
6
+ const title = document.getElementById("share-title");
7
+ const meta = document.getElementById("share-meta");
8
+ const content = document.getElementById("share-content");
9
+
10
+ loadShare().catch((error) => {
11
+ title.textContent = "快照暂不可用";
12
+ meta.textContent = apiUrl;
13
+ content.innerHTML = `<div class="empty">${escapeHtml(error instanceof Error ? error.message : String(error))}</div>`;
14
+ });
15
+
16
+ async function loadShare() {
17
+ if (!shareId) {
18
+ title.textContent = "缺少分享 ID";
19
+ meta.textContent = "请打开带有 ?id=snap_... 的链接。";
20
+ content.innerHTML = '<div class="empty">没有提供分享 ID。</div>';
21
+ return;
22
+ }
23
+
24
+ localStorage.setItem("codex-snapshots.api", apiUrl);
25
+
26
+ const response = await fetch(`${apiUrl}/api/snapshots/${encodeURIComponent(shareId)}`, {
27
+ cache: "no-store",
28
+ });
29
+ const payload = await response.json().catch(() => ({}));
30
+
31
+ if (!response.ok) {
32
+ throw new Error(payload.error || `无法从 ${apiUrl} 加载快照`);
33
+ }
34
+
35
+ renderSnapshot(payload);
36
+ }
37
+
38
+ function renderSnapshot(payload) {
39
+ const snapshot = payload.snapshot || {};
40
+ const share = payload.share || {};
41
+ const turns = Array.isArray(snapshot.turns) ? snapshot.turns : [];
42
+
43
+ title.textContent = share.title || snapshot.title || "快照";
44
+ meta.textContent = [
45
+ share.engineLabel || snapshot.engineLabel || "Codex",
46
+ share.id || snapshot.id || "未知",
47
+ `${share.turnCount ?? turns.length} 条记录`,
48
+ `已脱敏:${(share.redacted ?? snapshot.redacted) ? "是" : "否"}`,
49
+ apiUrl,
50
+ ].join(" | ");
51
+
52
+ content.innerHTML = turns.length
53
+ ? turns.map(renderTurn).join("")
54
+ : '<div class="empty">这个快照没有可分享的对话记录。</div>';
55
+ }
56
+
57
+ function renderTurn(turn) {
58
+ const role = turn.kind === "tool" ? "tool" : turn.role === "user" ? "user" : "assistant";
59
+ const body = turn.kind === "tool"
60
+ ? `<details class="tool-details" open><summary>工具${turn.name ? ` / ${escapeHtml(turn.name)}` : ""}</summary><pre>${escapeHtml(turn.text || "")}</pre></details>`
61
+ : `${turn.html || renderPlainText(turn.text)}${renderImages(turn.images || [])}`;
62
+
63
+ return `<article class="turn ${escapeHtml(role)}"><div class="message-card"><div class="body">${sanitizeClientHtml(body)}</div></div></article>`;
64
+ }
65
+
66
+ function renderPlainText(value) {
67
+ return String(value || "")
68
+ .split(/\n{2,}/)
69
+ .map((block) => `<p>${escapeHtml(block).replace(/\n/g, "<br>")}</p>`)
70
+ .join("");
71
+ }
72
+
73
+ function renderImages(images) {
74
+ if (!Array.isArray(images) || !images.length) {
75
+ return "";
76
+ }
77
+
78
+ return `<div class="attachment-grid">${images.map((image, index) => {
79
+ const label = `${image.mimeType || "image"}${image.size ? ` / ${image.size}` : ""}`;
80
+ if (!image.src) {
81
+ return `<figure class="image-attachment image-unavailable"><div>${escapeHtml(image.unavailableReason || "图片暂不可用")}</div><figcaption>${escapeHtml(label)}</figcaption></figure>`;
82
+ }
83
+ return `<figure class="image-attachment"><img src="${escapeHtml(image.src)}" alt="${escapeHtml(image.alt || `图片附件 ${index + 1}`)}" decoding="async"><figcaption>${escapeHtml(label)}</figcaption></figure>`;
84
+ }).join("")}</div>`;
85
+ }
86
+
87
+ function sanitizeClientHtml(value) {
88
+ return String(value)
89
+ .replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "")
90
+ .replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, "")
91
+ .replace(/<(?:iframe|object|embed)\b[^>]*>[\s\S]*?<\/(?:iframe|object|embed)>/gi, "")
92
+ .replace(/\s+on[a-z]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, "");
93
+ }
94
+
95
+ function escapeHtml(value) {
96
+ return String(value ?? "")
97
+ .replace(/&/g, "&amp;")
98
+ .replace(/</g, "&lt;")
99
+ .replace(/>/g, "&gt;")
100
+ .replace(/"/g, "&quot;")
101
+ .replace(/'/g, "&#39;");
102
+ }
103
+
104
+ function normalizeApiUrl(value) {
105
+ return String(value || DEFAULT_API_URL).trim().replace(/\/+$/, "");
106
+ }
@@ -0,0 +1,605 @@
1
+ :root {
2
+ --ink: #16191f;
3
+ --muted: #687386;
4
+ --line: #d9dee4;
5
+ --paper: #f4f0e7;
6
+ --panel: #fffdf8;
7
+ --panel-soft: #faf7ef;
8
+ --blue: #255f82;
9
+ --green: #0c6958;
10
+ --red: #ad3728;
11
+ --amber: #a56d13;
12
+ --shadow-soft: 0 24px 70px -58px rgba(22, 25, 31, 0.5);
13
+ --grid-strong: rgba(22, 25, 31, 0.065);
14
+ --grid-soft: rgba(22, 25, 31, 0.038);
15
+ }
16
+
17
+ * {
18
+ box-sizing: border-box;
19
+ }
20
+
21
+ body {
22
+ margin: 0;
23
+ min-height: 100vh;
24
+ color: var(--ink);
25
+ background:
26
+ linear-gradient(90deg, var(--grid-strong) 1px, transparent 1px),
27
+ linear-gradient(var(--grid-soft) 1px, transparent 1px),
28
+ var(--paper);
29
+ background-size: 24px 24px;
30
+ font-family: "Iowan Old Style", "Palatino Linotype", Georgia, "Songti SC", "Noto Serif CJK SC", "Source Han Serif SC", serif;
31
+ text-rendering: optimizeLegibility;
32
+ -webkit-font-smoothing: antialiased;
33
+ }
34
+
35
+ a {
36
+ color: inherit;
37
+ text-decoration: none;
38
+ }
39
+
40
+ button,
41
+ input {
42
+ font: inherit;
43
+ }
44
+
45
+ code,
46
+ pre,
47
+ .eyebrow,
48
+ .status-pill,
49
+ .button,
50
+ label span,
51
+ .share-meta,
52
+ .shot-kicker,
53
+ .shot-meta {
54
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "PingFang SC", "Microsoft YaHei", monospace;
55
+ }
56
+
57
+ .shell,
58
+ .share-shell {
59
+ width: min(1360px, calc(100vw - 32px));
60
+ margin: 0 auto;
61
+ padding: 34px 0 72px;
62
+ }
63
+
64
+ .hero {
65
+ display: grid;
66
+ min-height: min(760px, calc(100vh - 48px));
67
+ align-items: center;
68
+ gap: clamp(28px, 5vw, 72px);
69
+ grid-template-columns: minmax(0, 0.82fr) minmax(520px, 1.18fr);
70
+ }
71
+
72
+ .hero-copy {
73
+ max-width: 620px;
74
+ }
75
+
76
+ .eyebrow {
77
+ margin: 0 0 12px;
78
+ color: var(--blue);
79
+ font-size: 12px;
80
+ font-weight: 900;
81
+ letter-spacing: 0.08em;
82
+ text-transform: none;
83
+ }
84
+
85
+ h1,
86
+ h2,
87
+ p {
88
+ margin-top: 0;
89
+ }
90
+
91
+ h1 {
92
+ margin-bottom: 24px;
93
+ font-size: 92px;
94
+ line-height: 1.16;
95
+ letter-spacing: 0;
96
+ }
97
+
98
+ h2 {
99
+ margin-bottom: 14px;
100
+ font-size: 34px;
101
+ line-height: 1.25;
102
+ letter-spacing: 0;
103
+ }
104
+
105
+ .lede,
106
+ .status-panel p,
107
+ .commands p {
108
+ color: var(--muted);
109
+ font-size: 20px;
110
+ line-height: 1.7;
111
+ }
112
+
113
+ .actions {
114
+ display: flex;
115
+ flex-wrap: wrap;
116
+ gap: 12px;
117
+ margin-top: 34px;
118
+ }
119
+
120
+ .button {
121
+ display: inline-flex;
122
+ min-height: 46px;
123
+ align-items: center;
124
+ justify-content: center;
125
+ border: 1px solid rgba(22, 25, 31, 0.14);
126
+ border-radius: 8px;
127
+ background: var(--panel);
128
+ padding: 0 18px;
129
+ color: var(--ink);
130
+ font-size: 13px;
131
+ font-weight: 900;
132
+ text-transform: none;
133
+ transition: transform 160ms ease, border-color 160ms ease, background-color 160ms ease;
134
+ }
135
+
136
+ .button:hover {
137
+ border-color: rgba(22, 25, 31, 0.34);
138
+ transform: translateY(-1px);
139
+ }
140
+
141
+ .button.primary {
142
+ border-color: #07111f;
143
+ background: #07111f;
144
+ color: white;
145
+ }
146
+
147
+ .product-shot {
148
+ display: grid;
149
+ min-height: 560px;
150
+ overflow: hidden;
151
+ border: 3px solid var(--ink);
152
+ background: rgba(255, 253, 248, 0.76);
153
+ box-shadow: 0 34px 110px -74px rgba(22, 25, 31, 0.8);
154
+ grid-template-columns: minmax(240px, 0.33fr) minmax(0, 0.67fr);
155
+ }
156
+
157
+ .shot-sidebar,
158
+ .shot-main {
159
+ min-width: 0;
160
+ padding: 26px;
161
+ }
162
+
163
+ .shot-sidebar {
164
+ border-right: 3px solid var(--ink);
165
+ background: rgba(250, 247, 239, 0.68);
166
+ }
167
+
168
+ .shot-title {
169
+ margin-bottom: 18px;
170
+ font-size: 34px;
171
+ font-weight: 900;
172
+ line-height: 1;
173
+ }
174
+
175
+ .shot-search {
176
+ height: 42px;
177
+ margin-bottom: 18px;
178
+ border: 1px solid var(--line);
179
+ background: var(--panel);
180
+ }
181
+
182
+ .shot-row {
183
+ display: flex;
184
+ min-height: 48px;
185
+ align-items: center;
186
+ gap: 12px;
187
+ border-bottom: 1px solid rgba(22, 25, 31, 0.1);
188
+ color: var(--muted);
189
+ }
190
+
191
+ .shot-row span {
192
+ width: 14px;
193
+ height: 14px;
194
+ border: 2px solid currentColor;
195
+ border-radius: 4px;
196
+ }
197
+
198
+ .shot-row.active {
199
+ color: var(--ink);
200
+ }
201
+
202
+ .shot-main {
203
+ display: flex;
204
+ flex-direction: column;
205
+ gap: 24px;
206
+ }
207
+
208
+ .shot-top {
209
+ display: flex;
210
+ align-items: flex-start;
211
+ justify-content: space-between;
212
+ gap: 18px;
213
+ border-bottom: 3px solid var(--ink);
214
+ padding-bottom: 18px;
215
+ }
216
+
217
+ .shot-top > div:first-child {
218
+ min-width: 0;
219
+ flex: 1;
220
+ }
221
+
222
+ .shot-heading {
223
+ font-size: 40px;
224
+ font-weight: 900;
225
+ line-height: 1.24;
226
+ }
227
+
228
+ .shot-switches {
229
+ display: flex;
230
+ flex: 0 0 auto;
231
+ gap: 6px;
232
+ }
233
+
234
+ .shot-switches span {
235
+ width: 34px;
236
+ height: 28px;
237
+ border: 1px solid var(--line);
238
+ background: var(--panel);
239
+ }
240
+
241
+ .shot-switches .on {
242
+ border-color: rgba(12, 105, 88, 0.38);
243
+ background: #e6f6f1;
244
+ }
245
+
246
+ .shot-meta {
247
+ border: 1px solid var(--line);
248
+ background: var(--panel);
249
+ padding: 14px;
250
+ color: var(--muted);
251
+ font-size: 13px;
252
+ font-weight: 900;
253
+ }
254
+
255
+ .shot-bubble {
256
+ max-width: 78%;
257
+ padding: 18px 22px;
258
+ font-size: 22px;
259
+ line-height: 1.45;
260
+ }
261
+
262
+ .shot-bubble.user {
263
+ align-self: flex-end;
264
+ border: 1px solid #cfe6e2;
265
+ border-radius: 18px;
266
+ background: #eef9f6;
267
+ }
268
+
269
+ .shot-bubble.assistant {
270
+ align-self: flex-start;
271
+ }
272
+
273
+ .status-grid {
274
+ display: grid;
275
+ gap: 18px;
276
+ grid-template-columns: repeat(2, minmax(0, 1fr));
277
+ }
278
+
279
+ .status-panel,
280
+ .commands,
281
+ .share-header,
282
+ .empty {
283
+ border: 1px solid rgba(22, 25, 31, 0.12);
284
+ background: rgba(255, 253, 248, 0.9);
285
+ box-shadow: var(--shadow-soft);
286
+ }
287
+
288
+ .status-panel,
289
+ .commands {
290
+ padding: 26px;
291
+ }
292
+
293
+ .panel-heading {
294
+ display: flex;
295
+ align-items: center;
296
+ justify-content: space-between;
297
+ gap: 12px;
298
+ margin-bottom: 18px;
299
+ }
300
+
301
+ .status-pill {
302
+ display: inline-flex;
303
+ min-height: 32px;
304
+ align-items: center;
305
+ border: 1px solid rgba(22, 25, 31, 0.12);
306
+ border-radius: 999px;
307
+ padding: 0 12px;
308
+ background: var(--panel-soft);
309
+ color: var(--muted);
310
+ font-size: 12px;
311
+ font-weight: 900;
312
+ text-transform: none;
313
+ }
314
+
315
+ .status-pill.ready {
316
+ border-color: rgba(12, 105, 88, 0.24);
317
+ background: #e9f7f3;
318
+ color: var(--green);
319
+ }
320
+
321
+ .status-pill.error {
322
+ border-color: rgba(173, 55, 40, 0.24);
323
+ background: #fff0ec;
324
+ color: var(--red);
325
+ }
326
+
327
+ pre {
328
+ max-width: 100%;
329
+ overflow: auto;
330
+ margin: 22px 0 0;
331
+ border: 1px solid #253043;
332
+ border-radius: 8px;
333
+ background: #111722;
334
+ color: #edf4ff;
335
+ padding: 16px;
336
+ font-size: 13px;
337
+ line-height: 1.6;
338
+ }
339
+
340
+ .share-form {
341
+ display: grid;
342
+ gap: 12px;
343
+ margin-top: 22px;
344
+ }
345
+
346
+ label {
347
+ display: grid;
348
+ gap: 7px;
349
+ }
350
+
351
+ label span {
352
+ color: var(--muted);
353
+ font-size: 12px;
354
+ font-weight: 900;
355
+ text-transform: none;
356
+ }
357
+
358
+ input {
359
+ min-height: 46px;
360
+ width: 100%;
361
+ border: 1px solid var(--line);
362
+ border-radius: 8px;
363
+ background: white;
364
+ padding: 0 13px;
365
+ color: var(--ink);
366
+ }
367
+
368
+ input:focus {
369
+ border-color: var(--blue);
370
+ outline: 3px solid rgba(37, 95, 130, 0.14);
371
+ }
372
+
373
+ .commands {
374
+ display: grid;
375
+ align-items: start;
376
+ gap: 22px;
377
+ margin-top: 18px;
378
+ grid-template-columns: minmax(220px, 0.35fr) minmax(0, 0.65fr);
379
+ }
380
+
381
+ .share-shell {
382
+ max-width: 1220px;
383
+ }
384
+
385
+ .share-header {
386
+ border-bottom: 3px solid var(--ink);
387
+ padding: 28px;
388
+ }
389
+
390
+ .share-header h1 {
391
+ margin-bottom: 18px;
392
+ font-size: 72px;
393
+ }
394
+
395
+ .share-meta {
396
+ margin: 0;
397
+ color: var(--muted);
398
+ font-size: 13px;
399
+ font-weight: 900;
400
+ }
401
+
402
+ .turns {
403
+ display: grid;
404
+ gap: 34px;
405
+ margin-top: 34px;
406
+ }
407
+
408
+ .turn {
409
+ display: flex;
410
+ min-width: 0;
411
+ }
412
+
413
+ .turn.user {
414
+ justify-content: flex-end;
415
+ }
416
+
417
+ .turn.assistant,
418
+ .turn.tool {
419
+ justify-content: flex-start;
420
+ }
421
+
422
+ .message-card {
423
+ min-width: 0;
424
+ max-width: min(960px, 76%);
425
+ }
426
+
427
+ .turn.user .message-card {
428
+ border: 1px solid #d6e9e5;
429
+ border-radius: 18px;
430
+ background: #eef9f6;
431
+ padding: 20px 28px;
432
+ box-shadow: var(--shadow-soft);
433
+ }
434
+
435
+ .turn.tool .message-card {
436
+ width: min(960px, 86%);
437
+ border: 1px solid #efd99f;
438
+ border-radius: 8px;
439
+ background: #fff8df;
440
+ padding: 16px 18px;
441
+ }
442
+
443
+ .body {
444
+ min-width: 0;
445
+ color: var(--ink);
446
+ font-size: 19px;
447
+ line-height: 1.75;
448
+ overflow-wrap: anywhere;
449
+ }
450
+
451
+ .body > *:first-child {
452
+ margin-top: 0;
453
+ }
454
+
455
+ .body > *:last-child {
456
+ margin-bottom: 0;
457
+ }
458
+
459
+ .body code {
460
+ border: 1px solid rgba(22, 25, 31, 0.12);
461
+ border-radius: 6px;
462
+ background: rgba(22, 25, 31, 0.06);
463
+ padding: 0.08rem 0.34rem;
464
+ font-size: 0.9em;
465
+ }
466
+
467
+ .body pre,
468
+ .tool-details pre {
469
+ max-width: 100%;
470
+ overflow: auto;
471
+ border: 1px solid #253043;
472
+ border-radius: 8px;
473
+ background: #111722;
474
+ color: #edf4ff;
475
+ padding: 16px;
476
+ font: 13px/1.58 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
477
+ }
478
+
479
+ .attachment-grid {
480
+ display: grid;
481
+ gap: 18px;
482
+ margin-top: 24px;
483
+ }
484
+
485
+ .image-attachment {
486
+ margin: 0;
487
+ min-width: 0;
488
+ }
489
+
490
+ .image-attachment img {
491
+ display: block;
492
+ max-width: 100%;
493
+ max-height: 540px;
494
+ border: 1px solid rgba(22, 25, 31, 0.18);
495
+ border-radius: 8px;
496
+ background: #fff;
497
+ object-fit: contain;
498
+ }
499
+
500
+ .image-attachment figcaption {
501
+ margin-top: 10px;
502
+ color: var(--muted);
503
+ font: 800 14px/1.35 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
504
+ }
505
+
506
+ .image-unavailable {
507
+ border: 1px dashed var(--line);
508
+ border-radius: 8px;
509
+ padding: 16px;
510
+ color: var(--muted);
511
+ }
512
+
513
+ .empty {
514
+ padding: 22px;
515
+ color: var(--muted);
516
+ }
517
+
518
+ @media (max-width: 980px) {
519
+ .hero,
520
+ .status-grid,
521
+ .commands {
522
+ grid-template-columns: 1fr;
523
+ }
524
+
525
+ .hero {
526
+ min-height: 0;
527
+ padding-top: 28px;
528
+ }
529
+
530
+ .product-shot {
531
+ min-height: 460px;
532
+ grid-template-columns: 1fr;
533
+ }
534
+
535
+ .shot-sidebar {
536
+ display: none;
537
+ }
538
+
539
+ h1 {
540
+ font-size: 78px;
541
+ }
542
+
543
+ .shot-heading {
544
+ font-size: 40px;
545
+ }
546
+
547
+ .share-header h1 {
548
+ font-size: 64px;
549
+ }
550
+ }
551
+
552
+ @media (max-width: 680px) {
553
+ .shell,
554
+ .share-shell {
555
+ width: min(100vw - 20px, 1360px);
556
+ padding-top: 18px;
557
+ }
558
+
559
+ .actions {
560
+ display: grid;
561
+ }
562
+
563
+ .product-shot {
564
+ min-height: 420px;
565
+ }
566
+
567
+ .shot-main,
568
+ .status-panel,
569
+ .commands,
570
+ .share-header {
571
+ padding: 18px;
572
+ }
573
+
574
+ .shot-top {
575
+ display: grid;
576
+ }
577
+
578
+ h1 {
579
+ font-size: 54px;
580
+ }
581
+
582
+ h2 {
583
+ font-size: 28px;
584
+ }
585
+
586
+ .shot-heading {
587
+ font-size: 36px;
588
+ }
589
+
590
+ .share-header h1 {
591
+ font-size: 44px;
592
+ }
593
+
594
+ .shot-bubble,
595
+ .message-card,
596
+ .turn.user .message-card,
597
+ .turn.tool .message-card {
598
+ width: 100%;
599
+ max-width: 100%;
600
+ }
601
+
602
+ .body {
603
+ font-size: 17px;
604
+ }
605
+ }