@debonet/idd 0.0.0-stage → 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.
- package/INSTRUCTIONS.md +141 -0
- package/README.md +89 -2
- package/bin/idd +16 -0
- package/bin/idd-merge-hook +13 -0
- package/docs/existing-page-migration.md +92 -0
- package/docs/hosted-viewer.md +164 -0
- package/package.json +47 -4
- package/scripts/check.js +538 -0
- package/scripts/commands.js +847 -0
- package/scripts/html-text.js +50 -0
- package/scripts/import-page.js +80 -0
- package/scripts/merge-hook.js +365 -0
- package/scripts/page-build.js +21 -0
- package/scripts/project-lock.js +135 -0
- package/specs/adoption.md +133 -0
- package/specs/build.md +176 -0
- package/specs/guidance/agent-merge.md +63 -0
- package/specs/guidance/answers.md +108 -0
- package/specs/guidance/claude-code.md +76 -0
- package/specs/guidance/codex.md +85 -0
- package/specs/guidance/session.md +223 -0
- package/specs/guidance/writing-questions.md +171 -0
- package/specs/hosted-questionnaire.md +738 -0
- package/specs/idd-merge-hook.md +42 -0
- package/specs/idd-page.md +340 -0
- package/specs/project-files.md +188 -0
- package/specs/questions-page.md +169 -0
- package/specs/themes.md +94 -0
- package/templates/questions-page.html +337 -0
|
@@ -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 (`<form>`), 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.
|
package/specs/themes.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
## Built-in themes
|
|
35
|
+
|
|
36
|
+
* {#s-ncz9te <m-qqilfo@d782597} The viewer offers the light, dark, retro, silk, and editorial
|
|
37
|
+
built-in themes on the splash page, dashboard, and every project questionnaire.
|
|
38
|
+
* {#s-c51zaj <m-qqilfo@d782597} Retro uses a green CRT appearance and period-style
|
|
39
|
+
typography; silk uses subtle colors and decoration.
|
|
40
|
+
|
|
41
|
+
## Project themes
|
|
42
|
+
|
|
43
|
+
* {#s-h3kyvr <m-qqilfo@d782597} A project's IDD branch may add themes through
|
|
44
|
+
`idd/themes.json` and one CSS file at `idd/themes/<id>.css` for each theme.
|
|
45
|
+
* {#s-w4m2ek <m-qqilfo@d782597} `idd/themes.json` is a JSON object with exactly
|
|
46
|
+
`defaultThemeId` and `themes`; `defaultThemeId` names one theme in the
|
|
47
|
+
`themes` array, and each theme object has exactly `id`, `name`, `stylesheet`,
|
|
48
|
+
and optional `hooks` members. Theme ids are unique lowercase letters,
|
|
49
|
+
digits, and hyphens, and project themes cannot reuse a built-in theme id.
|
|
50
|
+
`stylesheet` is exactly `idd/themes/<id>.css`. `hooks` may contain only
|
|
51
|
+
`header`, `beforeQuestions`, `afterQuestions`, and `footer`, each with a
|
|
52
|
+
plain-text string value.
|
|
53
|
+
* {#s-oz1uov <m-qqilfo@d782597} The viewer offers the project's themes alongside the
|
|
54
|
+
built-in themes, and uses the project's default when the user has no valid
|
|
55
|
+
saved preference for that questionnaire. If the manifest or default is
|
|
56
|
+
absent or invalid, the viewer uses light.
|
|
57
|
+
* {#s-svvpd7 <m-qqilfo@d782597} The project default may name a theme supplied by that
|
|
58
|
+
project; the user can choose any offered built-in or project theme for the
|
|
59
|
+
current questionnaire.
|
|
60
|
+
* {#s-b7uom8 <m-qqilfo@d782597} Each theme is a separate CSS module loaded only while that
|
|
61
|
+
theme is selected; changing themes does not replace questionnaire data or
|
|
62
|
+
another project's theme.
|
|
63
|
+
|
|
64
|
+
## Theme choice and content hooks
|
|
65
|
+
|
|
66
|
+
* {#s-6bc09d <m-qqilfo@d782597} The questionnaire page lets the user choose a theme for
|
|
67
|
+
that project and code branch. The viewer saves the choice in that browser
|
|
68
|
+
under the repository and branch identity, and keeps that preference when
|
|
69
|
+
the user returns to the questionnaire.
|
|
70
|
+
* {#s-eydwk6 <m-fjqj3t@17d6d3e} The splash page and dashboard each have an independent built-in theme
|
|
71
|
+
selector in the content area directly below the site header; changing either
|
|
72
|
+
site theme leaves the other surface's theme and each project's questionnaire
|
|
73
|
+
theme unchanged. Each selector is a compact inline row displayed as
|
|
74
|
+
`Theme [ selected theme ]`. Theme changes, including the change between
|
|
75
|
+
splash, dashboard, and questionnaire surfaces, animate together over about
|
|
76
|
+
1.2 seconds.
|
|
77
|
+
* {#s-7jgi72 <m-fjqj3t@17d6d3e} The splash page and dashboard both default to Editorial.
|
|
78
|
+
Each can use any built-in theme, and their theme ids are saved separately in
|
|
79
|
+
the browser.
|
|
80
|
+
* {#s-mn6l2h <arch} A saved project theme preference contains only the theme id and
|
|
81
|
+
project identity; it never contains an authentication token or answer data.
|
|
82
|
+
* {#s-9cwnxc <arch} A saved site theme preference contains only the selected page-surface
|
|
83
|
+
key and built-in theme id; it never contains an authentication token or answer
|
|
84
|
+
data.
|
|
85
|
+
* {#s-sk0afi <m-qqilfo@d782597} A theme can provide plain-text content at the page header,
|
|
86
|
+
before the questions, after the questions, and the page footer. The viewer
|
|
87
|
+
renders that content as text and never interprets it as HTML or JavaScript.
|
|
88
|
+
* {#s-5ref5b <arch} Theme styles and hook content cannot execute JavaScript or load
|
|
89
|
+
resources from outside the page's origin.
|
|
90
|
+
|
|
91
|
+
## Dependencies
|
|
92
|
+
|
|
93
|
+
* {#p-bac5t2 >specs/hosted-questionnaire.md} The hosted page, its browser/server
|
|
94
|
+
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>
|