cellenium 0.3.5__tar.gz → 0.4.0__tar.gz
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.
- cellenium-0.4.0/.claude/commands/cellenium/plan.md +9 -0
- cellenium-0.4.0/.claude/skills/cellenium/plan/SKILL.md +162 -0
- cellenium-0.4.0/.claude/skills/cellenium/plan/template.html +453 -0
- cellenium-0.3.5/README.md → cellenium-0.4.0/PKG-INFO +231 -209
- cellenium-0.3.5/PKG-INFO → cellenium-0.4.0/README.md +211 -228
- {cellenium-0.3.5 → cellenium-0.4.0}/pyproject.toml +16 -11
- cellenium-0.4.0/src/cellenium/ai/__init__.py +9 -0
- cellenium-0.4.0/src/cellenium/api/__init__.py +9 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/tests/test_cli.py +2 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/uv.lock +3870 -3865
- cellenium-0.3.5/src/cellenium/engine/__init__.py +0 -0
- cellenium-0.3.5/src/cellenium/functions/__init__.py +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/.claude/bin/checklist.js +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/.claude/commands/cellenium/artifact.md +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/.claude/commands/cellenium/checklist/before-startup.md +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/.claude/commands/cellenium/checklist/cruising.md +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/.claude/commands/cellenium/checklist/fordec.md +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/.claude/commands/cellenium/checklist/landing.md +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/.claude/commands/cellenium/evaluator.md +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/.claude/commands/cellenium/executor.md +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/.claude/commands/cellenium/locator-writer.md +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/.claude/commands/cellenium/prd-reader.md +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/.claude/commands/cellenium/test-spec.md +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/.claude/commands/cellenium/web.md +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/.claude/commands/cellenium/writer.md +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/.claude/skills/cellenium/artifact/SKILL.md +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/.claude/skills/cellenium/before-startup/SKILL.md +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/.claude/skills/cellenium/cruising/SKILL.md +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/.claude/skills/cellenium/emergency/fordec/SKILL.md +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/.claude/skills/cellenium/evaluator/SKILL.md +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/.claude/skills/cellenium/executor/SKILL.md +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/.claude/skills/cellenium/landing/SKILL.md +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/.claude/skills/cellenium/locator-writer/SKILL.md +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/.claude/skills/cellenium/prd-reader/SKILL.md +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/.claude/skills/cellenium/test-spec/SKILL.md +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/.claude/skills/cellenium/web/SKILL.md +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/.claude/skills/cellenium/writer/SKILL.md +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/.claude/skills/checklist/before-startup/SKILL.md +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/.claude/skills/checklist/cruising/SKILL.md +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/.claude/skills/checklist/emergency/fordec/SKILL.md +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/.claude/skills/checklist/landing/SKILL.md +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/.claude/test/install.test.js +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/.env.example +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/.gitignore +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/LICENSE +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/src/cellenium/__init__.py +0 -0
- {cellenium-0.3.5/src/cellenium/ai → cellenium-0.4.0/src/cellenium/ai/agents}/__init__.py +0 -0
- {cellenium-0.3.5/src/cellenium/ai/agents → cellenium-0.4.0/src/cellenium/ai/agents/vision}/__init__.py +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/src/cellenium/ai/agents/vision/config/agents.yaml +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/src/cellenium/ai/agents/vision/config/tasks.yaml +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/src/cellenium/ai/agents/vision/crew.py +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/src/cellenium/ai/agents/vision/schemas.py +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/src/cellenium/ai/agents/vision/skills/evaluator/SKILL.md +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/src/cellenium/ai/agents/vision/skills/vision/SKILL.md +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/src/cellenium/ai/agents/vision/tools/vision.py +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/src/cellenium/ai/config.py +0 -0
- {cellenium-0.3.5/src/cellenium/ai/agents/vision → cellenium-0.4.0/src/cellenium/ai/tools}/__init__.py +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/src/cellenium/ai/tools/image_encoder.py +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/src/cellenium/api/vision.py +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/src/cellenium/cli.py +0 -0
- {cellenium-0.3.5/src/cellenium/ai/tools → cellenium-0.4.0/src/cellenium/engine}/__init__.py +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/src/cellenium/engine/manager.py +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/src/cellenium/engine/page_engine.py +0 -0
- {cellenium-0.3.5/src/cellenium/api → cellenium-0.4.0/src/cellenium/functions}/__init__.py +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/src/cellenium/functions/locators.py +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/src/cellenium/functions/logger.py +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/src/cellenium/functions/sheets.py +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/src/cellenium/settings.py +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/src/cellenium/templates/env.template +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/tests/e2e/conftest.py +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/tests/e2e/test_google_search.py +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/tests/test_google_sheet_locators.py +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/tests/test_page_engine.py +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/tests/test_settings.py +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/tests/test_vision_agent.py +0 -0
- {cellenium-0.3.5 → cellenium-0.4.0}/tests/test_vision_api.py +0 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: plan
|
|
3
|
+
description: Write an implementation plan as an interactive HTML page — a three-level tree of disputable claims with visuals and inline decisions — and iterate on it through the Respond loop.
|
|
4
|
+
argument-hint: <feature or change to plan>
|
|
5
|
+
command: /cellenium:plan
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Before doing anything else, read `.claude/skills/cellenium/plan/SKILL.md` (or `~/.claude/skills/cellenium/plan/SKILL.md` if it is not in the project) and follow it as your instructions. The page template is `template.html` in the same folder.
|
|
9
|
+
Plan this: $ARGUMENTS
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: plan
|
|
3
|
+
description: Write an implementation plan as one interactive HTML page instead of a plan.md file. The plan is a three-level tree of claims that the user can disagree with, each with a visual (screen mockup, code, flow, or table) and inline decisions with recommendations. The user answers in the page, clicks Respond, and pastes the generated text back. Use when the user asks for a plan, a design proposal, or wants to review an approach before implementation.
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Interactive HTML Plan
|
|
8
|
+
|
|
9
|
+
Write the plan as one self-contained HTML page. The user reads it fast, opens only the depth they need, answers decisions in place, and sends the answers back to you with one paste.
|
|
10
|
+
|
|
11
|
+
Do not write a `plan.md` file. Do not implement anything while this skill is active.
|
|
12
|
+
|
|
13
|
+
## Workflow
|
|
14
|
+
|
|
15
|
+
1. **Research.** Read the relevant code, tests, and docs before you write a claim. Every level-3 claim must point to a real file. If a file does not exist yet, say "new file".
|
|
16
|
+
2. **Build the tree.** Follow [Rules](#rules).
|
|
17
|
+
3. **Write the page.**
|
|
18
|
+
- Output path: `.plans/<kebab-slug>.html` in the project root. Create `.plans/` if it does not exist.
|
|
19
|
+
- Copy `template.html` from this skill folder to the output path. Do not rewrite the template.
|
|
20
|
+
- Replace the single line `__PLAN_DATA__` with the plan JSON ([Schema](#schema)). In the JSON, write every `</` as `<\/`.
|
|
21
|
+
- Validate the JSON before you continue, for example: `python -c "import json,re,sys; s=open(sys.argv[1],encoding='utf-8').read(); json.loads(re.search(r'<script id=\"plan-data\"[^>]*>(.*?)</script>', s, re.S).group(1))" .plans/<slug>.html`.
|
|
22
|
+
4. **Open the page.** Windows: `start "" ".plans\<slug>.html"`. macOS: `open`. Linux: `xdg-open`. If you cannot open it, give the path.
|
|
23
|
+
5. **Stop and wait.** In chat, send only: the file path, the outcome sentence, the number of decisions, and this instruction: "Answer in the page, click **Respond**, and paste the text here." Do not repeat the plan in chat.
|
|
24
|
+
|
|
25
|
+
## Rules
|
|
26
|
+
|
|
27
|
+
### 1. The plan is a tree with three levels
|
|
28
|
+
|
|
29
|
+
| Level | Label in the page | Content |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| Root (`outcome`) | Result after this plan | One sentence: what the user has when the plan is complete. |
|
|
32
|
+
| 1 | Outcome | What a user or caller can do, see, or rely on. No file names. |
|
|
33
|
+
| 2 | How it works | The mechanism: data flow, components, rules, limits. |
|
|
34
|
+
| 3 | Where in code | The exact files, functions, tables, and tests that change. |
|
|
35
|
+
|
|
36
|
+
- Use 3–7 level-1 nodes. Use 1–5 children for each node.
|
|
37
|
+
- A reader who opens only level 1 must understand the full result.
|
|
38
|
+
- Do not go deeper than level 3. Put more detail in `detail` or in the visual.
|
|
39
|
+
|
|
40
|
+
### 2. Every node is a claim that the user can disagree with
|
|
41
|
+
|
|
42
|
+
A claim is one specific, testable sentence. It contains a number, a name, a boundary, or a behavior that a person can say "no" to.
|
|
43
|
+
|
|
44
|
+
| Bad (heading) | Good (claim) |
|
|
45
|
+
|---|---|
|
|
46
|
+
| Scheduling support | A user can schedule a maximum of 50 messages at the same time. |
|
|
47
|
+
| Error handling | If the send fails, the worker tries 3 more times, then marks the message `failed`. |
|
|
48
|
+
| Database changes | A new table `scheduled_messages` stores each message with its `send_at` time. |
|
|
49
|
+
|
|
50
|
+
If you cannot write a claim, you do not know the answer yet. Make it a decision (rule 4).
|
|
51
|
+
|
|
52
|
+
### 3. Every node has a visual
|
|
53
|
+
|
|
54
|
+
Select the visual from what the claim is about:
|
|
55
|
+
|
|
56
|
+
| Claim is about | `visual.kind` |
|
|
57
|
+
|---|---|
|
|
58
|
+
| What a user sees | `mockup`: an ASCII screen of the UI state. |
|
|
59
|
+
| Code that changes | `code`: the real file path, line range, and a short snippet. Start added lines with `+` and removed lines with `-`. |
|
|
60
|
+
| Order of steps or data flow | `flow`: the steps in order. |
|
|
61
|
+
| Values, limits, mappings, comparisons | `table`. |
|
|
62
|
+
|
|
63
|
+
Keep each visual small: a maximum of about 15 lines. A visual shows the claim; it does not repeat it. Copy code snippets from the real files when they exist. Do not invent line numbers.
|
|
64
|
+
|
|
65
|
+
### 4. Decisions sit next to the part they change
|
|
66
|
+
|
|
67
|
+
- Put each open question in the `decisions` array of the node that it affects. Do not collect questions at the end.
|
|
68
|
+
- Give 2–4 options. Each option has a short `label` and a `description` with the trade-off.
|
|
69
|
+
- Mark exactly one option `"recommended": true` when you have a recommendation. Put it first.
|
|
70
|
+
- Set `"multi": true` only when the options are not exclusive.
|
|
71
|
+
- The page adds an "Other" option automatically. Do not add one.
|
|
72
|
+
- Ask only what changes the plan. Do not ask what the code, the request, or a safe default already answers. Write the default as a claim.
|
|
73
|
+
- Number decisions `D1`, `D2`, … in the order they appear in the tree. Keep the IDs stable across revisions.
|
|
74
|
+
|
|
75
|
+
### 5. Write in Simplified Technical English (ASD-STE100)
|
|
76
|
+
|
|
77
|
+
All text in the page (claims, details, questions, options, captions) follows these rules:
|
|
78
|
+
|
|
79
|
+
- One topic in each sentence. A maximum of 20 words in an instruction and 25 words in a description.
|
|
80
|
+
- Use the active voice and the simple present tense. ("The worker sends the message", not "The message will be sent".)
|
|
81
|
+
- Use the imperative for instructions. ("Run the migration.")
|
|
82
|
+
- Use one word for one meaning. Do not change terms for variety. If you write "worker" once, do not write "job runner" later.
|
|
83
|
+
- Use simple, common words: "use" (not "utilize"), "start" (not "initiate"), "show" (not "display"), "make sure" (not "ensure").
|
|
84
|
+
- Do not use "-ing" words as nouns or adjectives when a simple verb or noun is clear.
|
|
85
|
+
- Write the articles ("a", "the") and the subjects. Do not use telegraphic style.
|
|
86
|
+
- Do not use filler: "basically", "simply", "robust", "seamless", "leverage".
|
|
87
|
+
- Technical names (files, functions, flags) stay exactly as they are in the code.
|
|
88
|
+
|
|
89
|
+
### 6. The response loop
|
|
90
|
+
|
|
91
|
+
The user's paste starts with `PLAN RESPONSE`. When you get it:
|
|
92
|
+
|
|
93
|
+
- It is **feedback on the plan, not approval**. Do not start implementation.
|
|
94
|
+
- Apply every answered decision and every agreed claim.
|
|
95
|
+
- For each disagreement or comment, change the claim. If the change is not clear, ask in chat.
|
|
96
|
+
- If there are unanswered decisions or new questions, ask them in chat with `AskUserQuestion` (or plain text if the tool is not available). Ask only what blocks the next revision.
|
|
97
|
+
- Then write a new revision to the **same file**: increase `revision` by 1 and list each change in `changes` (one short sentence for each change). Keep node IDs and decision IDs stable; give new nodes new IDs. Remove answered decisions and write their result as claims.
|
|
98
|
+
- Repeat until the user explicitly says to implement (for example, "approved", "go", "implement it"). A response with zero disagreements is still not approval: ask "Implement this plan?" in chat.
|
|
99
|
+
- When you implement, follow the tree. Report against the node IDs.
|
|
100
|
+
|
|
101
|
+
## Schema
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"title": "Scheduled messages",
|
|
106
|
+
"request": "The user's request in one sentence.",
|
|
107
|
+
"file": ".plans/scheduled-messages.html",
|
|
108
|
+
"revision": 1,
|
|
109
|
+
"changes": [],
|
|
110
|
+
"outcome": "A user can write a message now, and the app sends it at the time that the user selects.",
|
|
111
|
+
"nodes": [
|
|
112
|
+
{
|
|
113
|
+
"id": "1",
|
|
114
|
+
"claim": "A user can schedule a maximum of 50 messages at the same time.",
|
|
115
|
+
"detail": "Optional. One or two sentences of context.",
|
|
116
|
+
"visual": { "kind": "mockup", "caption": "Composer with the schedule menu", "text": "+-----------------+\n| Message [Send v]|\n+-----------------+" },
|
|
117
|
+
"decisions": [
|
|
118
|
+
{
|
|
119
|
+
"id": "D1",
|
|
120
|
+
"question": "What is the maximum number of scheduled messages for each user?",
|
|
121
|
+
"multi": false,
|
|
122
|
+
"options": [
|
|
123
|
+
{ "label": "50", "description": "Low storage cost. Most users schedule fewer than 10.", "recommended": true },
|
|
124
|
+
{ "label": "100", "description": "Supports power users. Doubles the worst-case queue size." }
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
],
|
|
128
|
+
"children": [
|
|
129
|
+
{
|
|
130
|
+
"id": "1.1",
|
|
131
|
+
"claim": "The worker sends each message within 60 seconds of its scheduled time.",
|
|
132
|
+
"visual": { "kind": "flow", "caption": "Send path", "steps": ["Composer", "POST /schedule", "scheduled_messages", "Worker (every 30 s)", "Chat service"] },
|
|
133
|
+
"children": [
|
|
134
|
+
{
|
|
135
|
+
"id": "1.1.1",
|
|
136
|
+
"claim": "A new migration adds the table scheduled_messages.",
|
|
137
|
+
"visual": { "kind": "code", "file": "db/migrations/0042_scheduled_messages.sql", "change": "add", "lang": "sql", "snippet": "+CREATE TABLE scheduled_messages (\n+ id BIGSERIAL PRIMARY KEY,\n+ send_at TIMESTAMPTZ NOT NULL\n+);" }
|
|
138
|
+
}
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
]
|
|
142
|
+
}
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Field notes:
|
|
148
|
+
|
|
149
|
+
- `id` is the tree position (`1`, `1.2`, `1.2.3`). The page uses it in the response text.
|
|
150
|
+
- `visual.kind` is one of `mockup` (`text`), `code` (`file`, optional `lines` such as `"40-58"`, `change` = `add` | `modify` | `delete`, `snippet`), `flow` (`steps`), or `table` (`columns`, `rows`). All kinds accept `caption`.
|
|
151
|
+
- `changes` is empty in revision 1.
|
|
152
|
+
- The page computes the level from the depth in the tree. Do not add a level field.
|
|
153
|
+
|
|
154
|
+
## Self-check before you open the page
|
|
155
|
+
|
|
156
|
+
- [ ] The outcome sentence and the level-1 claims alone explain the full result.
|
|
157
|
+
- [ ] Every node is a claim with a specific value, name, or behavior. No node is a heading.
|
|
158
|
+
- [ ] Every node has a visual of the correct kind.
|
|
159
|
+
- [ ] Every level-3 node names a real file path (or "new file").
|
|
160
|
+
- [ ] Every decision is on the node that it affects, has 2–4 options, and has a recommendation when you have one.
|
|
161
|
+
- [ ] All text follows Simplified Technical English.
|
|
162
|
+
- [ ] The JSON is valid, and every `</` inside it is written as `<\/`.
|
|
@@ -0,0 +1,453 @@
|
|
|
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
|
+
<title>Plan</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #f7f6f3; --surface: #ffffff; --surface-2: #f0eee9; --border: #dedad2;
|
|
10
|
+
--text: #1f1d1a; --muted: #6b665e; --accent: #c2541a; --accent-soft: #fbe9df;
|
|
11
|
+
--ok: #2f7d4f; --ok-soft: #e2f2e8; --bad: #b3261e; --bad-soft: #f9e1df;
|
|
12
|
+
--code-bg: #1e1c19; --code-text: #ece8e1; --add: #9fe0b0; --del: #f2a39c;
|
|
13
|
+
--l1: #c2541a; --l2: #3b6ea8; --l3: #6d5a9c;
|
|
14
|
+
--radius: 10px; --mono: ui-monospace, "Cascadia Code", Consolas, Menlo, monospace;
|
|
15
|
+
}
|
|
16
|
+
@media (prefers-color-scheme: dark) {
|
|
17
|
+
:root:not([data-theme="light"]) {
|
|
18
|
+
--bg: #161513; --surface: #1f1e1b; --surface-2: #2a2825; --border: #3a3733;
|
|
19
|
+
--text: #ece8e1; --muted: #a29c92; --accent: #e8743b; --accent-soft: #3a2519;
|
|
20
|
+
--ok: #6cc28f; --ok-soft: #1c3226; --bad: #f08a80; --bad-soft: #3d1f1c;
|
|
21
|
+
--code-bg: #0f0e0d; --l1: #e8743b; --l2: #6fa0db; --l3: #a593d6;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
:root[data-theme="dark"] {
|
|
25
|
+
--bg: #161513; --surface: #1f1e1b; --surface-2: #2a2825; --border: #3a3733;
|
|
26
|
+
--text: #ece8e1; --muted: #a29c92; --accent: #e8743b; --accent-soft: #3a2519;
|
|
27
|
+
--ok: #6cc28f; --ok-soft: #1c3226; --bad: #f08a80; --bad-soft: #3d1f1c;
|
|
28
|
+
--code-bg: #0f0e0d; --l1: #e8743b; --l2: #6fa0db; --l3: #a593d6;
|
|
29
|
+
}
|
|
30
|
+
* { box-sizing: border-box; }
|
|
31
|
+
body { margin: 0; background: var(--bg); color: var(--text); font: 15px/1.55 system-ui, -apple-system, "Segoe UI", sans-serif; }
|
|
32
|
+
main { max-width: 920px; margin: 0 auto; padding: 24px 16px 120px; }
|
|
33
|
+
h1 { font-size: 26px; line-height: 1.25; margin: 0 0 6px; }
|
|
34
|
+
.muted { color: var(--muted); }
|
|
35
|
+
.small { font-size: 13px; }
|
|
36
|
+
.card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; }
|
|
37
|
+
.outcome { margin: 18px 0; border-left: 4px solid var(--accent); }
|
|
38
|
+
.outcome .label, .eyebrow { font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); font-weight: 600; }
|
|
39
|
+
.outcome p { margin: 4px 0 0; font-size: 18px; font-weight: 600; }
|
|
40
|
+
.changes { margin: 0 0 18px; background: var(--accent-soft); border-color: transparent; }
|
|
41
|
+
.changes ul { margin: 6px 0 0; padding-left: 20px; }
|
|
42
|
+
.toolbar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 12px 0 18px; }
|
|
43
|
+
.seg { display: inline-flex; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
|
|
44
|
+
.seg button { border: 0; border-right: 1px solid var(--border); border-radius: 0; }
|
|
45
|
+
.seg button:last-child { border-right: 0; }
|
|
46
|
+
button { font: inherit; color: var(--text); background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 6px 12px; cursor: pointer; }
|
|
47
|
+
button:hover { background: var(--surface-2); }
|
|
48
|
+
button.active { background: var(--text); color: var(--bg); }
|
|
49
|
+
button.primary { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600; padding: 9px 18px; }
|
|
50
|
+
button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
51
|
+
.legend { display: flex; gap: 12px; flex-wrap: wrap; }
|
|
52
|
+
.lv { display: inline-block; font-size: 11px; font-weight: 700; padding: 1px 7px; border-radius: 999px; color: #fff; white-space: nowrap; }
|
|
53
|
+
.lv1 { background: var(--l1); } .lv2 { background: var(--l2); } .lv3 { background: var(--l3); }
|
|
54
|
+
|
|
55
|
+
.node { margin: 10px 0; }
|
|
56
|
+
.node > .card { padding: 12px 14px; }
|
|
57
|
+
.node.lvl1 > .card { border-left: 4px solid var(--l1); }
|
|
58
|
+
.node.lvl2 > .card { border-left: 4px solid var(--l2); }
|
|
59
|
+
.node.lvl3 > .card { border-left: 4px solid var(--l3); }
|
|
60
|
+
.node.verdict-disagree > .card { background: var(--bad-soft); }
|
|
61
|
+
.node.verdict-agree > .card { background: var(--ok-soft); }
|
|
62
|
+
.children { margin-left: 22px; padding-left: 12px; border-left: 1px dashed var(--border); }
|
|
63
|
+
.head { display: flex; gap: 10px; align-items: flex-start; }
|
|
64
|
+
.twisty { flex: none; width: 26px; height: 26px; padding: 0; border-radius: 6px; font-size: 12px; line-height: 1; }
|
|
65
|
+
.twisty[disabled] { visibility: hidden; }
|
|
66
|
+
.claim { flex: 1; min-width: 0; }
|
|
67
|
+
.claim .text { font-weight: 600; font-size: 16px; }
|
|
68
|
+
.claim .meta { display: flex; gap: 8px; align-items: center; margin-bottom: 2px; }
|
|
69
|
+
.nid { font-family: var(--mono); font-size: 12px; color: var(--muted); }
|
|
70
|
+
.detail { margin: 6px 0 0; color: var(--muted); }
|
|
71
|
+
.body { margin-top: 10px; display: grid; gap: 10px; }
|
|
72
|
+
.collapsed > .children { display: none; }
|
|
73
|
+
|
|
74
|
+
.visual { border: 1px solid var(--border); border-radius: 8px; overflow: hidden; background: var(--surface-2); }
|
|
75
|
+
.visual .cap { font-size: 12px; color: var(--muted); padding: 6px 10px; border-bottom: 1px solid var(--border); display: flex; gap: 8px; justify-content: space-between; flex-wrap: wrap; }
|
|
76
|
+
.visual pre { margin: 0; padding: 10px 12px; overflow-x: auto; font: 13px/1.45 var(--mono); }
|
|
77
|
+
.screen .chrome { display: flex; gap: 6px; padding: 8px 10px; border-bottom: 1px solid var(--border); background: var(--surface); }
|
|
78
|
+
.screen .chrome i { width: 10px; height: 10px; border-radius: 50%; background: var(--border); }
|
|
79
|
+
.screen pre { background: var(--surface); }
|
|
80
|
+
.code pre { background: var(--code-bg); color: var(--code-text); }
|
|
81
|
+
.code .add { color: var(--add); } .code .del { color: var(--del); }
|
|
82
|
+
.tag { font-family: var(--mono); font-size: 11px; padding: 0 6px; border-radius: 4px; border: 1px solid var(--border); }
|
|
83
|
+
.flow { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding: 12px; }
|
|
84
|
+
.flow .step { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 6px 10px; font-size: 13px; }
|
|
85
|
+
.flow .arrow { color: var(--muted); }
|
|
86
|
+
.tbl { width: 100%; border-collapse: collapse; font-size: 13px; background: var(--surface); }
|
|
87
|
+
.tbl th, .tbl td { text-align: left; padding: 6px 10px; border-bottom: 1px solid var(--border); vertical-align: top; }
|
|
88
|
+
.tbl th { background: var(--surface-2); }
|
|
89
|
+
.tblwrap { overflow-x: auto; }
|
|
90
|
+
|
|
91
|
+
.decision { border: 1px solid var(--accent); border-radius: 8px; padding: 12px; background: var(--surface); }
|
|
92
|
+
.decision .q { font-weight: 600; margin: 2px 0 8px; }
|
|
93
|
+
.decision.answered { border-color: var(--ok); }
|
|
94
|
+
.opt { display: flex; gap: 10px; align-items: flex-start; padding: 8px 10px; border: 1px solid var(--border); border-radius: 8px; margin: 6px 0; cursor: pointer; }
|
|
95
|
+
.opt:hover { background: var(--surface-2); }
|
|
96
|
+
.opt input { margin-top: 4px; }
|
|
97
|
+
.opt .rec { font-size: 11px; font-weight: 700; color: var(--ok); margin-left: 6px; }
|
|
98
|
+
.opt .desc { display: block; font-size: 13px; color: var(--muted); }
|
|
99
|
+
.opt input[type="text"] { width: 100%; margin-top: 4px; }
|
|
100
|
+
input[type="text"], textarea { font: inherit; color: var(--text); background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 6px 8px; width: 100%; }
|
|
101
|
+
textarea { min-height: 64px; resize: vertical; }
|
|
102
|
+
|
|
103
|
+
.feedback { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
|
|
104
|
+
.feedback button { font-size: 13px; padding: 4px 10px; }
|
|
105
|
+
.feedback button.on-agree { background: var(--ok); border-color: var(--ok); color: #fff; }
|
|
106
|
+
.feedback button.on-disagree { background: var(--bad); border-color: var(--bad); color: #fff; }
|
|
107
|
+
.feedback textarea { flex-basis: 100%; }
|
|
108
|
+
|
|
109
|
+
.bar { position: fixed; left: 0; right: 0; bottom: 0; background: var(--surface); border-top: 1px solid var(--border); padding: 10px 16px; }
|
|
110
|
+
.bar .in { max-width: 920px; margin: 0 auto; display: flex; gap: 12px; align-items: center; justify-content: space-between; flex-wrap: wrap; }
|
|
111
|
+
.modal { position: fixed; inset: 0; background: rgba(0,0,0,.45); display: none; align-items: center; justify-content: center; padding: 16px; }
|
|
112
|
+
.modal.open { display: flex; }
|
|
113
|
+
.modal .card { width: min(760px, 100%); max-height: 90vh; display: flex; flex-direction: column; gap: 10px; }
|
|
114
|
+
.modal textarea { flex: 1; min-height: 320px; font: 13px/1.45 var(--mono); }
|
|
115
|
+
.row { display: flex; gap: 8px; justify-content: flex-end; flex-wrap: wrap; }
|
|
116
|
+
.error { background: var(--bad-soft); color: var(--bad); border-color: var(--bad); margin-bottom: 16px; }
|
|
117
|
+
@media (max-width: 560px) { .children { margin-left: 8px; padding-left: 8px; } h1 { font-size: 22px; } }
|
|
118
|
+
</style>
|
|
119
|
+
</head>
|
|
120
|
+
<body>
|
|
121
|
+
<main id="app"></main>
|
|
122
|
+
|
|
123
|
+
<div class="bar"><div class="in">
|
|
124
|
+
<span id="progress" class="small muted"></span>
|
|
125
|
+
<div class="row">
|
|
126
|
+
<button id="reset" type="button">Clear answers</button>
|
|
127
|
+
<button id="respond" class="primary" type="button">Respond</button>
|
|
128
|
+
</div>
|
|
129
|
+
</div></div>
|
|
130
|
+
|
|
131
|
+
<div class="modal" id="modal" role="dialog" aria-modal="true" aria-labelledby="modal-title">
|
|
132
|
+
<div class="card">
|
|
133
|
+
<strong id="modal-title">Paste this text into Claude</strong>
|
|
134
|
+
<span class="small muted">The text is copied to your clipboard. If the copy fails, select the text and copy it manually.</span>
|
|
135
|
+
<textarea id="out" readonly aria-label="Response text"></textarea>
|
|
136
|
+
<div class="row">
|
|
137
|
+
<button id="close" type="button">Close</button>
|
|
138
|
+
<button id="copy" class="primary" type="button">Copy again</button>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<!--
|
|
144
|
+
PLAN DATA. Claude replaces the line below with the plan JSON (see SKILL.md for the schema).
|
|
145
|
+
Inside this block, write every "</" as "<\/" so the JSON cannot close the script tag.
|
|
146
|
+
-->
|
|
147
|
+
<script id="plan-data" type="application/json">
|
|
148
|
+
__PLAN_DATA__
|
|
149
|
+
</script>
|
|
150
|
+
|
|
151
|
+
<script>
|
|
152
|
+
(function () {
|
|
153
|
+
"use strict";
|
|
154
|
+
|
|
155
|
+
var DEMO = {
|
|
156
|
+
title: "Scheduled messages (demo plan)",
|
|
157
|
+
request: "Let users schedule chat messages.",
|
|
158
|
+
outcome: "A user can write a message now and the app sends it at a time that the user selects.",
|
|
159
|
+
revision: 1,
|
|
160
|
+
nodes: [
|
|
161
|
+
{
|
|
162
|
+
id: "1", claim: "A user can schedule a maximum of 50 messages at the same time.",
|
|
163
|
+
detail: "The app rejects message 51 and shows an error.",
|
|
164
|
+
visual: { kind: "mockup", caption: "Composer with the schedule menu", text: "+--------------------------------------+\n| Type a message... [Send v] |\n| | Send now |\n| | Schedule |\n+--------------------------------------+" },
|
|
165
|
+
decisions: [{ id: "D1", question: "What is the maximum number of scheduled messages for each user?", options: [
|
|
166
|
+
{ label: "50", description: "Low storage cost. Most users schedule fewer than 10.", recommended: true },
|
|
167
|
+
{ label: "100", description: "Supports power users. Doubles the worst-case queue size." },
|
|
168
|
+
{ label: "No limit", description: "Simple for users. Abuse risk." }
|
|
169
|
+
] }],
|
|
170
|
+
children: [
|
|
171
|
+
{ id: "1.1", claim: "The worker sends each message within 60 seconds of its scheduled time.",
|
|
172
|
+
visual: { kind: "flow", steps: ["Composer", "POST /schedule", "scheduled_messages table", "Worker (every 30 s)", "Chat service"] },
|
|
173
|
+
children: [
|
|
174
|
+
{ id: "1.1.1", claim: "A new table stores the scheduled messages.",
|
|
175
|
+
visual: { kind: "code", file: "db/migrations/0042_scheduled_messages.sql", change: "add", lang: "sql",
|
|
176
|
+
snippet: "+CREATE TABLE scheduled_messages (\n+ id BIGSERIAL PRIMARY KEY,\n+ user_id BIGINT NOT NULL,\n+ send_at TIMESTAMPTZ NOT NULL\n+);" } }
|
|
177
|
+
] }
|
|
178
|
+
]
|
|
179
|
+
}
|
|
180
|
+
]
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
var LEVEL = { 1: "Outcome", 2: "How it works", 3: "Where in code" };
|
|
184
|
+
|
|
185
|
+
function el(tag, attrs, kids) {
|
|
186
|
+
var n = document.createElement(tag);
|
|
187
|
+
if (attrs) Object.keys(attrs).forEach(function (k) {
|
|
188
|
+
var v = attrs[k];
|
|
189
|
+
if (v == null || v === false) return;
|
|
190
|
+
if (k === "text") n.textContent = v;
|
|
191
|
+
else if (k === "class") n.className = v;
|
|
192
|
+
else if (k.slice(0, 2) === "on") n.addEventListener(k.slice(2), v);
|
|
193
|
+
else n.setAttribute(k, v === true ? "" : v);
|
|
194
|
+
});
|
|
195
|
+
(kids || []).forEach(function (c) { if (c != null) n.appendChild(typeof c === "string" ? document.createTextNode(c) : c); });
|
|
196
|
+
return n;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function loadPlan() {
|
|
200
|
+
var raw = document.getElementById("plan-data").textContent.trim();
|
|
201
|
+
if (!raw || raw === "__PLAN_DATA__") return { plan: DEMO, error: null };
|
|
202
|
+
try { return { plan: JSON.parse(raw), error: null }; }
|
|
203
|
+
catch (e) { return { plan: DEMO, error: "The plan JSON is not valid: " + e.message + ". The page shows the demo plan." }; }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
var loaded = loadPlan();
|
|
207
|
+
var plan = loaded.plan;
|
|
208
|
+
var nodesById = {}, decisions = [], order = [];
|
|
209
|
+
(function index(list, depth, parent) {
|
|
210
|
+
(list || []).forEach(function (n) {
|
|
211
|
+
n._depth = Math.min(depth, 3); n._parent = parent;
|
|
212
|
+
nodesById[n.id] = n; order.push(n);
|
|
213
|
+
(n.decisions || []).forEach(function (d) { d._node = n; decisions.push(d); });
|
|
214
|
+
index(n.children, depth + 1, n);
|
|
215
|
+
});
|
|
216
|
+
})(plan.nodes, 1, null);
|
|
217
|
+
|
|
218
|
+
// ---------- state (per-viewer convenience only; the Respond text is the real hand-off) ----------
|
|
219
|
+
var KEY = "plan-state:" + (plan.title || "") + ":" + (plan.revision || 1);
|
|
220
|
+
var state = { verdict: {}, comment: {}, answer: {}, other: {}, general: "" };
|
|
221
|
+
try { var saved = JSON.parse(localStorage.getItem(KEY) || "null"); if (saved) state = Object.assign(state, saved); } catch (e) {}
|
|
222
|
+
function save() { try { localStorage.setItem(KEY, JSON.stringify(state)); } catch (e) {} updateProgress(); }
|
|
223
|
+
|
|
224
|
+
// ---------- visuals ----------
|
|
225
|
+
function renderVisual(v) {
|
|
226
|
+
if (!v || !v.kind) return null;
|
|
227
|
+
var cap = v.caption ? el("span", { text: v.caption }) : null;
|
|
228
|
+
if (v.kind === "mockup") {
|
|
229
|
+
return el("div", { class: "visual screen" }, [
|
|
230
|
+
el("div", { class: "chrome", "aria-hidden": "true" }, [el("i"), el("i"), el("i")]),
|
|
231
|
+
cap ? el("div", { class: "cap" }, [cap]) : null,
|
|
232
|
+
el("pre", { text: v.text || "" })
|
|
233
|
+
]);
|
|
234
|
+
}
|
|
235
|
+
if (v.kind === "code") {
|
|
236
|
+
var pre = el("pre");
|
|
237
|
+
String(v.snippet || "").split("\n").forEach(function (line, i, all) {
|
|
238
|
+
var cls = line[0] === "+" ? "add" : line[0] === "-" ? "del" : null;
|
|
239
|
+
pre.appendChild(el("span", { class: cls, text: line + (i < all.length - 1 ? "\n" : "") }));
|
|
240
|
+
});
|
|
241
|
+
return el("div", { class: "visual code" }, [
|
|
242
|
+
el("div", { class: "cap" }, [
|
|
243
|
+
el("span", { text: (v.file || "") + (v.lines ? ":" + v.lines : "") }),
|
|
244
|
+
el("span", {}, [v.change ? el("span", { class: "tag", text: v.change }) : null, v.caption ? " " + v.caption : null])
|
|
245
|
+
]),
|
|
246
|
+
pre
|
|
247
|
+
]);
|
|
248
|
+
}
|
|
249
|
+
if (v.kind === "flow") {
|
|
250
|
+
var flow = el("div", { class: "flow" });
|
|
251
|
+
(v.steps || []).forEach(function (s, i) {
|
|
252
|
+
if (i) flow.appendChild(el("span", { class: "arrow", "aria-hidden": "true", text: "→" }));
|
|
253
|
+
flow.appendChild(el("span", { class: "step", text: s }));
|
|
254
|
+
});
|
|
255
|
+
return el("div", { class: "visual" }, [cap ? el("div", { class: "cap" }, [cap]) : null, flow]);
|
|
256
|
+
}
|
|
257
|
+
if (v.kind === "table") {
|
|
258
|
+
var t = el("table", { class: "tbl" }, [
|
|
259
|
+
el("thead", {}, [el("tr", {}, (v.columns || []).map(function (c) { return el("th", { text: c }); }))]),
|
|
260
|
+
el("tbody", {}, (v.rows || []).map(function (r) { return el("tr", {}, r.map(function (c) { return el("td", { text: String(c) }); })); }))
|
|
261
|
+
]);
|
|
262
|
+
return el("div", { class: "visual" }, [cap ? el("div", { class: "cap" }, [cap]) : null, el("div", { class: "tblwrap" }, [t])]);
|
|
263
|
+
}
|
|
264
|
+
return el("div", { class: "visual" }, [el("pre", { text: v.text || JSON.stringify(v, null, 2) })]);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ---------- decisions ----------
|
|
268
|
+
function renderDecision(d) {
|
|
269
|
+
var box = el("div", { class: "decision", id: "dec-" + d.id });
|
|
270
|
+
var name = "d-" + d.id;
|
|
271
|
+
var type = d.multi ? "checkbox" : "radio";
|
|
272
|
+
function current() { var a = state.answer[d.id]; return d.multi ? (a || []) : a; }
|
|
273
|
+
function refresh() { box.classList.toggle("answered", isAnswered(d)); }
|
|
274
|
+
function pick(label, checked) {
|
|
275
|
+
if (d.multi) {
|
|
276
|
+
var a = current().filter(function (x) { return x !== label; });
|
|
277
|
+
if (checked) a.push(label);
|
|
278
|
+
state.answer[d.id] = a;
|
|
279
|
+
} else state.answer[d.id] = label;
|
|
280
|
+
refresh(); save();
|
|
281
|
+
}
|
|
282
|
+
box.appendChild(el("div", { class: "eyebrow", text: "Decision " + d.id + (d.multi ? " · select one or more" : "") }));
|
|
283
|
+
box.appendChild(el("div", { class: "q", text: d.question }));
|
|
284
|
+
var opts = (d.options || []).concat([{ label: "Other", _other: true }]);
|
|
285
|
+
opts.forEach(function (o) {
|
|
286
|
+
var a = current();
|
|
287
|
+
var checked = d.multi ? a.indexOf(o.label) >= 0 : a === o.label;
|
|
288
|
+
var input = el("input", { type: type, name: name, checked: checked, onchange: function (e) { pick(o.label, e.target.checked); } });
|
|
289
|
+
var kids = [el("strong", { text: o.label }), o.recommended ? el("span", { class: "rec", text: "Recommended" }) : null];
|
|
290
|
+
if (o.description) kids.push(el("span", { class: "desc", text: o.description }));
|
|
291
|
+
if (o._other) kids.push(el("input", { type: "text", placeholder: "Write your answer", value: state.other[d.id] || "",
|
|
292
|
+
"aria-label": "Other answer for " + d.id,
|
|
293
|
+
oninput: function (e) { state.other[d.id] = e.target.value; if (e.target.value && !input.checked) { input.checked = true; pick("Other", true); } else save(); } }));
|
|
294
|
+
box.appendChild(el("label", { class: "opt" }, [input, el("span", { style: "flex:1;min-width:0" }, kids)]));
|
|
295
|
+
});
|
|
296
|
+
refresh();
|
|
297
|
+
return box;
|
|
298
|
+
}
|
|
299
|
+
function isAnswered(d) {
|
|
300
|
+
var a = state.answer[d.id];
|
|
301
|
+
if (d.multi ? !(a && a.length) : !a) return false;
|
|
302
|
+
var hasOther = d.multi ? a.indexOf("Other") >= 0 : a === "Other";
|
|
303
|
+
return !hasOther || !!(state.other[d.id] || "").trim();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ---------- nodes ----------
|
|
307
|
+
var nodeEls = {};
|
|
308
|
+
function renderNode(n) {
|
|
309
|
+
var hasKids = !!(n.children && n.children.length);
|
|
310
|
+
var wrap = el("div", { class: "node lvl" + n._depth, id: "node-" + n.id });
|
|
311
|
+
var twisty = el("button", { class: "twisty", type: "button", disabled: !hasKids, "aria-label": "Show or hide details of " + n.id,
|
|
312
|
+
onclick: function () { setOpen(n.id, wrap.classList.contains("collapsed")); } });
|
|
313
|
+
var comment = el("textarea", { placeholder: "What is wrong, or what do you want instead?", "aria-label": "Comment on " + n.id });
|
|
314
|
+
comment.value = state.comment[n.id] || "";
|
|
315
|
+
comment.addEventListener("input", function () { state.comment[n.id] = comment.value; save(); });
|
|
316
|
+
var agree = el("button", { type: "button", text: "✓ Agree" });
|
|
317
|
+
var disagree = el("button", { type: "button", text: "✗ Disagree" });
|
|
318
|
+
var note = el("button", { type: "button", text: "Comment" });
|
|
319
|
+
function paint() {
|
|
320
|
+
var v = state.verdict[n.id];
|
|
321
|
+
agree.className = v === "agree" ? "on-agree" : "";
|
|
322
|
+
disagree.className = v === "disagree" ? "on-disagree" : "";
|
|
323
|
+
wrap.classList.toggle("verdict-agree", v === "agree");
|
|
324
|
+
wrap.classList.toggle("verdict-disagree", v === "disagree");
|
|
325
|
+
comment.style.display = (v === "disagree" || comment.value || comment._show) ? "" : "none";
|
|
326
|
+
}
|
|
327
|
+
function setVerdict(v) { state.verdict[n.id] = state.verdict[n.id] === v ? null : v; save(); paint(); if (v === "disagree") comment.focus(); }
|
|
328
|
+
agree.addEventListener("click", function () { setVerdict("agree"); });
|
|
329
|
+
disagree.addEventListener("click", function () { setVerdict("disagree"); });
|
|
330
|
+
note.addEventListener("click", function () { comment._show = true; paint(); comment.focus(); });
|
|
331
|
+
|
|
332
|
+
var body = el("div", { class: "body" }, [renderVisual(n.visual)]
|
|
333
|
+
.concat((n.decisions || []).map(renderDecision))
|
|
334
|
+
.concat([el("div", { class: "feedback" }, [agree, disagree, note, comment])]));
|
|
335
|
+
|
|
336
|
+
wrap.appendChild(el("div", { class: "card" }, [
|
|
337
|
+
el("div", { class: "head" }, [twisty, el("div", { class: "claim" }, [
|
|
338
|
+
el("div", { class: "meta" }, [el("span", { class: "nid", text: n.id }), el("span", { class: "lv lv" + n._depth, text: LEVEL[n._depth] })]),
|
|
339
|
+
el("div", { class: "text", text: n.claim }),
|
|
340
|
+
n.detail ? el("p", { class: "detail", text: n.detail }) : null
|
|
341
|
+
])]),
|
|
342
|
+
body
|
|
343
|
+
]));
|
|
344
|
+
if (hasKids) wrap.appendChild(el("div", { class: "children" }, n.children.map(renderNode)));
|
|
345
|
+
nodeEls[n.id] = { wrap: wrap, twisty: twisty, hasKids: hasKids };
|
|
346
|
+
paint();
|
|
347
|
+
return wrap;
|
|
348
|
+
}
|
|
349
|
+
function setOpen(id, open) {
|
|
350
|
+
var e = nodeEls[id]; if (!e || !e.hasKids) return;
|
|
351
|
+
e.wrap.classList.toggle("collapsed", !open);
|
|
352
|
+
e.twisty.textContent = open ? "▾" : "▸";
|
|
353
|
+
e.twisty.setAttribute("aria-expanded", String(open));
|
|
354
|
+
}
|
|
355
|
+
function setDepth(depth) {
|
|
356
|
+
order.forEach(function (n) { setOpen(n.id, n._depth < depth); });
|
|
357
|
+
document.querySelectorAll("[data-depth]").forEach(function (b) { b.classList.toggle("active", +b.dataset.depth === depth); });
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ---------- page ----------
|
|
361
|
+
var app = document.getElementById("app");
|
|
362
|
+
document.title = plan.title || "Plan";
|
|
363
|
+
if (loaded.error) app.appendChild(el("div", { class: "card error", text: loaded.error }));
|
|
364
|
+
app.appendChild(el("div", { class: "eyebrow", text: "Plan · revision " + (plan.revision || 1) }));
|
|
365
|
+
app.appendChild(el("h1", { text: plan.title || "Plan" }));
|
|
366
|
+
if (plan.request) app.appendChild(el("div", { class: "muted", text: "Request: " + plan.request }));
|
|
367
|
+
app.appendChild(el("div", { class: "card outcome" }, [el("div", { class: "label", text: "Result after this plan" }), el("p", { text: plan.outcome || "" })]));
|
|
368
|
+
if (plan.changes && plan.changes.length) {
|
|
369
|
+
app.appendChild(el("div", { class: "card changes" }, [el("div", { class: "eyebrow", text: "Changes since the previous revision" }),
|
|
370
|
+
el("ul", {}, plan.changes.map(function (c) { return el("li", { text: c }); }))]));
|
|
371
|
+
}
|
|
372
|
+
app.appendChild(el("div", { class: "toolbar" }, [
|
|
373
|
+
el("span", { class: "small muted", text: "Show:" }),
|
|
374
|
+
el("div", { class: "seg" }, [1, 2, 3].map(function (d) {
|
|
375
|
+
return el("button", { type: "button", "data-depth": d, text: ["Outcomes", "+ How", "+ Code"][d - 1], onclick: function () { setDepth(d); } });
|
|
376
|
+
})),
|
|
377
|
+
el("span", { class: "legend small muted" }, [el("span", { class: "lv lv1", text: LEVEL[1] }), el("span", { class: "lv lv2", text: LEVEL[2] }), el("span", { class: "lv lv3", text: LEVEL[3] })])
|
|
378
|
+
]));
|
|
379
|
+
(plan.nodes || []).forEach(function (n) { app.appendChild(renderNode(n)); });
|
|
380
|
+
|
|
381
|
+
var general = el("textarea", { placeholder: "Anything else? Missing parts, priorities, constraints.", "aria-label": "General comment" });
|
|
382
|
+
general.value = state.general || "";
|
|
383
|
+
general.addEventListener("input", function () { state.general = general.value; save(); });
|
|
384
|
+
app.appendChild(el("div", { class: "card", style: "margin-top:18px" }, [el("div", { class: "eyebrow", text: "General comment" }), general]));
|
|
385
|
+
setDepth(1);
|
|
386
|
+
|
|
387
|
+
// ---------- respond ----------
|
|
388
|
+
function quote(s) { return '"' + String(s).replace(/\s+/g, " ").trim() + '"'; }
|
|
389
|
+
function buildResponse() {
|
|
390
|
+
var out = [];
|
|
391
|
+
out.push("PLAN RESPONSE");
|
|
392
|
+
out.push("Plan: " + (plan.title || "") + " (revision " + (plan.revision || 1) + ")");
|
|
393
|
+
if (plan.file) out.push("File: " + plan.file);
|
|
394
|
+
out.push("This is feedback on the plan. It is not approval to implement.");
|
|
395
|
+
out.push("");
|
|
396
|
+
out.push("## Decisions");
|
|
397
|
+
var open = [];
|
|
398
|
+
decisions.forEach(function (d) {
|
|
399
|
+
if (!isAnswered(d)) { open.push(d); return; }
|
|
400
|
+
var a = state.answer[d.id]; var picked = d.multi ? a : [a];
|
|
401
|
+
var rec = (d.options || []).filter(function (o) { return o.recommended; }).map(function (o) { return o.label; });
|
|
402
|
+
var text = picked.map(function (p) { return p === "Other" ? "Other: " + quote(state.other[d.id]) : quote(p); }).join(", ");
|
|
403
|
+
var tag = picked.length === rec.length && picked.every(function (p) { return rec.indexOf(p) >= 0; }) ? " (recommended)" : rec.length ? " (not the recommendation)" : "";
|
|
404
|
+
out.push("- " + d.id + " [node " + d._node.id + "] " + d.question + " -> " + text + tag);
|
|
405
|
+
});
|
|
406
|
+
if (!decisions.length) out.push("- (the plan has no decisions)");
|
|
407
|
+
if (open.length) { out.push(""); out.push("## Unanswered decisions"); open.forEach(function (d) { out.push("- " + d.id + " [node " + d._node.id + "] " + d.question); }); }
|
|
408
|
+
|
|
409
|
+
var dis = order.filter(function (n) { return state.verdict[n.id] === "disagree"; });
|
|
410
|
+
var com = order.filter(function (n) { return state.verdict[n.id] !== "disagree" && (state.comment[n.id] || "").trim(); });
|
|
411
|
+
var agr = order.filter(function (n) { return state.verdict[n.id] === "agree"; });
|
|
412
|
+
out.push(""); out.push("## Disagreements");
|
|
413
|
+
if (!dis.length) out.push("- none");
|
|
414
|
+
dis.forEach(function (n) { out.push("- " + n.id + " " + quote(n.claim) + (state.comment[n.id] ? "\n Reason: " + quote(state.comment[n.id]) : "\n Reason: (none given)")); });
|
|
415
|
+
if (com.length) { out.push(""); out.push("## Comments"); com.forEach(function (n) { out.push("- " + n.id + " " + quote(n.claim) + "\n Comment: " + quote(state.comment[n.id])); }); }
|
|
416
|
+
out.push(""); out.push("## Agreed");
|
|
417
|
+
out.push(agr.length ? "- " + agr.map(function (n) { return n.id; }).join(", ") : "- none marked");
|
|
418
|
+
var silent = order.length - dis.length - agr.length - com.length;
|
|
419
|
+
if (silent > 0) out.push("- " + silent + " node(s) have no feedback");
|
|
420
|
+
if ((state.general || "").trim()) { out.push(""); out.push("## General comment"); out.push(state.general.trim()); }
|
|
421
|
+
return out.join("\n");
|
|
422
|
+
}
|
|
423
|
+
function copy(text) {
|
|
424
|
+
var ta = document.getElementById("out");
|
|
425
|
+
if (navigator.clipboard && window.isSecureContext) return navigator.clipboard.writeText(text).catch(function () { ta.select(); document.execCommand("copy"); });
|
|
426
|
+
ta.select(); try { document.execCommand("copy"); } catch (e) {}
|
|
427
|
+
return Promise.resolve();
|
|
428
|
+
}
|
|
429
|
+
var modal = document.getElementById("modal");
|
|
430
|
+
document.getElementById("respond").addEventListener("click", function () {
|
|
431
|
+
var t = buildResponse(); var ta = document.getElementById("out");
|
|
432
|
+
ta.value = t; modal.classList.add("open"); copy(t);
|
|
433
|
+
});
|
|
434
|
+
document.getElementById("copy").addEventListener("click", function () { copy(document.getElementById("out").value); });
|
|
435
|
+
document.getElementById("close").addEventListener("click", function () { modal.classList.remove("open"); });
|
|
436
|
+
modal.addEventListener("click", function (e) { if (e.target === modal) modal.classList.remove("open"); });
|
|
437
|
+
document.addEventListener("keydown", function (e) { if (e.key === "Escape") modal.classList.remove("open"); });
|
|
438
|
+
document.getElementById("reset").addEventListener("click", function () {
|
|
439
|
+
if (!confirm("Clear all answers and comments on this page?")) return;
|
|
440
|
+
try { localStorage.removeItem(KEY); } catch (e) {}
|
|
441
|
+
location.reload();
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
function updateProgress() {
|
|
445
|
+
var answered = decisions.filter(isAnswered).length;
|
|
446
|
+
var touched = order.filter(function (n) { return state.verdict[n.id] || (state.comment[n.id] || "").trim(); }).length;
|
|
447
|
+
document.getElementById("progress").textContent = "Decisions " + answered + "/" + decisions.length + " · feedback on " + touched + "/" + order.length + " nodes";
|
|
448
|
+
}
|
|
449
|
+
updateProgress();
|
|
450
|
+
})();
|
|
451
|
+
</script>
|
|
452
|
+
</body>
|
|
453
|
+
</html>
|