@debonet/idd 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,169 @@
1
+ # Questions page
2
+
3
+ Statement ids: complete
4
+
5
+ ## Purpose
6
+
7
+ Claude Code's questionnaire, the page the human answers on: it draws a
8
+ project's questions as cards, saves each answer to its store as he taps or
9
+ types, and sends the session one message when he presses Done. Its store is
10
+ where his answers reach a Claude Code session; the project's answer file
11
+ (`specs/project-files.md`) is their record.
12
+
13
+ ## His words
14
+
15
+ > {#m-0pmw3x} From his instructions file `~/.claude/CLAUDE.md`, section
16
+ > "Questions for me go on a web questions page", as read 2026-11-04:
17
+ > "Answers are saved in the page's `db` (`answers/<sId>`) as I tap, and are
18
+ > not sent one by one. When I press the page's **Done: evaluate my answers**
19
+ > button, it sends one comment starting `DONE:` listing every answered
20
+ > question still in the open list, then redraws the page itself so they move
21
+ > into "Already answered"."
22
+
23
+ > {#m-3hliqv} From the same section: "Every save on the page writes
24
+ > `pending/<sId>` as well as `answers/<sId>` (phase 1; the template does
25
+ > this), so a question I answer or change is in `pending/` until you act on
26
+ > it, whether or not a comment reached you."
27
+
28
+ > {#m-hq9v8b} From the same section: "`sBody` is inserted as HTML. Escape
29
+ > any markup you quote in it (`&lt;form&gt;`), or the page draws it live and
30
+ > the card breaks."
31
+
32
+ > {#m-ic301q} RULING (CEO) 2026-10-14 (AIOS): "Your question 43 is
33
+ > malformed. There's nothing for me to click on/edit"
34
+
35
+ > {#m-pnt1kf} RULING (CEO) 2026-10-12 (AIOS): "When you ask new question:
36
+ > 1) collapse the group of already-answered questions. 2) open the local
37
+ > browser to the page"
38
+
39
+ ## Terms
40
+
41
+ * {#d-jftp0w <m-0pmw3x@96f952f} **Answered question** — a question whose saved answer
42
+ holds a chosen option or a non-empty note.
43
+ * {#d-7ydizp <m-0pmw3x@96f952f} **Open question** — a question that is not an answered
44
+ question.
45
+ * {#d-u5x224 <m-3hliqv@12040c4} **Pending answer** — a question whose `pending/<id>`
46
+ document exists in the page's store: an answer saved on the page that the
47
+ session has not yet incorporated.
48
+
49
+ ## API
50
+
51
+ * {#p-tnv4q6 >specs/project-files.md} The questions the page draws, their ids,
52
+ titles, bodies and options, are a project's question file as
53
+ `specs/project-files.md` gives it, put into the page by
54
+ `idd build` (`specs/idd-page.md`).
55
+ * {#s-a73a0q <m-0pmw3x@96f952f} **The answer document.** Each save writes the document
56
+ `answers/<id>` in the page's store, `<id>` the question's id, holding
57
+ exactly `sChoice`, the chosen option's key or `""` when none is chosen,
58
+ `sNote`, the note field's text, and `dtm`, the time of the save as an ISO
59
+ 8601 string.
60
+ * {#s-8ety6a <m-3hliqv@12040c4} **The pending document.** The same save then writes
61
+ `pending/<id>` holding the same three members with the same values.
62
+ {#s-gwaqfz <m-3hliqv@12040c4} It writes `pending/<id>` only after `answers/<id>` is
63
+ written, so a pending answer always has its answer document.
64
+ * {#s-dv6vxh <m-3hliqv@12040c4} **The page never clears the queue.** The page never
65
+ deletes or empties a `pending/` document; only the session does
66
+ (`specs/guidance/session.md`, The pending queue).
67
+ * {#s-0kblei <m-0pmw3x@96f952f} **One message.** Pressing Done sends at most one comment,
68
+ whose first line is `DONE: incorporate these answers now:` and each further
69
+ line names one question as `<id> (<title>): <key> | note: <note>`, with
70
+ `-` in place of `<key>` when no option is chosen and with ` | note: <note>`
71
+ left out when the note is empty.
72
+ * {#s-ju5n0u <m-0pmw3x@96f952f} **Nothing else is sent.** A choice, a note edit or a save
73
+ sends no comment and no message of any kind; the Done comment is the only
74
+ thing the page sends the session.
75
+
76
+ ## What the page draws
77
+
78
+ * {#s-qk26bo <arch} **The title.** The page's document title and its heading are
79
+ the title of the project's page file (`specs/project-files.md`).
80
+ * {#s-b7wxuw <m-ic301q@19b1856} **A card per question.** Each question is drawn as one
81
+ card showing its id, its title, its body, one choice per option showing the
82
+ option's key and text, the option's tag under its text when it has one,
83
+ and one note field.
84
+ * {#s-6c80ki <m-hq9v8b@ca2df04,m-ic301q@19b1856} **Question content is safe text.**
85
+ Titles, option text and tags are never interpreted as markup. A body is plain
86
+ text with backtick-delimited code spans and blank-line paragraph breaks; HTML
87
+ such as `<form>` remains literal text. Every card's options and note field
88
+ are drawn regardless of its body.
89
+ * {#s-9zo30l <arch} **Code spans.** Text between a pair of backticks in a body is
90
+ drawn in the code style, without the backticks.
91
+ * {#s-kz7e3u <arch} **Paragraphs.** A blank line in a body starts a new paragraph.
92
+ * {#s-46ya3e <m-0pmw3x@96f952f} **Open questions first.** Open questions are drawn first,
93
+ in the order of the question file.
94
+ * {#s-3l5xjx <m-0pmw3x@96f952f,m-pnt1kf@f4f0fc4} **The answered group.** Answered questions are
95
+ drawn inside one group, below the open questions, whose heading reads
96
+ `Already answered (<n>)` with `<n>` their count; {#s-psnlzg <m-pnt1kf@f4f0fc4} the
97
+ group is collapsed each time the page draws it; {#s-ni4dwa <arch} inside it the
98
+ questions are in the reverse of the question file's order, the last
99
+ question in the file first. {#s-ztak58 <arch} With no answered question there is
100
+ no group.
101
+ * {#s-k1cfvs <arch} **Saved state.** A card whose saved answer holds a choice shows
102
+ `Saved: <key>` under its note field.
103
+ * {#s-0kjwif <arch} **The Done button.** The page shows one button labelled
104
+ `Done: incorporate my answers` above the questions.
105
+
106
+ ## Saving
107
+
108
+ * {#s-c9g0sj <m-0pmw3x@96f952f} **A choice saves.** Choosing an option saves that question's
109
+ answer at once.
110
+ * {#s-vodny0 <m-0pmw3x@96f952f} **A note saves.** Typing in a note field saves that
111
+ question's answer within one second after the last keystroke.
112
+ * {#s-14wp7m <arch} **Nothing moves while he works.** A save does not move any card
113
+ between the open questions and the answered group; cards move only when
114
+ the page loads and after a Done comment is sent.
115
+ * {#s-mhf10z <m-0pmw3x@96f952f} **Loading.** When the page loads with its store
116
+ available, it reads every `answers/` document, shows each saved choice and
117
+ note on its card, and splits the questions into open and answered by them.
118
+ * {#s-rnlgmp <arch} **No store.** When the page's store is not available, the page
119
+ shows a line above the questions saying saving is not available here, each
120
+ attempted save shows `Not saved:` and a reason on its card, and nothing is
121
+ written.
122
+ * {#s-z7et0u <arch} **A failed save.** A save whose write fails shows `Not saved:`
123
+ and the failure's message on its card; when the `answers/` write fails, no
124
+ `pending/` document is written for it.
125
+
126
+ ## Done
127
+
128
+ * {#s-sjn8r1 <arch} **Notes are flushed first.** Pressing Done first saves every
129
+ question whose note field's text differs from its last saved note, and
130
+ composes the comment only after those saves, so the comment holds every
131
+ character typed before the press.
132
+ * {#s-fz9dor <m-3hliqv@12040c4,m-0pmw3x@96f952f} **Which questions it names.** The Done comment
133
+ names every question that has a pending answer when Done is pressed, in
134
+ the order of the question file.
135
+ * {#s-8pbkpq <arch} **Nothing to name.** With no pending answer, the page sends no
136
+ comment and shows `Nothing newly answered.` beside the button.
137
+ * {#s-exa326 <m-0pmw3x@96f952f} **After sending.** Once the comment is sent, the page
138
+ redraws so every answered question it named is in the answered group,
139
+ scrolls to its top, and shows `Sent <n> answers to Claude.` beside the
140
+ button, `<n>` the number of questions named.
141
+ * {#s-h65nig <arch} **One thread per load.** The first Done comment of a page load
142
+ starts a thread anchored at the page's header; every later Done comment of
143
+ the same load is sent into that thread.
144
+ * {#s-xtwuep <arch} **When the session cannot be reached.** When the page cannot
145
+ send comments, or the comment service reports that it cannot send to
146
+ Claude now, the page sends nothing, moves no card, and shows beside the
147
+ button a line telling him to tell Claude "done".
148
+ * {#s-49jzsg <arch} **A failed send.** When sending fails, the page moves no card
149
+ and shows `Not sent` with the failure's code beside the button.
150
+
151
+ ## Hand check
152
+
153
+ * {#i-wup5vo <m-0pmw3x@96f952f,m-3hliqv@12040c4,m-pnt1kf@f4f0fc4} **Do:** in a scratch project, run
154
+ `idd init "Scratch Open Questions"`, add two questions to its question
155
+ file, one with a body holding `<form>`, run `idd build`, publish the
156
+ built page as a Claude Code artifact with comments and store enabled, and
157
+ open it signed in to claude.ai. Tap an option on the first question, type
158
+ a note on the second, and press Done straight after typing. **Look for:**
159
+ the second card shows `<form>` as text with its options drawn; a thread
160
+ on the page holds one comment opening `DONE: incorporate these answers now:`
161
+ with one line per question, the note complete; the session's store
162
+ listing shows `pending/q1` and `pending/q2`; after a reload both questions
163
+ sit in a collapsed `Already answered (2)` group.
164
+
165
+ ## Dependencies
166
+
167
+ * {#p-ctfrwf >specs/project-files.md} The question file and the page file it
168
+ draws from.
169
+ * {#p-ro6feq >specs/idd-page.md} The command that builds the page.
@@ -0,0 +1,114 @@
1
+ # Themes
2
+
3
+ Statement ids: complete
4
+
5
+ ## Purpose
6
+
7
+ The hosted questionnaire's built-in and project-provided visual themes and
8
+ per-project theme preference.
9
+
10
+ ## His words
11
+
12
+ > {#m-qqilfo} RULING (CEO), 2026-11-09: “Ok, create a few themes:
13
+ >
14
+ > 1) light (like what's there now)
15
+ > 2) dark
16
+ > 3) retro (should look like a 1980's green crt and font)
17
+ > 4) silk (something really subtle)
18
+ >
19
+ > Separate theme can be chosen for each project.
20
+ >
21
+ > Themes should be completely modular.
22
+ >
23
+ > A project can supply its own additional themes by having them in its idd
24
+ > repository. The projects themes should be the default if the user hasn't
25
+ > specifically chosen a different theme for this project.
26
+ >
27
+ > Themes should largely be css, but we can have a "hook" for adding content in
28
+ > certain places in the page (header, footer, etc)”
29
+
30
+ > {#m-fjqj3t} “The dashboard should either be independently themeable (preferred)
31
+ > or should match the splash page. Either way, the splash page theme should be
32
+ > a theming option.”
33
+
34
+ > {#m-rakcji} THOUGHT (CEO), 2026-11-13: “Theme choice should be shared
35
+ > across browsers, but not across users. Perhaps we should maintain a list of
36
+ > <user>,<theme> in the idd-<branch> directory on github, unless there is
37
+ > another way (e.g. is there github-user storage offered?)”
38
+
39
+ ## Built-in themes
40
+
41
+ * {#s-ncz9te <m-qqilfo@d782597} The viewer offers the light, dark, retro, silk, and editorial
42
+ built-in themes on the splash page, dashboard, and every project questionnaire.
43
+ * {#s-c51zaj <m-qqilfo@d782597} Retro uses a green CRT appearance and period-style
44
+ typography; silk uses subtle colors and decoration.
45
+
46
+ ## Project themes
47
+
48
+ * {#s-h3kyvr <m-qqilfo@d782597} A project's IDD branch may add themes through
49
+ `idd/themes.json` and one CSS file at `idd/themes/<id>.css` for each theme.
50
+ * {#s-w4m2ek <m-qqilfo@d782597} `idd/themes.json` is a JSON object with exactly
51
+ `defaultThemeId` and `themes`; `defaultThemeId` names one theme in the
52
+ `themes` array, and each theme object has exactly `id`, `name`, `stylesheet`,
53
+ and optional `hooks` members. Theme ids are unique lowercase letters,
54
+ digits, and hyphens, and project themes cannot reuse a built-in theme id.
55
+ `stylesheet` is exactly `idd/themes/<id>.css`. `hooks` may contain only
56
+ `header`, `beforeQuestions`, `afterQuestions`, and `footer`, each with a
57
+ plain-text string value.
58
+ * {#s-oz1uov <m-qqilfo@d782597} The viewer offers the project's themes alongside the
59
+ built-in themes, and uses the project's default when the user has no valid
60
+ saved preference for that questionnaire. If the manifest or default is
61
+ absent or invalid, the viewer uses light.
62
+ * {#s-svvpd7 <m-qqilfo@d782597} The project default may name a theme supplied by that
63
+ project; the user can choose any offered built-in or project theme for the
64
+ current questionnaire.
65
+ * {#s-b7uom8 <m-qqilfo@d782597} Each theme is a separate CSS module loaded only while that
66
+ theme is selected; changing themes does not replace questionnaire data or
67
+ another project's theme.
68
+
69
+ ## Theme choice and content hooks
70
+
71
+ * {#s-6bc09d <m-qqilfo@d782597} The questionnaire page lets the user choose a theme for
72
+ that project and code branch. The viewer saves the choice under the
73
+ authenticated GitHub user, repository and branch identity, and keeps that
74
+ preference when the user returns to the questionnaire in another browser.
75
+ * {#s-jl9nzo <m-rakcji@fd6be8a} A project theme preference is keyed to the
76
+ authenticated GitHub user's immutable account id and the repository and
77
+ branch identity; two users may choose different themes for the same project.
78
+ The splash, dashboard and project theme choices are stored in Firestore and
79
+ shared across browsers for that user. The hosted service does not expose one
80
+ user's preferences to another user.
81
+ * {#s-wmmbpw <m-rakcji@fd6be8a} The browser reads and writes the signed-in user's
82
+ theme choices through `/api/theme-preferences`, authenticating with its
83
+ in-memory GitHub access token. The service verifies the token's GitHub user
84
+ id before reading or writing that user's preference record. Theme preference
85
+ data contains only theme ids and project/repository branch keys;
86
+ it contains no access tokens or answers.
87
+ * {#s-6io4k8 <arch} Before sign-in, the splash page uses a browser-local
88
+ theme choice. After sign-in, the user's saved splash theme replaces that
89
+ browser-local choice.
90
+ * {#s-eydwk6 <m-fjqj3t@17d6d3e} The splash page and dashboard each have an independent built-in theme
91
+ selector in the content area directly below the site header; changing either
92
+ site theme leaves the other surface's theme and each project's questionnaire
93
+ theme unchanged. Each selector is a compact inline row displayed as
94
+ `Theme [ selected theme ]`. Theme changes, including the change between
95
+ splash, dashboard, and questionnaire surfaces, animate together over about
96
+ 1.2 seconds.
97
+ * {#s-7jgi72 <m-fjqj3t@17d6d3e} The splash page and dashboard both default to Editorial.
98
+ Each can use any built-in theme, and their theme ids are saved separately in
99
+ the browser.
100
+ * {#s-mn6l2h <arch} A saved project theme preference contains only the theme id and
101
+ project identity; it never contains an authentication token or answer data.
102
+ * {#s-9cwnxc <arch} A saved site theme preference contains only the selected page-surface
103
+ key and built-in theme id; it never contains an authentication token or answer
104
+ data.
105
+ * {#s-sk0afi <m-qqilfo@d782597} A theme can provide plain-text content at the page header,
106
+ before the questions, after the questions, and the page footer. The viewer
107
+ renders that content as text and never interprets it as HTML or JavaScript.
108
+ * {#s-5ref5b <arch} Theme styles and hook content cannot execute JavaScript or load
109
+ resources from outside the page's origin.
110
+
111
+ ## Dependencies
112
+
113
+ * {#p-bac5t2 >specs/hosted-questionnaire.md} The hosted page, its browser/server
114
+ boundary, and the repository data branch.
@@ -0,0 +1,337 @@
1
+ <!-- @implements s-a73a0q@cf94207 -->
2
+ <!-- @implements s-8ety6a@6d34bce -->
3
+ <!-- @implements s-gwaqfz@8785440 -->
4
+ <!-- @implements s-dv6vxh@19ba3a0 -->
5
+ <!-- @implements s-0kblei@baea47d -->
6
+ <!-- @implements s-ju5n0u@9402d80 -->
7
+ <!-- @implements s-qk26bo@e7ce6af -->
8
+ <!-- @implements s-b7wxuw@a925940 -->
9
+ <!-- @implements s-6c80ki@be769aa -->
10
+ <!-- @implements s-9zo30l@4099111 -->
11
+ <!-- @implements s-kz7e3u@0399bde -->
12
+ <!-- @implements s-46ya3e@47b9134 -->
13
+ <!-- @implements s-3l5xjx@8adaada -->
14
+ <!-- @implements s-psnlzg@b3b8b04 -->
15
+ <!-- @implements s-ni4dwa@4a9d3fa -->
16
+ <!-- @implements s-ztak58@59cb7a9 -->
17
+ <!-- @implements s-k1cfvs@1bbdfbf -->
18
+ <!-- @implements s-0kjwif@46e7a60 -->
19
+ <!-- @implements s-c9g0sj@c577f27 -->
20
+ <!-- @implements s-vodny0@013dbe7 -->
21
+ <!-- @implements s-14wp7m@c9d69b9 -->
22
+ <!-- @implements s-mhf10z@629fbf3 -->
23
+ <!-- @implements s-rnlgmp@7d6e87c -->
24
+ <!-- @implements s-z7et0u@a677c46 -->
25
+ <!-- @implements s-sjn8r1@6b791d6 -->
26
+ <!-- @implements s-fz9dor@8d9d508 -->
27
+ <!-- @implements s-8pbkpq@8105718 -->
28
+ <!-- @implements s-exa326@eb8afdd -->
29
+ <!-- @implements s-h65nig@3fa5cbd -->
30
+ <!-- @implements s-xtwuep@adea03c -->
31
+ <!-- @implements s-49jzsg@75d6179 -->
32
+ <!-- @tested-by test/questions-page-draw.test.js -->
33
+ <!-- @tested-by test/questions-page-save.test.js -->
34
+ <!-- @tested-by test/questions-page-done.test.js -->
35
+ <!-- @checks i-wup5vo@090a4c2 -->
36
+ <title></title>
37
+ <style>
38
+ :root {
39
+ --bg: #f6f7f9;
40
+ --card: #ffffff;
41
+ --fg: #1d2330;
42
+ --muted: #5d6677;
43
+ --line: #d9dee7;
44
+ --accent: #2b5fd9;
45
+ --accent-soft: #e6edfc;
46
+ --ok: #1f7a4d;
47
+ --font-body: "IBM Plex Sans", system-ui, -apple-system, sans-serif;
48
+ --font-mono: "IBM Plex Mono", ui-monospace, Menlo, monospace;
49
+ }
50
+ @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) {
51
+ --bg: #12151b; --card: #1b2029; --fg: #e6e9ef; --muted: #9aa3b4; --line: #2c3340;
52
+ --accent: #7ea2ff; --accent-soft: #232d44; --ok: #5cc28f; color-scheme: dark } }
53
+ :root[data-theme="dark"] {
54
+ --bg: #12151b; --card: #1b2029; --fg: #e6e9ef; --muted: #9aa3b4; --line: #2c3340;
55
+ --accent: #7ea2ff; --accent-soft: #232d44; --ok: #5cc28f; color-scheme: dark }
56
+ body { background: var(--bg); color: var(--fg); font-family: var(--font-body); font-size: 16px; line-height: 1.45; padding-inline: 16px; padding-block: 20px 48px; }
57
+ main { max-width: 640px; margin: 0 auto; display: grid; gap: 18px; }
58
+ h1 { font-size: 1.35rem; margin: 0; text-wrap: balance; }
59
+ .lede { color: var(--muted); margin: 4px 0 0; font-size: 0.95rem; }
60
+ .q { background: var(--card); border: 1px solid var(--line); border-radius: 12px; padding: 16px; display: grid; gap: 10px; }
61
+ .q h2 { font-size: 1.02rem; margin: 0; text-wrap: balance; }
62
+ .num { font-family: var(--font-mono); font-size: 0.8rem; color: var(--muted); letter-spacing: 0.04em; }
63
+ .q p { margin: 0; font-size: 0.95rem; white-space: pre-wrap; }
64
+ code { font-family: var(--font-mono); font-size: 0.86em; background: var(--accent-soft); padding: 1px 4px; border-radius: 4px; overflow-wrap: anywhere; }
65
+ .opts { display: grid; gap: 8px; }
66
+ .opt { display: flex; gap: 10px; align-items: flex-start; border: 1px solid var(--line); border-radius: 10px; padding: 12px; cursor: pointer; }
67
+ .opt input { margin-top: 3px; width: 20px; height: 20px; accent-color: var(--accent); flex: none; }
68
+ .opt:has(input:checked) { border-color: var(--accent); background: var(--accent-soft); }
69
+ .opt span { min-width: 0; }
70
+ .tag { font-size: 0.75rem; color: var(--muted); display: block; margin-top: 2px; }
71
+ textarea { width: 100%; box-sizing: border-box; min-height: 52px; font: inherit; font-size: 0.95rem; color: var(--fg); background: var(--bg); border: 1px solid var(--line); border-radius: 8px; padding: 8px; }
72
+ textarea:focus-visible, .opt:focus-within { outline: 2px solid var(--accent); outline-offset: 1px; }
73
+ .status { font-size: 0.8rem; color: var(--muted); min-height: 1em; }
74
+ .status.ok { color: var(--ok); }
75
+ details.answered { display: grid; gap: 18px; }
76
+ details.answered > summary { cursor: pointer; color: var(--muted); font-size: 0.95rem; padding: 10px 0; }
77
+ details.answered[open] > summary { margin-bottom: 18px; }
78
+ .stack { display: grid; gap: 18px; }
79
+ #banner { font-size: 0.85rem; color: var(--muted); }
80
+ .sendbar { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; font-size: 0.85rem; color: var(--muted); }
81
+ .sendbar button { font: inherit; font-size: 0.9rem; padding: 8px 14px; border-radius: 8px; border: 1px solid var(--accent); background: var(--accent-soft); color: var(--fg); cursor: pointer; }
82
+ </style>
83
+
84
+ <main>
85
+ <header>
86
+ <h1 id="heading"></h1>
87
+ <p class="lede">Tap an answer; it saves as you go. Add a note if neither option is right. Press Done when you want Claude to read your answers.</p>
88
+ <p id="banner"></p>
89
+ <div class="sendbar"><button id="done" type="button">Done: incorporate my answers</button><span id="sendstate"></span></div>
90
+ </header>
91
+ <div id="list"></div>
92
+ </main>
93
+
94
+ <script>
95
+ const aPage = __PAGE__;
96
+ const vaQuestion = aPage.questions;
97
+ const msNoteDebounce = 700;
98
+ const aAnswer = {};
99
+ const aDraft = {};
100
+ const aCard = {};
101
+ const aTimer = {};
102
+ const spSave = new Set();
103
+ const asIdPending = new Set();
104
+ const eList = document.getElementById("list");
105
+ const eBanner = document.getElementById("banner");
106
+ const eSendState = document.getElementById("sendstate");
107
+ let db = null;
108
+ let comments = null;
109
+ let sIdThread = null;
110
+ let bBusy = false;
111
+
112
+ document.title = aPage.title;
113
+ document.getElementById("heading").textContent = aPage.title;
114
+
115
+ function feMake(sTag, sClass, sText) {
116
+ const e = document.createElement(sTag);
117
+ if (sClass) e.className = sClass;
118
+ if (sText !== undefined) e.textContent = sText;
119
+ return e;
120
+ }
121
+
122
+ function fbAnswered(a) {
123
+ return !!(a && (a.sChoice || a.sNote));
124
+ }
125
+
126
+ function fAppendSpan(eP, sText) {
127
+ sText.split(/`([^`]+)`/).forEach((s, n) => {
128
+ if (n % 2) eP.appendChild(feMake("code", "", s));
129
+ else if (s) eP.appendChild(document.createTextNode(s));
130
+ });
131
+ }
132
+
133
+ function fAppendBody(eQ, sBody) {
134
+ sBody.split(/\n\s*\n/).filter((s) => s.trim()).forEach((sPara) => {
135
+ const eP = document.createElement("p");
136
+ fAppendSpan(eP, sPara);
137
+ eQ.appendChild(eP);
138
+ });
139
+ }
140
+
141
+ function feOption(aQ, aO, sChoice, fOnChange) {
142
+ const eLabel = feMake("label", "opt");
143
+ const eIn = document.createElement("input");
144
+ eIn.type = "radio";
145
+ eIn.name = aQ.id;
146
+ eIn.value = aO.key;
147
+ eIn.checked = sChoice === aO.key;
148
+ eIn.addEventListener("change", fOnChange);
149
+ const eText = document.createElement("span");
150
+ eText.appendChild(feMake("b", "", aO.key + ". "));
151
+ eText.appendChild(document.createTextNode(aO.text));
152
+ if (aO.tag) eText.appendChild(feMake("span", "tag", aO.tag));
153
+ eLabel.appendChild(eIn);
154
+ eLabel.appendChild(eText);
155
+ return { eLabel, eIn };
156
+ }
157
+
158
+ function feCard(aQ) {
159
+ const aState = aDraft[aQ.id] || aAnswer[aQ.id] || {};
160
+ const eQ = feMake("section", "q");
161
+ eQ.appendChild(feMake("div", "num", aQ.id));
162
+ eQ.appendChild(feMake("h2", "", aQ.title));
163
+ fAppendBody(eQ, aQ.body);
164
+ const eOpts = feMake("div", "opts");
165
+ const vcRadio = aQ.options.map((aO) => {
166
+ const aOpt = feOption(aQ, aO, aState.sChoice, () => fpSave(aQ.id));
167
+ eOpts.appendChild(aOpt.eLabel);
168
+ return aOpt.eIn;
169
+ });
170
+ eQ.appendChild(eOpts);
171
+ const eNote = document.createElement("textarea");
172
+ eNote.placeholder = "Note (optional)";
173
+ eNote.value = aState.sNote || "";
174
+ eNote.addEventListener("input", () => {
175
+ clearTimeout(aTimer[aQ.id]);
176
+ aTimer[aQ.id] = setTimeout(() => fpSave(aQ.id), msNoteDebounce);
177
+ });
178
+ eQ.appendChild(eNote);
179
+ const eStatus = feMake("div", "status");
180
+ eQ.appendChild(eStatus);
181
+ aCard[aQ.id] = { eQ, vcRadio, eNote, eStatus };
182
+ return eQ;
183
+ }
184
+
185
+ function fStatus(sId, sMsg) {
186
+ const e = aCard[sId].eStatus;
187
+ const a = aAnswer[sId];
188
+ if (sMsg) { e.textContent = sMsg; e.className = "status"; }
189
+ else if (a && a.sChoice) { e.textContent = "Saved: " + a.sChoice; e.className = "status ok"; }
190
+ else { e.textContent = ""; e.className = "status"; }
191
+ }
192
+
193
+ function fRender() {
194
+ eList.textContent = "";
195
+ Object.keys(aCard).forEach((sId) => delete aCard[sId]);
196
+ const vaOpen = vaQuestion.filter((aQ) => !fbAnswered(aAnswer[aQ.id]));
197
+ const vaDone = vaQuestion.filter((aQ) => fbAnswered(aAnswer[aQ.id])).reverse();
198
+ const eOpen = feMake("div", "stack");
199
+ vaOpen.forEach((aQ) => eOpen.appendChild(feCard(aQ)));
200
+ eList.appendChild(eOpen);
201
+ if (vaDone.length) {
202
+ const eGroup = feMake("details", "answered");
203
+ eGroup.appendChild(feMake("summary", "", "Already answered (" + vaDone.length + ")"));
204
+ const eDone = feMake("div", "stack");
205
+ vaDone.forEach((aQ) => eDone.appendChild(feCard(aQ)));
206
+ eGroup.appendChild(eDone);
207
+ eList.appendChild(eGroup);
208
+ }
209
+ vaQuestion.forEach((aQ) => fStatus(aQ.id));
210
+ }
211
+
212
+ async function fpxError(fp) {
213
+ try { await fp(); return null; }
214
+ catch (x) { return x || new Error("try again"); }
215
+ }
216
+
217
+ function fsFailure(x) {
218
+ return (x && x.message) || "try again";
219
+ }
220
+
221
+ async function fpWrite(sId) {
222
+ const aNow = aCard[sId];
223
+ const eChecked = aNow.vcRadio.find((e) => e.checked);
224
+ const aNext = { sChoice: eChecked ? eChecked.value : "", sNote: aNow.eNote.value, dtm: new Date().toISOString() };
225
+ clearTimeout(aTimer[sId]);
226
+ await pStore;
227
+ if (!db) { fStatus(sId, "Not saved: saving is not available here."); return; }
228
+ fStatus(sId, "Saving…");
229
+ const xAnswer = await fpxError(() => db.doc("answers/" + sId).set(aNext));
230
+ if (xAnswer) { aDraft[sId] = aNext; fStatus(sId, "Not saved: " + fsFailure(xAnswer)); return; }
231
+ aAnswer[sId] = aNext;
232
+ delete aDraft[sId];
233
+ const xPending = await fpxError(() => db.doc("pending/" + sId).set({ ...aNext }));
234
+ if (xPending) { fStatus(sId, "Not saved: " + fsFailure(xPending)); return; }
235
+ asIdPending.add(sId);
236
+ fStatus(sId);
237
+ }
238
+
239
+ function fpSave(sId) {
240
+ const p = fpWrite(sId);
241
+ spSave.add(p);
242
+ p.then(() => spSave.delete(p));
243
+ return p;
244
+ }
245
+
246
+ async function fpaDocs(sCollection) {
247
+ const aSnap = await db.collection(sCollection).get();
248
+ const a = {};
249
+ (aSnap.docs || []).forEach((d) => {
250
+ a[d.id] = typeof d.data === "function" ? d.data() : d.data;
251
+ });
252
+ return a;
253
+ }
254
+
255
+ async function fpStart() {
256
+ try { db = (await window.claude?.use?.("db")) || null; } catch (x) { db = null; }
257
+ if (!db) {
258
+ eBanner.textContent = "Saving is not available here; open this page signed in on claude.ai.";
259
+ } else {
260
+ try { Object.assign(aAnswer, await fpaDocs("answers")); } catch (x) { }
261
+ }
262
+ fRender();
263
+ }
264
+
265
+ async function fpComments() {
266
+ try { comments = (await window.claude?.use?.("comments")) || null; } catch (x) { comments = null; }
267
+ }
268
+
269
+ const pStore = fpStart();
270
+ const pComments = fpComments();
271
+
272
+ async function fpFlush() {
273
+ Object.keys(aTimer).forEach((sId) => clearTimeout(aTimer[sId]));
274
+ await Promise.all([...spSave]);
275
+ const vaStale = vaQuestion.filter((aQ) => aCard[aQ.id].eNote.value !== ((aAnswer[aQ.id] || {}).sNote || ""));
276
+ await Promise.all(vaStale.map((aQ) => fpSave(aQ.id)));
277
+ await Promise.all([...spSave]);
278
+ }
279
+
280
+ async function fpaPending() {
281
+ if (!db) return {};
282
+ try { return await fpaDocs("pending"); }
283
+ catch (x) {
284
+ const a = {};
285
+ asIdPending.forEach((sId) => { a[sId] = aAnswer[sId]; });
286
+ return a;
287
+ }
288
+ }
289
+
290
+ function fsLine(aQ, a) {
291
+ return aQ.id + " (" + aQ.title + "): " + (a.sChoice || "-") + (a.sNote ? " | note: " + a.sNote : "");
292
+ }
293
+
294
+ async function fpSendComment(sText) {
295
+ const eHeader = document.querySelector("main header");
296
+ const aRes = sIdThread
297
+ ? await comments.sendToClaude({ threadId: sIdThread, text: sText })
298
+ : await comments.sendToClaude({ anchor: await comments.anchorFor(eHeader), text: sText });
299
+ sIdThread = aRes.threadId;
300
+ }
301
+
302
+ async function fpbCanSend() {
303
+ await pComments;
304
+ if (!comments) return false;
305
+ try { return (await comments.canSendToClaude()) === "available"; }
306
+ catch (x) { return false; }
307
+ }
308
+
309
+ async function fpSend() {
310
+ await pStore;
311
+ await fpFlush();
312
+ const aPending = await fpaPending();
313
+ const vaNamed = vaQuestion.filter((aQ) => aQ.id in aPending);
314
+ if (!vaNamed.length) { eSendState.textContent = "Nothing newly answered."; return; }
315
+ if (!(await fpbCanSend())) {
316
+ eSendState.textContent = "Claude can't be reached from this view; tell Claude \"done\".";
317
+ return;
318
+ }
319
+ const vsLine = vaNamed.map((aQ) => fsLine(aQ, aAnswer[aQ.id] || aPending[aQ.id]));
320
+ const xSend = await fpxError(() => fpSendComment("DONE: incorporate these answers now:\n" + vsLine.join("\n")));
321
+ if (xSend) {
322
+ eSendState.textContent = "Not sent (" + (xSend.code || "error") + "); tell Claude \"done\".";
323
+ return;
324
+ }
325
+ eSendState.textContent = "Sent " + vaNamed.length + " answers to Claude.";
326
+ fRender();
327
+ window.scrollTo(0, 0);
328
+ }
329
+
330
+ async function fpDone() {
331
+ if (bBusy) return;
332
+ bBusy = true;
333
+ try { await fpSend(); } finally { bBusy = false; }
334
+ }
335
+
336
+ document.getElementById("done").addEventListener("click", fpDone);
337
+ </script>