ghost-reader 0.0.1__py3-none-any.whl

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.
Files changed (39) hide show
  1. ghost_reader/.release-version +1 -0
  2. ghost_reader/__init__.py +3 -0
  3. ghost_reader/agent_loader.py +64 -0
  4. ghost_reader/cli.py +1124 -0
  5. ghost_reader/constants.py +75 -0
  6. ghost_reader/defaults/__init__.py +1 -0
  7. ghost_reader/defaults/personas/__init__.py +1 -0
  8. ghost_reader/defaults/personas/dex.yaml +30 -0
  9. ghost_reader/defaults/personas/elena.yaml +30 -0
  10. ghost_reader/defaults/personas/mara.yaml +30 -0
  11. ghost_reader/defaults/personas/pip.yaml +30 -0
  12. ghost_reader/defaults/personas/rook.yaml +30 -0
  13. ghost_reader/defaults/templates/__init__.py +1 -0
  14. ghost_reader/defaults/templates/blog-review.html +384 -0
  15. ghost_reader/defaults/templates/report.html +1293 -0
  16. ghost_reader/dialogue.py +283 -0
  17. ghost_reader/errors.py +2 -0
  18. ghost_reader/feedback_store.py +56 -0
  19. ghost_reader/io.py +59 -0
  20. ghost_reader/models.py +227 -0
  21. ghost_reader/paths.py +68 -0
  22. ghost_reader/project.py +277 -0
  23. ghost_reader/release.py +56 -0
  24. ghost_reader/report.py +264 -0
  25. ghost_reader/reviews.py +89 -0
  26. ghost_reader/revision.py +165 -0
  27. ghost_reader/round.py +155 -0
  28. ghost_reader/server.py +281 -0
  29. ghost_reader/sync.py +112 -0
  30. ghost_reader/telemetry.py +111 -0
  31. ghost_reader/time.py +20 -0
  32. ghost_reader/validators.py +66 -0
  33. ghost_reader/verify.py +255 -0
  34. ghost_reader-0.0.1.dist-info/METADATA +221 -0
  35. ghost_reader-0.0.1.dist-info/RECORD +39 -0
  36. ghost_reader-0.0.1.dist-info/WHEEL +5 -0
  37. ghost_reader-0.0.1.dist-info/entry_points.txt +2 -0
  38. ghost_reader-0.0.1.dist-info/licenses/LICENSE +21 -0
  39. ghost_reader-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1293 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <link rel="icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" type="image/png">
7
+ <title>Ghost Reader Report</title>
8
+ <style>
9
+ /* ═══════════════════════════════════════════════
10
+ Design tokens — warm editorial manuscript review
11
+ ═══════════════════════════════════════════════ */
12
+ :root {
13
+ color-scheme: light;
14
+ /* Backgrounds */
15
+ --bg: #FAF9F5;
16
+ --panel: #FFFFFF;
17
+ /* Text */
18
+ --ink: #141413;
19
+ --text-body: #3D3D3A;
20
+ --muted: #87867F;
21
+ /* Borders */
22
+ --line: #D1CFC5;
23
+ --line-light: #F0EEE6;
24
+ /* Accents */
25
+ --accent: #D97757;
26
+ --accent-hover: #B85C3E;
27
+ --accent-soft: rgba(217, 119, 87, 0.12);
28
+ --accent-strong: rgba(217, 119, 87, 0.18);
29
+ /* Semantic */
30
+ --strength: #788C5D;
31
+ --strength-soft: rgba(120, 140, 93, 0.16);
32
+ --concern: #B04A3F;
33
+ --concern-soft: rgba(176, 74, 63, 0.10);
34
+ --concern-strong: rgba(176, 74, 63, 0.16);
35
+ --warning: #C78E3F;
36
+ /* Persona dots */
37
+ --dot-mara: #788C5D;
38
+ --dot-dex: #B04A3F;
39
+ --dot-pip: #D97757;
40
+ --dot-elena: #87867F;
41
+ --dot-rook: #C78E3F;
42
+ /* Persona score bars */
43
+ --score-engagement: #788C5D;
44
+ --score-character: #D97757;
45
+ --score-pacing: #3D3D3A;
46
+ --score-genre-fit: #E3DACC;
47
+ /* Typography */
48
+ --serif: ui-serif, Georgia, "Times New Roman", Times, serif;
49
+ --sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
50
+ --mono: ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace;
51
+ /* Spacing — 4pt grid */
52
+ --sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px;
53
+ --sp-5: 24px; --sp-6: 32px; --sp-7: 48px; --sp-8: 64px;
54
+ /* Radius */
55
+ --radius-xs: 4px;
56
+ --radius-row: 8px;
57
+ --radius-panel: 12px;
58
+ --radius-pill: 999px;
59
+ /* Borders */
60
+ --border: 1.5px solid var(--line);
61
+ --border-light: 1px solid var(--line-light);
62
+ /* Shadows — warm tint */
63
+ --shadow-sm: 0 1px 2px rgba(20,20,19,0.06);
64
+ --shadow-md: 0 4px 10px rgba(20,20,19,0.08);
65
+ --shadow-lg: 0 12px 28px rgba(20,20,19,0.12);
66
+ /* Transitions */
67
+ --t-fast: 0.12s ease;
68
+ --t-card: 0.15s ease;
69
+ }
70
+
71
+ /* ═══════════════════════════════════════════════
72
+ Reset & base
73
+ ═══════════════════════════════════════════════ */
74
+ * { box-sizing: border-box; }
75
+ body {
76
+ margin: 0;
77
+ background: var(--bg);
78
+ color: var(--ink);
79
+ font-family: var(--sans);
80
+ font-size: 15px;
81
+ line-height: 1.55;
82
+ -webkit-font-smoothing: antialiased;
83
+ text-rendering: optimizeLegibility;
84
+ }
85
+ h1, h2, h3 {
86
+ margin: 0;
87
+ font-family: var(--serif);
88
+ font-weight: 500;
89
+ line-height: 1.2;
90
+ }
91
+ h1 { font-size: 20px; letter-spacing: -0.01em; white-space: nowrap; }
92
+ h2 { font-size: 17px; letter-spacing: -0.01em; margin-bottom: 10px; }
93
+ h3 { font-size: 14px; margin-bottom: 8px; }
94
+ p { margin: 0 0 10px; }
95
+
96
+ /* ═══════════════════════════════════════════════
97
+ Buttons — 5 states, 36px height
98
+ ═══════════════════════════════════════════════ */
99
+ button {
100
+ display: inline-flex; align-items: center; justify-content: center;
101
+ height: 36px; padding: 0 16px;
102
+ font-family: var(--sans); font-size: 14px; font-weight: 500;
103
+ border-radius: var(--radius-row); border: var(--border);
104
+ background: var(--panel); color: var(--ink);
105
+ cursor: pointer;
106
+ transition: background var(--t-fast), border-color var(--t-fast), box-shadow var(--t-fast);
107
+ }
108
+ button:hover { background: var(--line-light); border-color: var(--text-body); }
109
+ button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
110
+ button:active { transform: scale(0.97); }
111
+ button:disabled { opacity: 0.45; cursor: default; }
112
+
113
+ button.primary {
114
+ background: var(--accent); color: var(--panel);
115
+ border-color: var(--accent);
116
+ }
117
+ button.primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
118
+ button.primary:disabled { opacity: 0.5; cursor: default; transform: none; }
119
+
120
+ button.pill {
121
+ border-radius: var(--radius-pill);
122
+ height: 28px; padding: 4px 12px; font-size: 12px;
123
+ }
124
+
125
+ /* ═══════════════════════════════════════════════
126
+ Inputs
127
+ ═══════════════════════════════════════════════ */
128
+ textarea, input[type="text"] {
129
+ border: var(--border);
130
+ border-radius: var(--radius-row);
131
+ padding: 5px 8px;
132
+ font-family: var(--sans); font-size: 12px;
133
+ width: 100%;
134
+ color: var(--ink); background: var(--panel);
135
+ transition: border-color var(--t-fast), box-shadow var(--t-fast);
136
+ }
137
+ textarea::placeholder, input[type="text"]::placeholder { color: var(--muted); }
138
+ textarea:focus, input[type="text"]:focus {
139
+ border-color: var(--accent); outline: none;
140
+ box-shadow: 0 0 0 3px rgba(217, 119, 87, 0.15);
141
+ }
142
+ textarea { min-height: 36px; resize: vertical; }
143
+ pre {
144
+ margin: 0; white-space: pre-wrap; overflow-wrap: anywhere;
145
+ background: var(--line-light);
146
+ border: var(--border); border-radius: var(--radius-xs);
147
+ padding: 12px; font-size: 12px; max-height: 240px; overflow: auto;
148
+ }
149
+
150
+ /* ═══════════════════════════════════════════════
151
+ Top bar — sticky
152
+ ═══════════════════════════════════════════════ */
153
+ .topbar {
154
+ position: sticky; top: 0; z-index: 10;
155
+ display: flex; justify-content: space-between; align-items: center;
156
+ gap: 12px; padding: 10px clamp(16px, 4vw, 40px);
157
+ border-bottom: var(--border); background: var(--panel);
158
+ flex-wrap: wrap;
159
+ }
160
+ .topbar-left { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
161
+ .topbar-right { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
162
+ .topbar-btn:disabled { opacity: 0.5; cursor: default; transform: none; }
163
+
164
+ .selection-badge {
165
+ font-size: 13px; color: var(--muted); white-space: nowrap;
166
+ }
167
+ .selection-badge.has-selection { color: var(--accent); font-weight: 600; }
168
+
169
+ /* ═══════════════════════════════════════════════
170
+ Meta pills
171
+ ═══════════════════════════════════════════════ */
172
+ .meta {
173
+ display: flex; flex-wrap: wrap; gap: 8px; margin: 0;
174
+ color: var(--muted); font-size: 13px;
175
+ }
176
+ .pill {
177
+ border: var(--border); border-radius: var(--radius-pill);
178
+ padding: 3px 10px; background: var(--bg);
179
+ font-size: 12px; font-family: var(--mono); letter-spacing: 0.04em;
180
+ }
181
+
182
+ .round-indicator {
183
+ font-size: 12px; color: var(--muted);
184
+ padding: 2px 10px; border: var(--border); border-radius: var(--radius-pill);
185
+ white-space: nowrap; background: var(--bg);
186
+ }
187
+ .round-nav-btn {
188
+ font-size: 12px; min-height: 26px; padding: 2px 8px;
189
+ border-radius: var(--radius-xs); line-height: 1;
190
+ }
191
+ .round-nav-btn:disabled { opacity: 0.35; cursor: default; transform: none; }
192
+ .hint-text { font-size: 11px; color: var(--muted); margin-left: 2px; }
193
+
194
+ /* ═══════════════════════════════════════════════
195
+ Two-panel main — content + annotation sidebar
196
+ ═══════════════════════════════════════════════ */
197
+ main {
198
+ display: grid;
199
+ grid-template-columns: 1fr 360px;
200
+ }
201
+ .story-panel {
202
+ padding: 28px clamp(20px, 5vw, 52px) 40px;
203
+ overflow-y: auto; max-height: calc(100vh - 57px);
204
+ position: sticky; top: 57px;
205
+ }
206
+ .annotation-panel {
207
+ padding: 20px 22px 40px;
208
+ overflow-y: auto; max-height: calc(100vh - 57px);
209
+ position: sticky; top: 57px;
210
+ border-left: var(--border); background: var(--bg);
211
+ }
212
+
213
+ /* ═══════════════════════════════════════════════
214
+ Story text — editorial reading surface
215
+ ═══════════════════════════════════════════════ */
216
+ .story-content { max-width: 65ch; margin: 0 auto; }
217
+ .story-content h2 {
218
+ font-family: var(--serif); font-weight: 500; margin-bottom: 4px;
219
+ }
220
+ .story-content > .summary {
221
+ font-family: var(--sans); font-size: 13px; color: var(--muted);
222
+ margin-bottom: 16px;
223
+ }
224
+ .story-paragraph {
225
+ padding: 10px 12px; margin: 2px 0;
226
+ border-radius: var(--radius-row);
227
+ border-left: 3px solid transparent;
228
+ transition: background var(--t-fast), border-color var(--t-fast), box-shadow var(--t-fast);
229
+ scroll-margin-top: 16px; cursor: default;
230
+ }
231
+ .story-paragraph.has-comments {
232
+ border-left-color: var(--strength);
233
+ background: rgba(120, 140, 93, 0.06);
234
+ cursor: pointer;
235
+ }
236
+ .story-paragraph.has-comments:hover {
237
+ background: rgba(120, 140, 93, 0.10);
238
+ box-shadow: 0 0 0 1.5px rgba(120, 140, 93, 0.12);
239
+ }
240
+ .story-paragraph:target,
241
+ .story-paragraph.focused,
242
+ .story-paragraph.synced {
243
+ background: rgba(217, 119, 87, 0.12);
244
+ border-left-color: var(--accent);
245
+ box-shadow: 0 0 0 2px rgba(217, 119, 87, 0.2);
246
+ }
247
+ .story-paragraph.has-selected {
248
+ background: rgba(217, 119, 87, 0.08);
249
+ border-left-color: var(--accent);
250
+ }
251
+ .story-paragraph p {
252
+ font-family: var(--serif); font-size: 16px; line-height: 1.7;
253
+ overflow-wrap: anywhere; margin: 0;
254
+ }
255
+ .paragraph-label {
256
+ font-family: var(--mono); font-size: 11px;
257
+ color: var(--muted); margin-bottom: 4px;
258
+ user-select: none; letter-spacing: 0.05em;
259
+ }
260
+ .comment-count {
261
+ display: inline-block;
262
+ font-family: var(--mono); font-size: 11px;
263
+ color: var(--strength); margin-left: 6px; font-weight: 600;
264
+ }
265
+ .selected-count {
266
+ display: inline-block;
267
+ font-family: var(--mono); font-size: 11px;
268
+ color: var(--accent); margin-left: 4px;
269
+ }
270
+
271
+ /* ═══════════════════════════════════════════════
272
+ Persona filter
273
+ ═══════════════════════════════════════════════ */
274
+ .persona-filter {
275
+ display: flex; gap: 6px; margin-bottom: 18px; flex-wrap: wrap;
276
+ position: sticky; top: 0; background: var(--bg);
277
+ padding-bottom: 10px; z-index: 2;
278
+ }
279
+ .persona-filter button {
280
+ height: 28px; padding: 4px 12px;
281
+ font-size: 12px; border-radius: var(--radius-pill);
282
+ }
283
+ .persona-filter button.active {
284
+ background: var(--accent); color: var(--panel); border-color: var(--accent);
285
+ }
286
+
287
+ /* ═══════════════════════════════════════════════
288
+ Reader type tag
289
+ ═══════════════════════════════════════════════ */
290
+ .reader-type-tag {
291
+ font-size: 11px; color: var(--muted); font-style: italic;
292
+ }
293
+ .focus-primary { color: var(--ink); }
294
+ .focus-secondary { color: var(--muted); }
295
+
296
+ /* ═══════════════════════════════════════════════
297
+ Persona profile card
298
+ ═══════════════════════════════════════════════ */
299
+ .persona-profile-card {
300
+ border: var(--border); border-radius: var(--radius-row);
301
+ padding: 10px 14px; margin-bottom: 16px; background: var(--panel);
302
+ }
303
+ .profile-header {
304
+ display: flex; align-items: center; gap: 8px;
305
+ margin-bottom: 8px; flex-wrap: wrap;
306
+ }
307
+ .profile-focus {
308
+ font-size: 12px; color: var(--muted); margin-bottom: 6px;
309
+ }
310
+ .profile-scores {
311
+ display: flex; gap: 14px; margin-bottom: 8px; flex-wrap: wrap;
312
+ }
313
+ .profile-score-bar {
314
+ display: flex; align-items: center; gap: 4px;
315
+ }
316
+ .profile-score-label {
317
+ font-family: var(--mono); font-size: 10px; font-weight: 600;
318
+ color: var(--muted); width: 12px;
319
+ }
320
+ .profile-score-meter {
321
+ width: 44px; height: 5px; background: var(--line-light);
322
+ border-radius: var(--radius-pill); overflow: hidden;
323
+ }
324
+ .profile-score-meter span {
325
+ display: block; height: 100%; border-radius: inherit;
326
+ }
327
+ .profile-score-value {
328
+ font-family: var(--mono); font-size: 10px;
329
+ color: var(--text-body); min-width: 16px;
330
+ }
331
+ .profile-summary {
332
+ font-size: 12px; color: var(--muted); font-style: italic; line-height: 1.4;
333
+ }
334
+
335
+ /* ═══════════════════════════════════════════════
336
+ Comment groups and cards
337
+ ═══════════════════════════════════════════════ */
338
+ .comment-group {
339
+ margin-bottom: 18px;
340
+ border: var(--border); border-radius: var(--radius-row);
341
+ background: var(--panel); overflow: hidden;
342
+ scroll-margin-top: 16px;
343
+ }
344
+ .comment-group.synced {
345
+ border-color: var(--accent);
346
+ box-shadow: 0 0 0 2px rgba(217, 119, 87, 0.15);
347
+ }
348
+ .comment-group-header {
349
+ padding: 10px 12px; background: var(--line-light);
350
+ border-bottom: var(--border-light);
351
+ display: flex; align-items: baseline; gap: 8px;
352
+ cursor: pointer; transition: background var(--t-fast);
353
+ }
354
+ .comment-group-header:hover { background: #E6E3DA; }
355
+ .paragraph-ref {
356
+ font-family: var(--mono); font-weight: 600; font-size: 13px;
357
+ color: var(--accent); flex-shrink: 0;
358
+ }
359
+ .paragraph-snippet {
360
+ font-size: 12px; color: var(--muted);
361
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
362
+ }
363
+ .group-meta {
364
+ font-family: var(--mono); font-size: 11px;
365
+ color: var(--muted); margin-left: auto; flex-shrink: 0;
366
+ }
367
+
368
+ .comment-card {
369
+ padding: 10px 12px; border-bottom: var(--border-light);
370
+ }
371
+ .comment-card:last-child { border-bottom: none; }
372
+ .comment-card.concern {
373
+ border-left: 3px solid var(--concern); padding-left: 9px;
374
+ }
375
+ .comment-card.strength {
376
+ border-left: 3px solid var(--strength); padding-left: 9px;
377
+ }
378
+ .comment-card.selected { background: var(--accent-soft); }
379
+ .comment-card.selected.concern { background: var(--concern-soft); }
380
+ .comment-card.filtered-out { display: none; }
381
+
382
+ .comment-card-head {
383
+ display: flex; align-items: center; gap: 8px;
384
+ margin-bottom: 6px; flex-wrap: wrap;
385
+ }
386
+ .persona-dot {
387
+ width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0;
388
+ }
389
+ .persona-dot.mara { background: var(--dot-mara); }
390
+ .persona-dot.dex { background: var(--dot-dex); }
391
+ .persona-dot.pip { background: var(--dot-pip); }
392
+ .persona-dot.elena { background: var(--dot-elena); }
393
+ .persona-dot.rook { background: var(--dot-rook); }
394
+ .persona-name { font-weight: 600; font-size: 13px; }
395
+ .item-id {
396
+ font-family: var(--mono); font-size: 11px; color: var(--muted);
397
+ }
398
+ .kind-badge {
399
+ font-family: var(--mono); font-size: 11px;
400
+ text-transform: uppercase; letter-spacing: .05em;
401
+ padding: 1px 6px; border-radius: var(--radius-xs); font-weight: 600;
402
+ }
403
+ .kind-badge.concern { color: var(--concern); background: var(--concern-soft); }
404
+ .kind-badge.strength { color: var(--strength); background: var(--strength-soft); }
405
+
406
+ .include-check {
407
+ display: inline-flex; align-items: center; gap: 5px;
408
+ font-size: 12px; font-weight: 600; color: var(--muted);
409
+ margin-left: auto; cursor: pointer; user-select: none;
410
+ padding: 3px 10px; border: var(--border); border-radius: var(--radius-pill);
411
+ transition: background var(--t-fast), color var(--t-fast), border-color var(--t-fast);
412
+ }
413
+ .include-check:has(input:checked) {
414
+ background: var(--accent-soft); color: var(--accent);
415
+ border-color: var(--accent);
416
+ }
417
+ .include-check:hover { border-color: var(--accent); }
418
+ .include-check input { margin: 0; cursor: pointer; accent-color: var(--accent); }
419
+
420
+ .comment-card-body { font-size: 13px; line-height: 1.45; overflow-wrap: anywhere; }
421
+ .comment-text { margin-bottom: 4px; }
422
+ .comment-effect { font-size: 12px; color: var(--concern); margin-bottom: 2px; }
423
+ .comment-hint { font-size: 12px; color: var(--accent); }
424
+
425
+ .comment-card-inputs {
426
+ margin-top: 8px; padding-top: 8px;
427
+ border-top: 1.5px dashed var(--line); display: grid; gap: 6px;
428
+ }
429
+ .input-row { display: flex; align-items: center; gap: 6px; }
430
+ .input-row label {
431
+ font-family: var(--mono); font-size: 11px;
432
+ color: var(--muted); white-space: nowrap; flex-shrink: 0; min-width: 52px;
433
+ }
434
+ .input-row input { flex: 1; min-width: 0; font-size: 12px; padding: 4px 8px; }
435
+
436
+ /* ═══════════════════════════════════════════════
437
+ Dialogue thread
438
+ ═══════════════════════════════════════════════ */
439
+ .dialogue-thread {
440
+ margin-top: 8px; padding: 8px 10px;
441
+ background: var(--line-light);
442
+ border-left: 3px solid var(--line); border-radius: var(--radius-xs);
443
+ }
444
+ .dialogue-thread .input-row {
445
+ margin-top: 8px; padding-top: 8px; border-top: 1.5px dashed var(--line);
446
+ }
447
+ .dialogue-turn { margin-bottom: 8px; }
448
+ .dialogue-turn:last-of-type { margin-bottom: 0; }
449
+ .dialogue-role {
450
+ font-family: var(--mono); font-size: 11px;
451
+ text-transform: uppercase; letter-spacing: .05em; margin-bottom: 2px;
452
+ }
453
+ .dialogue-role.user { color: var(--accent); font-weight: 600; }
454
+ .dialogue-role.persona { color: var(--muted); }
455
+ .dialogue-text { font-size: 12px; line-height: 1.4; white-space: pre-wrap; overflow-wrap: anywhere; }
456
+
457
+ /* ═══════════════════════════════════════════════
458
+ Toast
459
+ ═══════════════════════════════════════════════ */
460
+ .toast {
461
+ position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
462
+ background: var(--ink); color: var(--panel);
463
+ padding: 8px 18px; border-radius: var(--radius-pill);
464
+ font-family: var(--sans); font-size: 13px;
465
+ z-index: 100; pointer-events: none; opacity: 0;
466
+ transition: opacity 300ms ease;
467
+ }
468
+ .toast.show { opacity: 1; }
469
+
470
+ /* ═══════════════════════════════════════════════
471
+ Footer
472
+ ═══════════════════════════════════════════════ */
473
+ .report-footer {
474
+ margin-top: var(--sp-8); padding: 20px clamp(16px, 4vw, 40px);
475
+ border-top: 1px solid var(--line);
476
+ font-family: var(--mono); font-size: 12px; color: var(--muted);
477
+ text-align: center;
478
+ }
479
+
480
+ /* ═══════════════════════════════════════════════
481
+ Responsive
482
+ ═══════════════════════════════════════════════ */
483
+ @media (max-width: 860px) {
484
+ main { grid-template-columns: 1fr; }
485
+ .story-panel { position: static; max-height: none; padding-bottom: 20px; }
486
+ .annotation-panel { position: static; max-height: none; border-left: none; border-top: var(--border); }
487
+ .story-content { max-width: none; }
488
+ .topbar { flex-direction: column; align-items: flex-start; }
489
+ .topbar-right { width: 100%; }
490
+ .topbar-right button { flex: 1; }
491
+ }
492
+
493
+ /* ═══════════════════════════════════════════════
494
+ Reduced motion
495
+ ═══════════════════════════════════════════════ */
496
+ @media (prefers-reduced-motion: reduce) {
497
+ *, *::before, *::after {
498
+ animation-duration: 0.01ms !important;
499
+ animation-iteration-count: 1 !important;
500
+ transition-duration: 0.01ms !important;
501
+ }
502
+ .card:hover { transform: none; }
503
+ button:active { transform: none; }
504
+ }
505
+ </style>
506
+ </head>
507
+ <body>
508
+ <header class="topbar">
509
+ <div class="topbar-left">
510
+ <h1>Ghost Reader Report</h1>
511
+ <div class="meta" id="report-meta"></div>
512
+ </div>
513
+ <div class="topbar-right">
514
+ <button id="prev-round-btn" class="round-nav-btn" disabled>&#9664;</button>
515
+ <span class="round-indicator" id="round-indicator">Round 1</span>
516
+ <button id="next-round-btn" class="round-nav-btn" disabled>&#9654;</button>
517
+ <span class="selection-badge" id="selection-badge">No items selected</span>
518
+ <button id="copy-refine-btn" class="topbar-btn" onclick="copyRefinePrompt()" disabled
519
+ title="Iterate on review quality — keep selected items, regenerate the rest, continue dialogue">Refine Reviews</button>
520
+ <button id="copy-revision-btn" class="topbar-btn" onclick="copyRevisionPrompt()" disabled
521
+ title="Generate a story revision handoff prompt from selected feedback">Revise Story</button>
522
+ <button id="submit-feedback-btn" class="primary" onclick="submitFeedback()"
523
+ disabled style="display:none">Submit Feedback</button>
524
+ </div>
525
+ </header>
526
+ <main>
527
+ <section class="story-panel" id="story-panel"></section>
528
+ <section class="annotation-panel" id="annotation-panel"></section>
529
+ </main>
530
+ <footer class="report-footer" id="report-footer">
531
+ </footer>
532
+ <div class="toast" id="toast"></div>
533
+ <pre id="feedback-yaml" hidden></pre>
534
+ <pre id="refine-prompt" hidden></pre>
535
+ <pre id="revision-prompt" hidden></pre>
536
+ <script type="application/json" id="payload-data">__GHOST_READER_PAYLOAD__</script>
537
+ <script>
538
+ const payload = JSON.parse(document.getElementById("payload-data").textContent);
539
+ const meta = document.getElementById("report-meta");
540
+ const storyPanel = document.getElementById("story-panel");
541
+ const annotationPanel = document.getElementById("annotation-panel");
542
+ const yamlBox = document.getElementById("feedback-yaml");
543
+ const refinePromptBox = document.getElementById("refine-prompt");
544
+ const revisionPromptBox = document.getElementById("revision-prompt");
545
+ const copyRefineButton = document.getElementById("copy-refine-btn");
546
+ const copyRevisionButton = document.getElementById("copy-revision-btn");
547
+ const selectionBadge = document.getElementById("selection-badge");
548
+ const roundIndicator = document.getElementById("round-indicator");
549
+ const prevRoundBtn = document.getElementById("prev-round-btn");
550
+ const nextRoundBtn = document.getElementById("next-round-btn");
551
+ const reportFooter = document.getElementById("report-footer");
552
+ const toast = document.getElementById("toast");
553
+ const selectedFeedbackIds = new Set((payload.feedback_summary?.selected_items || []).map((item) => item.target_id));
554
+ const feedbackNotes = payload.feedback_summary?.user_notes || {};
555
+ const dialogueQuestions = {};
556
+ let activePersona = null;
557
+ let toastTimer = null;
558
+ let dialogueDebounce = null;
559
+
560
+ let currentRound = (payload.current_round != null) ? payload.current_round : 1;
561
+ let roundIds = payload.round_ids || [1];
562
+ let dialogueThreads = payload.dialogue_threads || {};
563
+ let storyUnit = payload.story_unit || '';
564
+ let projectRoot = (payload.commands && payload.commands.project_root) ? payload.commands.project_root : '';
565
+
566
+ function esc(value) {
567
+ return String(value ?? "").replace(/[&<>"']/g, (ch) => ({
568
+ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#039;"
569
+ }[ch]));
570
+ }
571
+
572
+ function formatFocus(focus) {
573
+ if (!focus) return "";
574
+ if (typeof focus === "string") return esc(focus);
575
+ if (typeof focus === "object") {
576
+ var parts = [];
577
+ if (Array.isArray(focus.primary) && focus.primary.length) {
578
+ parts.push('<span class="focus-primary">' + esc(focus.primary.join(", ")) + '</span>');
579
+ }
580
+ if (Array.isArray(focus.secondary) && focus.secondary.length) {
581
+ parts.push('<span class="focus-secondary">' + esc(focus.secondary.join(", ")) + '</span>');
582
+ }
583
+ return parts.join(" &bull; ");
584
+ }
585
+ return esc(String(focus));
586
+ }
587
+
588
+ function yamlQuote(value) {
589
+ return JSON.stringify(String(value ?? ""));
590
+ }
591
+
592
+ function yamlDump(obj, indent) {
593
+ indent = indent || 0;
594
+ var prefix = "";
595
+ for (var i = 0; i < indent; i++) { prefix += " "; }
596
+ if (obj === null || obj === undefined) return "null";
597
+ if (typeof obj === "string") return yamlQuote(obj);
598
+ if (typeof obj === "number" || typeof obj === "boolean") return String(obj);
599
+ if (Array.isArray(obj)) {
600
+ if (!obj.length) return "[]";
601
+ return obj.map(function(item) {
602
+ if (typeof item === "object" && item !== null) {
603
+ var itemLines = yamlDump(item, indent + 1).split("\n");
604
+ return prefix + "- " + itemLines[0].trim() + (itemLines.length > 1 ? "\n" + itemLines.slice(1).join("\n") : "");
605
+ }
606
+ return prefix + "- " + yamlDump(item, indent);
607
+ }).join("\n");
608
+ }
609
+ if (typeof obj === "object") {
610
+ var keys = Object.keys(obj);
611
+ if (!keys.length) return "{}";
612
+ return keys.map(function(key) {
613
+ var val = obj[key];
614
+ if (typeof val === "object" && val !== null) {
615
+ return prefix + key + ":\n" + yamlDump(val, indent + 1);
616
+ }
617
+ return prefix + key + ": " + yamlDump(val, indent);
618
+ }).join("\n");
619
+ }
620
+ return String(obj);
621
+ }
622
+
623
+ function expandLocators(item) {
624
+ const result = new Set();
625
+ for (const evidence of item.evidence || []) {
626
+ const locator = evidence.locator;
627
+ if (!locator) continue;
628
+ const rangeMatch = locator.match(/^([a-z]+)(\d+)\s*[-–]\s*([a-z]+)(\d+)$/i);
629
+ if (rangeMatch && rangeMatch[1].toLowerCase() === rangeMatch[3].toLowerCase()) {
630
+ const prefix = rangeMatch[1];
631
+ const start = Number(rangeMatch[2]);
632
+ const end = Number(rangeMatch[4]);
633
+ for (let i = start; i <= end; i += 1) result.add(prefix + i);
634
+ } else {
635
+ result.add(locator);
636
+ }
637
+ }
638
+ return result;
639
+ }
640
+
641
+ let _groupsCache = null;
642
+ function paragraphGroups() {
643
+ if (_groupsCache) return _groupsCache;
644
+ const source = payload.source_text || {};
645
+ const groups = [];
646
+ for (const para of source.paragraphs || []) {
647
+ const items = [];
648
+ for (const review of payload.reviews) {
649
+ for (const item of [...(review.strengths || []), ...(review.concerns || [])]) {
650
+ const kind = item.id.startsWith("s") ? "strength" : "concern";
651
+ const itemLocs = expandLocators(item);
652
+ const paraLocs = new Set(para.locators || []);
653
+ let matched = false;
654
+ for (const loc of itemLocs) {
655
+ if (paraLocs.has(loc)) { matched = true; break; }
656
+ }
657
+ if (matched) {
658
+ items.push({ review, item, kind, target: review.persona_id + ":" + item.id });
659
+ }
660
+ }
661
+ }
662
+ if (items.length) {
663
+ groups.push({
664
+ paragraphId: para.id,
665
+ text: para.text,
666
+ items
667
+ });
668
+ }
669
+ }
670
+ _groupsCache = groups;
671
+ return groups;
672
+ }
673
+
674
+ function selectedCountForParagraph(paraId) {
675
+ const groups = paragraphGroups();
676
+ const group = groups.find((g) => String(g.paragraphId) === String(paraId));
677
+ if (!group) return 0;
678
+ return group.items.filter((entry) => selectedFeedbackIds.has(entry.target)).length;
679
+ }
680
+
681
+ function renderStory() {
682
+ const source = payload.source_text || {};
683
+ if (!source.found || !source.paragraphs?.length) {
684
+ storyPanel.innerHTML = "";
685
+ return;
686
+ }
687
+ const groups = paragraphGroups();
688
+ const paraHasComments = new Set(groups.map((g) => String(g.paragraphId)));
689
+ const paragraphs = source.paragraphs.map((para) => {
690
+ const hasComments = paraHasComments.has(String(para.id));
691
+ const commentCount = groups.filter((g) => String(g.paragraphId) === String(para.id)).reduce((sum, g) => sum + g.items.length, 0);
692
+ const selCount = selectedCountForParagraph(para.id);
693
+ return '<div class="story-paragraph' +
694
+ (hasComments ? ' has-comments' : '') +
695
+ (selCount > 0 ? ' has-selected' : '') +
696
+ '" id="para-' + esc(para.id) + '" data-paragraph-id="' + esc(para.id) + '">' +
697
+ '<div class="paragraph-label">Paragraph ' + esc(para.id) +
698
+ (commentCount ? '<span class="comment-count">' + commentCount + ' annotation' + (commentCount !== 1 ? 's' : '') + '</span>' : '') +
699
+ (selCount ? '<span class="selected-count">(' + selCount + ' selected)</span>' : '') +
700
+ '</div>' +
701
+ '<p>' + esc(para.text) + '</p></div>';
702
+ }).join("");
703
+ storyPanel.innerHTML = '<div class="story-content"><h2>Story Text</h2><p class="summary">Hover a highlighted passage to see its annotations. Click to scroll the annotation panel.</p><div class="story-paragraphs">' + paragraphs + '</div></div>';
704
+ }
705
+
706
+ function renderDialogueThread(review, itemId, target) {
707
+ var thread = dialogueThreads[target] || dialogueThreads[itemId] || null;
708
+ var turns = (thread && thread.turns) ? thread.turns : [];
709
+ var threadId = (thread && thread.thread_id) ? thread.thread_id : target;
710
+ var dialogueQ = dialogueQuestions[threadId] || "";
711
+
712
+ var html = '<div class="dialogue-thread" data-thread-id="' + esc(threadId) + '">';
713
+ for (var i = 0; i < turns.length; i++) {
714
+ var turn = turns[i];
715
+ var userText = turn.user || "";
716
+ var personaText = turn.persona || "";
717
+ if (userText) {
718
+ html += '<div class="dialogue-turn"><div class="dialogue-role user">You</div><div class="dialogue-text">' + esc(userText) + '</div></div>';
719
+ }
720
+ if (personaText) {
721
+ html += '<div class="dialogue-turn"><div class="dialogue-role persona">' + esc(review.persona_name || review.persona_id) + '</div><div class="dialogue-text">' + esc(personaText) + '</div></div>';
722
+ }
723
+ if (!userText && !personaText && turn.text) {
724
+ html += '<div class="dialogue-turn"><div class="dialogue-text">' + esc(turn.text) + '</div></div>';
725
+ }
726
+ }
727
+ html += '<div class="input-row"><label for="ask-' + esc(target.replace(/:/g, "-")) + '">Ask ' + esc(review.persona_name || review.persona_id) + '</label><input type="text" id="ask-' + esc(target.replace(/:/g, "-")) + '" data-dialogue="' + esc(threadId) + '" placeholder="Follow-up question for this persona..." value="' + esc(dialogueQ) + '"></div>';
728
+ html += '</div>';
729
+ return html;
730
+ }
731
+
732
+ function renderPersonaProfileCard(personaId) {
733
+ if (!personaId) return '<div class="persona-profile-card" id="persona-profile-card" style="display:none"></div>';
734
+ var review = null;
735
+ for (var i = 0; i < payload.reviews.length; i++) {
736
+ if (payload.reviews[i].persona_id === personaId) {
737
+ review = payload.reviews[i];
738
+ break;
739
+ }
740
+ }
741
+ if (!review) return '<div class="persona-profile-card" id="persona-profile-card" style="display:none"></div>';
742
+
743
+ var scores = review.scores || {};
744
+ var scoreKeys = ["engagement", "character", "pacing", "genre_fit"];
745
+ var scoreLabels = { engagement: "E", character: "C", pacing: "P", genre_fit: "G" };
746
+ var scoreBars = scoreKeys.map(function(key) {
747
+ var val = Number(scores[key]) || 0;
748
+ var pct = Math.max(0, Math.min(10, val)) * 10;
749
+ return '<div class="profile-score-bar"><span class="profile-score-label">' + scoreLabels[key] + '</span><div class="profile-score-meter"><span style="width:' + pct + '%;background:var(--score-' + key.replace(/_/g, '-') + ')"></span></div><span class="profile-score-value">' + val + '</span></div>';
750
+ }).join("");
751
+
752
+ var wouldContinue = review.overall?.would_continue;
753
+ var summary = review.overall?.summary || "";
754
+
755
+ return '<div class="persona-profile-card" id="persona-profile-card">' +
756
+ '<div class="profile-header">' +
757
+ '<span class="persona-dot ' + esc(review.persona_id) + '"></span>' +
758
+ '<span class="persona-name">' + esc(review.persona_name || review.persona_id) + '</span>' +
759
+ '<span class="reader-type-tag">' + esc(review.reader_type || "") + '</span>' +
760
+ (wouldContinue != null
761
+ ? (wouldContinue ? '<span class="pill" style="background:var(--strength-soft);color:var(--strength);border-color:var(--strength)">would continue</span>' : '<span class="pill" style="background:var(--concern-soft);color:var(--concern);border-color:var(--concern)">would pause</span>')
762
+ : '') +
763
+ '</div>' +
764
+ '<div class="profile-focus">' + formatFocus(review.review_focus) + '</div>' +
765
+ '<div class="profile-scores">' + scoreBars + '</div>' +
766
+ (summary ? '<div class="profile-summary">' + esc(summary.length > 240 ? summary.substring(0, 240) + "..." : summary) + '</div>' : '') +
767
+ '</div>';
768
+ }
769
+
770
+ function renderAnnotationPanel() {
771
+ const groups = paragraphGroups();
772
+ if (!groups.length) {
773
+ annotationPanel.innerHTML = '<p class="summary">No review annotations found for the story text.</p>';
774
+ return;
775
+ }
776
+ const filterHtml = '<div class="persona-filter"><button data-filter="all" class="active pill">All</button>' +
777
+ payload.reviews.map((r) => '<button data-filter="' + esc(r.persona_id) + '" class="pill">' + esc(r.persona_name || r.persona_id) + '</button>').join("") +
778
+ '</div>';
779
+ const profileCardHtml = renderPersonaProfileCard(activePersona);
780
+ const groupHtml = groups.map((group) => {
781
+ const selCount = group.items.filter((entry) => selectedFeedbackIds.has(entry.target)).length;
782
+ const cards = group.items.map((entry) => {
783
+ const checked = selectedFeedbackIds.has(entry.target) ? " checked" : "";
784
+ const note = feedbackNotes[entry.target] || "";
785
+ const dialogueThreadHtml = renderDialogueThread(entry.review, entry.item.id, entry.target);
786
+ return '<div class="comment-card ' + entry.kind +
787
+ (checked ? ' selected' : '') +
788
+ '" data-target="' + esc(entry.target) + '" data-persona="' + esc(entry.review.persona_id) + '">' +
789
+ '<div class="comment-card-head">' +
790
+ '<span class="persona-dot ' + esc(entry.review.persona_id) + '"></span>' +
791
+ '<span class="persona-name">' + esc(entry.review.persona_name || entry.review.persona_id) + '</span>' +
792
+ '<span class="item-id">' + esc(entry.item.id) + '</span>' +
793
+ '<span class="kind-badge ' + entry.kind + '">' + (entry.kind === "strength" ? "Strength" : "Concern") + '</span>' +
794
+ (entry.review.reader_type ? '<span class="reader-type-tag">' + esc(entry.review.reader_type) + '</span>' : '') +
795
+ '<label class="include-check"><input type="checkbox" data-select="' + esc(entry.target) + '"' + checked + '> Include</label>' +
796
+ '</div>' +
797
+ '<div class="comment-card-body">' +
798
+ '<div class="comment-text">' + esc(entry.item.reason) + '</div>' +
799
+ (entry.item.reader_effect ? '<div class="comment-effect">Effect: ' + esc(entry.item.reader_effect) + '</div>' : '') +
800
+ (entry.item.revision_hint ? '<div class="comment-hint">Hint: ' + esc(entry.item.revision_hint) + '</div>' : '') +
801
+ '</div>' +
802
+ dialogueThreadHtml +
803
+ '<div class="comment-card-inputs">' +
804
+ '<div class="input-row">' +
805
+ '<label for="note-' + esc(entry.target.replace(/:/g, "-")) + '">Your note</label>' +
806
+ '<input type="text" id="note-' + esc(entry.target.replace(/:/g, "-")) + '" data-note="' + esc(entry.target) + '" placeholder="Add your feedback on this point…" value="' + esc(note) + '">' +
807
+ '</div>' +
808
+ '</div>' +
809
+ '</div>';
810
+ }).join("");
811
+ return '<div class="comment-group" id="group-p' + esc(group.paragraphId) + '" data-paragraph-id="' + esc(group.paragraphId) + '">' +
812
+ '<div class="comment-group-header" data-scroll-to="' + esc(group.paragraphId) + '">' +
813
+ '<span class="paragraph-ref">Para ' + esc(group.paragraphId) + '</span>' +
814
+ '<span class="paragraph-snippet">' + esc(group.text.substring(0, 110)) + '</span>' +
815
+ '<span class="group-meta">' + group.items.length + ' annotation' + (group.items.length !== 1 ? 's' : '') +
816
+ (selCount ? ', ' + selCount + ' selected' : '') + '</span>' +
817
+ '</div>' +
818
+ cards +
819
+ '</div>';
820
+ }).join("");
821
+ annotationPanel.innerHTML = filterHtml + profileCardHtml + groupHtml;
822
+ applyFilter();
823
+ }
824
+
825
+ function applyFilter() {
826
+ const cards = annotationPanel.querySelectorAll(".comment-card");
827
+ const groups = annotationPanel.querySelectorAll(".comment-group");
828
+ const filterButtons = annotationPanel.querySelectorAll(".persona-filter button");
829
+ filterButtons.forEach((btn) => {
830
+ btn.classList.toggle("active", btn.dataset.filter === (activePersona || "all"));
831
+ });
832
+
833
+ var profileCard = document.getElementById("persona-profile-card");
834
+ if (activePersona && activePersona !== "all") {
835
+ if (profileCard) {
836
+ profileCard.outerHTML = renderPersonaProfileCard(activePersona);
837
+ } else {
838
+ var filterEl = annotationPanel.querySelector(".persona-filter");
839
+ if (filterEl) {
840
+ filterEl.insertAdjacentHTML("afterend", renderPersonaProfileCard(activePersona));
841
+ }
842
+ }
843
+ } else {
844
+ if (profileCard) {
845
+ profileCard.style.display = "none";
846
+ }
847
+ }
848
+
849
+ if (!activePersona || activePersona === "all") {
850
+ cards.forEach((c) => c.classList.remove("filtered-out"));
851
+ groups.forEach((g) => { g.style.display = ""; });
852
+ return;
853
+ }
854
+ cards.forEach((c) => {
855
+ c.classList.toggle("filtered-out", c.dataset.persona !== activePersona);
856
+ });
857
+ groups.forEach((g) => {
858
+ const visible = g.querySelectorAll(".comment-card:not(.filtered-out)").length;
859
+ g.style.display = visible ? "" : "none";
860
+ });
861
+ }
862
+
863
+ function updateSelectedStates() {
864
+ document.querySelectorAll(".comment-card").forEach((card) => {
865
+ card.classList.toggle("selected", selectedFeedbackIds.has(card.dataset.target));
866
+ });
867
+ document.querySelectorAll(".story-paragraph").forEach((para) => {
868
+ const selCount = selectedCountForParagraph(para.dataset.paragraphId);
869
+ para.classList.toggle("has-selected", selCount > 0);
870
+ const selSpan = para.querySelector(".selected-count");
871
+ if (selSpan) {
872
+ selSpan.textContent = selCount > 0 ? "(" + selCount + " selected)" : "";
873
+ selSpan.style.display = selCount > 0 ? "" : "none";
874
+ }
875
+ });
876
+ document.querySelectorAll(".comment-group").forEach((group) => {
877
+ const selCount = selectedCountForParagraph(group.dataset.paragraphId);
878
+ const meta = group.querySelector(".group-meta");
879
+ if (meta) {
880
+ const visibleCards = group.querySelectorAll(".comment-card:not(.filtered-out)").length;
881
+ meta.textContent = visibleCards + ' annotation' + (visibleCards !== 1 ? 's' : '') +
882
+ (selCount ? ', ' + selCount + ' selected' : '');
883
+ }
884
+ });
885
+
886
+ const hasSelection = selectedFeedbackIds.size > 0;
887
+ copyRefineButton.disabled = !hasSelection;
888
+ copyRevisionButton.disabled = !hasSelection;
889
+ }
890
+
891
+ function buildFeedback() {
892
+ var itemUidMap = {};
893
+ for (var ri = 0; ri < payload.reviews.length; ri++) {
894
+ var review = payload.reviews[ri];
895
+ var allItems = (review.strengths || []).concat(review.concerns || []);
896
+ for (var ii = 0; ii < allItems.length; ii++) {
897
+ var revItem = allItems[ii];
898
+ var key = review.persona_id + ":" + revItem.id;
899
+ if (revItem.item_uid) itemUidMap[key] = revItem.item_uid;
900
+ }
901
+ }
902
+ const selectedTargets = new Set([...document.querySelectorAll("input[data-select]:checked")].map((input) => input.dataset.select));
903
+
904
+ const fb = {
905
+ schema_version: 2,
906
+ session_id: payload.session_id,
907
+ round_id: currentRound,
908
+ mode: "revise",
909
+ refinement_pass: 0,
910
+ selected_items: []
911
+ };
912
+
913
+ if (selectedTargets.size) {
914
+ for (const target of [...selectedTargets].sort()) {
915
+ const noteInput = document.querySelector('input[data-note="' + CSS.escape(target) + '"]');
916
+ const note = noteInput ? noteInput.value.trim() : (feedbackNotes[target] || "");
917
+
918
+ var threadId = target;
919
+ var dialogueQ = "";
920
+ var card = document.querySelector('.comment-card[data-target="' + CSS.escape(target) + '"]');
921
+ if (card) {
922
+ var dialInput = card.querySelector('input[data-dialogue]');
923
+ if (dialInput) {
924
+ dialogueQ = dialInput.value.trim();
925
+ threadId = dialInput.dataset.dialogue || target;
926
+ }
927
+ }
928
+
929
+ const item = {
930
+ target_id: target,
931
+ item_uid: itemUidMap[target] || target,
932
+ thread_id: threadId
933
+ };
934
+ if (note) item.note = note;
935
+ if (dialogueQ) item.dialogue_question = dialogueQ;
936
+ fb.selected_items.push(item);
937
+ }
938
+ } else {
939
+ fb.selected_items = [];
940
+ }
941
+ fb.helpfulness_rating = null;
942
+
943
+ const lines = [
944
+ "schema_version: 2",
945
+ "session_id: " + yamlQuote(payload.session_id),
946
+ "round_id: " + currentRound,
947
+ "mode: revise",
948
+ "refinement_pass: 0",
949
+ "selected_items:"
950
+ ];
951
+ if (!fb.selected_items.length) {
952
+ lines.push(" []");
953
+ } else {
954
+ for (const item of fb.selected_items) {
955
+ lines.push(" - target_id: " + yamlQuote(item.target_id));
956
+ lines.push(" item_uid: " + yamlQuote(item.item_uid));
957
+ lines.push(" thread_id: " + yamlQuote(item.thread_id));
958
+ if (item.note) lines.push(" note: " + yamlQuote(item.note));
959
+ if (item.dialogue_question) lines.push(" dialogue_question: " + yamlQuote(item.dialogue_question));
960
+ }
961
+ }
962
+ lines.push("helpfulness_rating: null");
963
+ yamlBox.textContent = lines.join("\n");
964
+
965
+ const badge = selectionBadge;
966
+ if (selectedTargets.size) {
967
+ badge.textContent = selectedTargets.size + " item" + (selectedTargets.size !== 1 ? "s" : "") + " selected";
968
+ badge.classList.add("has-selection");
969
+ } else {
970
+ badge.textContent = "No items selected";
971
+ badge.classList.remove("has-selection");
972
+ }
973
+
974
+ selectedFeedbackIds.clear();
975
+ for (const target of selectedTargets) {
976
+ selectedFeedbackIds.add(target);
977
+ }
978
+ updateSelectedStates();
979
+
980
+ if (fb.selected_items.length > 0) {
981
+ buildRefinePrompt(fb);
982
+ buildRevisionPrompt(fb);
983
+ }
984
+ updateSubmitButton();
985
+
986
+ return fb;
987
+ }
988
+
989
+ function buildRefinePrompt(fb) {
990
+ fb.mode = "refine";
991
+ fb.round_id = currentRound;
992
+ var threads = {};
993
+ if (typeof dialogueThreads !== "undefined") {
994
+ Object.keys(dialogueThreads).forEach(function(tid) {
995
+ threads[tid] = dialogueThreads[tid];
996
+ });
997
+ }
998
+ refinePromptBox.textContent =
999
+ "# Ghost Reader Refine Prompt\n\n" +
1000
+ "Action: refine\n" +
1001
+ "Project root: " + projectRoot + "\n" +
1002
+ "Session ID: " + payload.session_id + "\n" +
1003
+ "Round: " + currentRound + "\n" +
1004
+ "Story unit: " + (storyUnit || "") + "\n\n" +
1005
+ "```yaml\n" + yamlDump(fb) + "\n```\n\n" +
1006
+ (Object.keys(threads).length ? "Dialogue thread state:\n```yaml\n" + yamlDump(threads) + "\n```\n" : "");
1007
+ }
1008
+
1009
+ function buildRevisionPrompt(fb) {
1010
+ fb.mode = "revise";
1011
+ fb.round_id = currentRound;
1012
+ revisionPromptBox.textContent =
1013
+ "# Ghost Reader Revision Prompt\n\n" +
1014
+ "Action: revise\n" +
1015
+ "Project root: " + projectRoot + "\n" +
1016
+ "Session ID: " + payload.session_id + "\n" +
1017
+ "Round: " + currentRound + "\n" +
1018
+ "Story unit: " + (storyUnit || "") + "\n\n" +
1019
+ "```yaml\n" + yamlDump(fb) + "\n```\n";
1020
+ }
1021
+
1022
+ function copyRefinePrompt() {
1023
+ copyText(refinePromptBox.textContent).then(function() {
1024
+ showToast("Refine prompt copied — paste into an LLM with Ghost Reader skill");
1025
+ });
1026
+ }
1027
+
1028
+ function copyRevisionPrompt() {
1029
+ copyText(revisionPromptBox.textContent).then(function() {
1030
+ showToast("Revision prompt copied");
1031
+ });
1032
+ }
1033
+
1034
+ async function copyText(value) {
1035
+ if (navigator.clipboard && window.isSecureContext) {
1036
+ await navigator.clipboard.writeText(value);
1037
+ } else {
1038
+ const holder = document.createElement("textarea");
1039
+ holder.value = value;
1040
+ holder.style.position = "fixed";
1041
+ holder.style.opacity = "0";
1042
+ document.body.appendChild(holder);
1043
+ holder.select();
1044
+ document.execCommand("copy");
1045
+ holder.remove();
1046
+ }
1047
+ }
1048
+
1049
+ function showToast(message) {
1050
+ toast.textContent = message;
1051
+ toast.classList.add("show");
1052
+ clearTimeout(toastTimer);
1053
+ toastTimer = setTimeout(() => { toast.classList.remove("show"); }, 2000);
1054
+ }
1055
+
1056
+ function scrollToParagraph(paragraphId) {
1057
+ const el = document.getElementById("para-" + paragraphId);
1058
+ if (!el) return;
1059
+ document.querySelectorAll(".story-paragraph.focused").forEach((p) => p.classList.remove("focused"));
1060
+ el.classList.add("focused");
1061
+ el.scrollIntoView({ behavior: "smooth", block: "center" });
1062
+ }
1063
+
1064
+ function scrollToGroup(paragraphId) {
1065
+ const el = document.getElementById("group-p" + paragraphId);
1066
+ if (!el) return;
1067
+ el.scrollIntoView({ behavior: "smooth", block: "start" });
1068
+ }
1069
+
1070
+ function updateRoundUI() {
1071
+ roundIndicator.textContent = "Round " + currentRound;
1072
+ var idx = roundIds.indexOf(currentRound);
1073
+ prevRoundBtn.disabled = idx <= 0;
1074
+ nextRoundBtn.disabled = idx < 0 || idx >= roundIds.length - 1;
1075
+ }
1076
+
1077
+ function hoverPair(paraId) {
1078
+ var para = document.getElementById("para-" + paraId);
1079
+ var group = document.getElementById("group-p" + paraId);
1080
+ if (para) para.classList.add("synced");
1081
+ if (group) group.classList.add("synced");
1082
+ }
1083
+ function unhoverPair(paraId) {
1084
+ var para = document.getElementById("para-" + paraId);
1085
+ var group = document.getElementById("group-p" + paraId);
1086
+ if (para) para.classList.remove("synced");
1087
+ if (group) group.classList.remove("synced");
1088
+ }
1089
+ document.addEventListener("mouseenter", function(evt) {
1090
+ var para = evt.target.closest(".story-paragraph.has-comments");
1091
+ if (para) { hoverPair(para.dataset.paragraphId); return; }
1092
+ var group = evt.target.closest(".comment-group");
1093
+ if (group && group.dataset.paragraphId) { hoverPair(group.dataset.paragraphId); }
1094
+ }, true);
1095
+ document.addEventListener("mouseleave", function(evt) {
1096
+ var para = evt.target.closest(".story-paragraph.has-comments");
1097
+ if (para) { unhoverPair(para.dataset.paragraphId); return; }
1098
+ var group = evt.target.closest(".comment-group");
1099
+ if (group && group.dataset.paragraphId) { unhoverPair(group.dataset.paragraphId); }
1100
+ }, true);
1101
+
1102
+ var _scrollSyncBusy = false;
1103
+ var _scrollSyncTimer = null;
1104
+ function dominantVisibleParaId(container) {
1105
+ var children = container.querySelectorAll(".story-paragraph.has-comments, .comment-group");
1106
+ var best = null;
1107
+ var bestArea = 0;
1108
+ var viewTop = 0;
1109
+ var viewBottom = window.innerHeight || document.documentElement.clientHeight;
1110
+ for (var i = 0; i < children.length; i++) {
1111
+ var r = children[i].getBoundingClientRect();
1112
+ if (r.bottom < 0 || r.top > viewBottom) continue;
1113
+ var visibleTop = Math.max(r.top, viewTop);
1114
+ var visibleBottom = Math.min(r.bottom, viewBottom);
1115
+ var area = Math.max(0, visibleBottom - visibleTop);
1116
+ if (area > bestArea) { bestArea = area; best = children[i]; }
1117
+ }
1118
+ if (!best) return null;
1119
+ if (best.classList.contains("story-paragraph")) return best.dataset.paragraphId;
1120
+ if (best.classList.contains("comment-group")) return best.dataset.paragraphId;
1121
+ return null;
1122
+ }
1123
+
1124
+ function scrollToPair(paraId, sourcePanel) {
1125
+ if (!paraId) return;
1126
+ _scrollSyncBusy = true;
1127
+ clearTimeout(_scrollSyncTimer);
1128
+ if (sourcePanel !== storyPanel) {
1129
+ var para = document.getElementById("para-" + paraId);
1130
+ if (para) para.scrollIntoView({ behavior: "smooth", block: "start" });
1131
+ }
1132
+ if (sourcePanel !== annotationPanel) {
1133
+ var group = document.getElementById("group-p" + paraId);
1134
+ if (group) group.scrollIntoView({ behavior: "smooth", block: "start" });
1135
+ }
1136
+ _scrollSyncTimer = setTimeout(function() { _scrollSyncBusy = false; }, 600);
1137
+ }
1138
+
1139
+ storyPanel.addEventListener("scroll", function() {
1140
+ if (_scrollSyncBusy) return;
1141
+ var paraId = dominantVisibleParaId(storyPanel);
1142
+ if (paraId) scrollToPair(paraId, storyPanel);
1143
+ }, { passive: true });
1144
+ annotationPanel.addEventListener("scroll", function() {
1145
+ if (_scrollSyncBusy) return;
1146
+ var paraId = dominantVisibleParaId(annotationPanel);
1147
+ if (paraId) scrollToPair(paraId, annotationPanel);
1148
+ }, { passive: true });
1149
+
1150
+ function init() {
1151
+ renderStory();
1152
+ renderAnnotationPanel();
1153
+ buildFeedback();
1154
+ updateRoundUI();
1155
+ meta.innerHTML = [
1156
+ "Session " + esc(payload.session_id),
1157
+ storyUnit ? esc(storyUnit) : null,
1158
+ payload.reviews.length + " persona reviews"
1159
+ ].filter(Boolean).map((x) => '<span class="pill">' + x + '</span>').join("");
1160
+
1161
+ // Footer with generation metadata
1162
+ var generatedDate = payload.generated_at || new Date().toISOString().slice(0, 10);
1163
+ reportFooter.innerHTML = 'Ghost Reader &mdash; persona-driven fiction review &middot; session ' +
1164
+ esc(payload.session_id) + ' &middot; generated ' + esc(generatedDate);
1165
+ }
1166
+
1167
+ document.addEventListener("input", (event) => {
1168
+ if (event.target.matches("input[data-select]")) {
1169
+ const target = event.target.dataset.select;
1170
+ document.querySelectorAll('input[data-select="' + CSS.escape(target) + '"]').forEach((input) => {
1171
+ input.checked = event.target.checked;
1172
+ });
1173
+ if (event.target.checked) {
1174
+ selectedFeedbackIds.add(target);
1175
+ } else {
1176
+ selectedFeedbackIds.delete(target);
1177
+ }
1178
+ buildFeedback();
1179
+ }
1180
+ if (event.target.matches("input[data-note]")) {
1181
+ feedbackNotes[event.target.dataset.note] = event.target.value;
1182
+ buildFeedback();
1183
+ }
1184
+ if (event.target.matches("input[data-dialogue]")) {
1185
+ dialogueQuestions[event.target.dataset.dialogue] = event.target.value;
1186
+ clearTimeout(dialogueDebounce);
1187
+ dialogueDebounce = setTimeout(() => buildFeedback(), 400);
1188
+ }
1189
+ });
1190
+
1191
+ document.addEventListener("click", (event) => {
1192
+ const storyPara = event.target.closest(".story-paragraph.has-comments");
1193
+ if (storyPara) {
1194
+ const paraId = storyPara.dataset.paragraphId;
1195
+ scrollToGroup(paraId);
1196
+ return;
1197
+ }
1198
+ const groupHeader = event.target.closest(".comment-group-header");
1199
+ if (groupHeader && groupHeader.dataset.scrollTo) {
1200
+ scrollToParagraph(groupHeader.dataset.scrollTo);
1201
+ return;
1202
+ }
1203
+ if (event.target.matches(".persona-filter button")) {
1204
+ activePersona = event.target.dataset.filter === "all" ? null : event.target.dataset.filter;
1205
+ applyFilter();
1206
+ return;
1207
+ }
1208
+ });
1209
+
1210
+ var isServerMode = window.location.protocol === "http:" || window.location.protocol === "https:";
1211
+ var submitFeedbackBtn = document.getElementById("submit-feedback-btn");
1212
+
1213
+ function updateSubmitButton() {
1214
+ if (!isServerMode) return;
1215
+ submitFeedbackBtn.style.display = "";
1216
+ var hasSelection = selectedFeedbackIds.size > 0;
1217
+ submitFeedbackBtn.disabled = !hasSelection;
1218
+ if (hasSelection) {
1219
+ submitFeedbackBtn.textContent = "Submit Feedback (" + selectedFeedbackIds.size + ")";
1220
+ } else {
1221
+ submitFeedbackBtn.textContent = "Submit Feedback";
1222
+ }
1223
+ }
1224
+
1225
+ async function submitFeedback() {
1226
+ if (!isServerMode) return;
1227
+ var fb = buildFeedback();
1228
+ if (!fb.selected_items || !fb.selected_items.length) {
1229
+ showToast("No items selected for feedback");
1230
+ return;
1231
+ }
1232
+ submitFeedbackBtn.disabled = true;
1233
+ submitFeedbackBtn.textContent = "Submitting...";
1234
+ try {
1235
+ var resp = await fetch("/api/feedback", {
1236
+ method: "POST",
1237
+ headers: { "Content-Type": "application/json" },
1238
+ body: JSON.stringify(fb)
1239
+ });
1240
+ var data = await resp.json();
1241
+ if (!resp.ok || data.error) {
1242
+ throw new Error(data.error || "Server error");
1243
+ }
1244
+ var dialoguePosted = 0;
1245
+ for (var i = 0; i < fb.selected_items.length; i++) {
1246
+ var item = fb.selected_items[i];
1247
+ var safeTarget = item.target_id.replace(/:/g, "-");
1248
+ var dialInput = document.getElementById("ask-" + safeTarget);
1249
+ if (dialInput && dialInput.value.trim()) {
1250
+ var parts = item.target_id.split(":");
1251
+ await fetch("/api/dialogue", {
1252
+ method: "POST",
1253
+ headers: { "Content-Type": "application/json" },
1254
+ body: JSON.stringify({
1255
+ persona_id: parts[0],
1256
+ item_id: parts[1],
1257
+ text: "**User:** " + dialInput.value.trim(),
1258
+ thread_id: item.thread_id,
1259
+ round_id: currentRound,
1260
+ item_uid: item.item_uid
1261
+ })
1262
+ });
1263
+ dialoguePosted++;
1264
+ }
1265
+ }
1266
+ var msg = "Feedback submitted — agent is processing";
1267
+ if (dialoguePosted) msg += " (" + dialoguePosted + " dialogue question" + (dialoguePosted !== 1 ? "s" : "") + ")";
1268
+ showToast(msg);
1269
+ } catch (err) {
1270
+ showToast("Error: " + (err.message || "Failed to submit"));
1271
+ updateSubmitButton();
1272
+ }
1273
+ }
1274
+
1275
+ prevRoundBtn.addEventListener("click", function() {
1276
+ var idx = roundIds.indexOf(currentRound);
1277
+ if (idx > 0) {
1278
+ currentRound = roundIds[idx - 1];
1279
+ updateRoundUI();
1280
+ }
1281
+ });
1282
+ nextRoundBtn.addEventListener("click", function() {
1283
+ var idx = roundIds.indexOf(currentRound);
1284
+ if (idx >= 0 && idx < roundIds.length - 1) {
1285
+ currentRound = roundIds[idx + 1];
1286
+ updateRoundUI();
1287
+ }
1288
+ });
1289
+
1290
+ init();
1291
+ </script>
1292
+ </body>
1293
+ </html>