saivivek6 Claude Opus 4.8 commited on
Commit
c97e8a9
·
1 Parent(s): 0b491ba

Deploy vivek app to Space: Vue UI (bundled dist), streaming widgets, JSON schema renderer

Browse files

Clean deploy: replaces prior anupa variant on main. Includes built frontend-vue/dist.
Excludes local adaptiveui.sqlite3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. Dockerfile +3 -3
  2. README.md +4 -4
  3. anupa/app.py +0 -13
  4. anupa/backend/config.py +0 -86
  5. anupa/backend/primitive_widget_map.py +0 -476
  6. anupa/backend/primitives.json +0 -51
  7. anupa/backend/server.py +0 -650
  8. vivek/.env.example +44 -0
  9. {anupa → vivek}/.gitattributes +0 -0
  10. {anupa → vivek}/.gitignore +3 -0
  11. {anupa → vivek}/Dockerfile +0 -0
  12. {anupa → vivek}/README.md +31 -4
  13. {anupa → vivek}/UpdatedReadme.md +3 -2
  14. vivek/app.py +23 -0
  15. {anupa → vivek}/backend/__init__.py +0 -0
  16. vivek/backend/auth.py +179 -0
  17. {anupa → vivek}/backend/combined_prompt.py +424 -100
  18. vivek/backend/config.py +264 -0
  19. vivek/backend/db.py +774 -0
  20. {anupa → vivek}/backend/engine.py +80 -9
  21. {anupa → vivek}/backend/llm.py +0 -0
  22. vivek/backend/server.py +1399 -0
  23. {anupa → vivek}/backend/utils.py +36 -4
  24. {anupa → vivek}/backend/widget_prompt.py +16 -10
  25. vivek/backend/widget_stream.py +137 -0
  26. vivek/frontend-vue/.gitignore +24 -0
  27. vivek/frontend-vue/README.md +5 -0
  28. vivek/frontend-vue/components.json +20 -0
  29. vivek/frontend-vue/dist/assets/index-B2HHLelt.css +0 -0
  30. vivek/frontend-vue/dist/assets/index-Bq2Moqhb.js +0 -0
  31. vivek/frontend-vue/dist/favicon.svg +1 -0
  32. vivek/frontend-vue/dist/icons.svg +24 -0
  33. vivek/frontend-vue/dist/index.html +14 -0
  34. vivek/frontend-vue/index.html +13 -0
  35. vivek/frontend-vue/package-lock.json +2537 -0
  36. vivek/frontend-vue/package.json +41 -0
  37. vivek/frontend-vue/public/favicon.svg +1 -0
  38. vivek/frontend-vue/public/icons.svg +24 -0
  39. vivek/frontend-vue/src/App.vue +8 -0
  40. vivek/frontend-vue/src/assets/bg-about.svg +33 -0
  41. vivek/frontend-vue/src/assets/bg-analytics.svg +30 -0
  42. vivek/frontend-vue/src/assets/bg-future.svg +31 -0
  43. vivek/frontend-vue/src/assets/hero.png +0 -0
  44. vivek/frontend-vue/src/assets/vite.svg +1 -0
  45. vivek/frontend-vue/src/assets/vue.svg +1 -0
  46. vivek/frontend-vue/src/components/AnalyticsPanel.vue +205 -0
  47. vivek/frontend-vue/src/components/BanditInsightsSection.vue +254 -0
  48. vivek/frontend-vue/src/components/HelloWorld.vue +93 -0
  49. vivek/frontend-vue/src/components/LiveWidgetFrame.vue +111 -0
  50. vivek/frontend-vue/src/components/LiveWidgetSchema.vue +213 -0
Dockerfile CHANGED
@@ -1,4 +1,4 @@
1
- FROM python:3.12-slim
2
 
3
  RUN useradd -m -u 1000 user
4
  USER user
@@ -6,10 +6,10 @@ ENV PATH="/home/user/.local/bin:$PATH"
6
 
7
  WORKDIR /app
8
 
9
- COPY --chown=user ./anupa/requirements.txt /app/requirements.txt
10
  RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt
11
 
12
- COPY --chown=user ./anupa /app
13
 
14
  ENV PORT=7860
15
  EXPOSE 7860
 
1
+ FROM python:3.12-slim
2
 
3
  RUN useradd -m -u 1000 user
4
  USER user
 
6
 
7
  WORKDIR /app
8
 
9
+ COPY --chown=user ./vivek/requirements.txt /app/requirements.txt
10
  RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt
11
 
12
+ COPY --chown=user ./vivek /app
13
 
14
  ENV PORT=7860
15
  EXPOSE 7860
README.md CHANGED
@@ -1,5 +1,5 @@
1
- ---
2
- title: AdaptiveUI (anupa)
3
  emoji: "🧠"
4
  colorFrom: indigo
5
  colorTo: blue
@@ -7,8 +7,8 @@ sdk: docker
7
  pinned: false
8
  ---
9
 
10
- # AdaptiveUI (anupa)
11
 
12
  Claude-style single-call response + widget HTML rendered in an iframe.
13
 
14
- This Space runs the `anupa` server on port 7860.
 
1
+ ---
2
+ title: AdaptiveUI (vivek)
3
  emoji: "🧠"
4
  colorFrom: indigo
5
  colorTo: blue
 
7
  pinned: false
8
  ---
9
 
10
+ # AdaptiveUI (vivek)
11
 
12
  Claude-style single-call response + widget HTML rendered in an iframe.
13
 
14
+ This Space runs the `vivek` server on port 7860.
anupa/app.py DELETED
@@ -1,13 +0,0 @@
1
- """Entry point.
2
-
3
- Run:
4
- pip install -r requirements.txt
5
- python app.py
6
- open http://localhost:5051
7
- """
8
-
9
- from backend.server import run_server
10
-
11
-
12
- if __name__ == "__main__":
13
- run_server()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
anupa/backend/config.py DELETED
@@ -1,86 +0,0 @@
1
- """Configuration and constants for the Adaptive Presentation Engine backend."""
2
-
3
- from pathlib import Path
4
- from dotenv import load_dotenv
5
- import os
6
-
7
- load_dotenv(override=True)
8
- # Runtime mode: 'openai_compat' uses an OpenAI-compatible remote API,
9
- # otherwise the code will prefer Anthropic (Claude) endpoints.
10
- LLM_MODE = os.getenv("LLM_MODE", "openai_compat").lower()
11
-
12
- # If false (default), strategy “primitives” are treated as soft style hints
13
- # and the backend will not aggressively post-process text to enforce them.
14
- STRICT_PRIMITIVES = os.getenv("STRICT_PRIMITIVES", "0").strip().lower() in {"1", "true", "yes", "on"}
15
-
16
- # Provider routing:
17
- # - /api/chat_plain uses BASELINE_LLM_MODE
18
- # - /api/chat uses ADAPTIVE_LLM_MODE
19
- BASELINE_LLM_MODE = os.getenv("BASELINE_LLM_MODE", LLM_MODE).lower()
20
- ADAPTIVE_LLM_MODE = os.getenv("ADAPTIVE_LLM_MODE", LLM_MODE).lower()
21
-
22
- # Widget rendering mode:
23
- # - "html": model outputs full HTML in <WIDGET> (rendered in iframe)
24
- # - "json": model outputs JSON UI schema in <WIDGET> (rendered by frontend renderer)
25
- WIDGET_MODE = os.getenv("WIDGET_MODE", "json").strip().lower()
26
-
27
- # Combined (Claude-style) generation limits
28
- COMBINED_TIMEOUT_SECONDS = int(os.getenv("COMBINED_TIMEOUT_SECONDS", "30"))
29
- COMBINED_MAX_TOKENS = int(os.getenv("COMBINED_MAX_TOKENS", "2800"))
30
-
31
- # OpenAI-compatible endpoint (Groq / other providers that offer OpenAI API)
32
- OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL", "https://api.groq.com/openai/v1")
33
- OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
34
- OPENAI_MODEL = os.getenv("OPENAI_MODEL", "llama-3.1-8b-instant")
35
-
36
- # Anthropic (Claude) API
37
- ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY", "")
38
- ANTHROPIC_MODEL = os.getenv("ANTHROPIC_MODEL", "claude-opus-4-6")
39
-
40
- # Anthropic Fast Mode (optional; safe to enable because we retry on failure)
41
- # Implementation uses the Anthropic beta header + request body speed field via SDK-supported
42
- # `extra_headers` / `extra_body`.
43
- ANTHROPIC_FAST_MODE_ENABLED = os.getenv("ANTHROPIC_FAST_MODE_ENABLED", "1").strip().lower() in {
44
- "1", "true", "yes", "on"
45
- }
46
- ANTHROPIC_FAST_MODE_SPEED = os.getenv("ANTHROPIC_FAST_MODE_SPEED", "fast") # "fast" or "standard"
47
- ANTHROPIC_FAST_MODE_BETA = os.getenv("ANTHROPIC_FAST_MODE_BETA", "fast-mode-2026-02-01")
48
-
49
- # Model / engine hyperparameters
50
- D = 10
51
- LAMBDA = 0.01
52
- GAMMA = 0.99
53
- ALPHA_G = 0.05
54
-
55
- # Base Thompson temperature (used when not forcing exploration)
56
- TS_TEMPERATURE = float(os.getenv("TS_TEMPERATURE", "2.0"))
57
-
58
- # ---- Exploration / correction knobs (Option B) ----
59
- # When we detect strong negative feedback, we:
60
- # - boost temperature (more exploration)
61
- # - penalize repeating the last strategy (strongly)
62
- # - damp the posterior for the last strategy (reduce confidence)
63
- NEG_EXPLORE_THRESHOLD = float(os.getenv("NEG_EXPLORE_THRESHOLD", "0.40")) # ev["neg"] >= this => explore
64
- EXPLORE_TEMP_BOOST = float(os.getenv("EXPLORE_TEMP_BOOST", "2.0"))
65
- EXPLORE_SCORE_PENALTY = float(os.getenv("EXPLORE_SCORE_PENALTY", "1.75"))
66
-
67
- # Posterior damping strength (scaled by neg_strength in [0,1])
68
- NEG_MU_SHRINK = float(os.getenv("NEG_MU_SHRINK", "0.25")) # shrink mean magnitude
69
- NEG_SINV_SHRINK = float(os.getenv("NEG_SINV_SHRINK", "0.35")) # shrink precision -> bigger covariance
70
-
71
- # Strategy primitives and descriptions used to constrain generation style
72
- STRATEGIES = {
73
- "structured_bullets": "Use 3-5 bullet points only (start each line with '- '). No intro sentence. Do NOT ask questions. Do NOT use numbered lists.",
74
- "narrative_prose": "Write 2-3 short paragraphs. No bullet points.",
75
- "concise_direct": "Reply in at most 3 sentences. Be direct.",
76
- "socratic_questions": "Brief acknowledgement, then ask 1-2 clarifying questions.",
77
- "step_by_step": "Numbered list of 3-6 steps only.",
78
- # NEW
79
- "comparison_table": "Return a single MARKDOWN TABLE only. Use columns that help compare options (e.g., Option | Pros | Cons | Best for). No bullets outside the table.",
80
- "visualization": "Return a simple TEXT visualization only (ASCII bar chart or small table-of-values). Put it in a fenced code block. No extra prose outside the code block.",
81
- }
82
- STRATEGY_NAMES = list(STRATEGIES.keys())
83
- K = len(STRATEGY_NAMES)
84
-
85
- HERE = Path(__file__).resolve().parent
86
- INDEX_HTML = HERE.parent / "index.html"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
anupa/backend/primitive_widget_map.py DELETED
@@ -1,476 +0,0 @@
1
- """Primitive-driven widget contracts, validator, and Claude-quality fallback templates.
2
-
3
- Each primitive in primitives.json defines:
4
- - required_components: HTML signals that must appear in generated widget.
5
- - forbidden_components: signals that must NOT appear.
6
- - extra_context: injected into build_widget_prompt() to steer the model.
7
- - fallback_type: which deterministic template to render if model violates contract.
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- import html as _html_mod
13
- import json
14
- import re
15
- from pathlib import Path
16
- from typing import Any
17
-
18
- from .widget_prompt import inject_design_system
19
-
20
- # ── Load primitives from JSON ──────────────────────────────────────────────
21
-
22
- _PRIMITIVES_PATH = Path(__file__).resolve().parent / "primitives.json"
23
-
24
- def _load_primitives() -> dict[str, Any]:
25
- try:
26
- return json.loads(_PRIMITIVES_PATH.read_text(encoding="utf-8"))
27
- except Exception:
28
- return {}
29
-
30
- PRIMITIVES: dict[str, Any] = _load_primitives()
31
-
32
- _DEFAULT_SPEC: dict[str, Any] = {
33
- "required_components": [],
34
- "forbidden_components": [],
35
- "extra_context": "",
36
- "fallback_type": "generic",
37
- }
38
-
39
-
40
- def get_primitive_spec(strategy_id: str) -> dict[str, Any]:
41
- return PRIMITIVES.get(strategy_id, _DEFAULT_SPEC)
42
-
43
-
44
- # ── Component signal registry ──────────────────────────────────────────────
45
-
46
- _COMPONENT_SIGNALS: dict[str, list[str]] = {
47
- "range_slider": ['type="range"', "type='range'"],
48
- "calculator": ["function calc(", "oninput=\"calc(", "oninput='calc("],
49
- "comparison_cards": ["class=\"raised", "class='raised", "comparison", "highlight"],
50
- "tabs": ["class=\"tab", "class='tab", ".panel"],
51
- "step_navigator": ["step-row", "step-num", "step_row"],
52
- "chart": ["<canvas", "Chart(", "new Chart"],
53
- "insight_cards": ["class=\"card", "class='card", "card-title"],
54
- "result_box": ["result-box", "result-val"],
55
- "prompt_chips": ["pill", "sendPrompt"],
56
- "insight_card": ["class=\"card", "class='card"],
57
- "search": ['type="text"', 'oninput="filter', 'oninput=\'filter'],
58
- "progress_bars": ["progress-bar", "progress-wrap"],
59
- }
60
-
61
- def _html_has(html: str, signals: list[str]) -> bool:
62
- return any(s.lower() in html.lower() for s in signals)
63
-
64
-
65
- def validate_widget_html(html: str, spec: dict[str, Any]) -> tuple[bool, str]:
66
- """Return (valid, reason). Invalid means model violated primitive contract."""
67
- if not html:
68
- return False, "empty_html"
69
- for comp in spec.get("required_components", []):
70
- sigs = _COMPONENT_SIGNALS.get(comp, [comp])
71
- if not _html_has(html, sigs):
72
- return False, f"missing_required:{comp}"
73
- for comp in spec.get("forbidden_components", []):
74
- sigs = _COMPONENT_SIGNALS.get(comp, [comp])
75
- if _html_has(html, sigs):
76
- return False, f"forbidden_component:{comp}"
77
- return True, ""
78
-
79
-
80
- # ── Escape helper ──────────────────────────────────────────────────────────
81
-
82
- def _e(s: str) -> str:
83
- return _html_mod.escape(str(s or ""), quote=True)
84
-
85
-
86
- # ── Fallback template helpers ──────────────────────────────────────────────
87
-
88
- def _parse_bullets(response: str) -> list[dict[str, str]]:
89
- """Extract bullet points as {title, body} dicts."""
90
- lines = response.split("\n")
91
- items: list[dict[str, str]] = []
92
- for line in lines:
93
- stripped = line.strip()
94
- for prefix in ("-", "•", "*", "→"):
95
- if stripped.startswith(prefix):
96
- text = stripped[len(prefix):].strip()
97
- if ":" in text:
98
- parts = text.split(":", 1)
99
- items.append({"title": parts[0].strip(), "body": parts[1].strip()})
100
- else:
101
- items.append({"title": text[:50], "body": text})
102
- break
103
- if len(items) >= 6:
104
- break
105
- return items or [{"title": "Key Point", "body": response[:200]}]
106
-
107
-
108
- def _parse_steps(response: str) -> list[dict[str, str]]:
109
- """Extract numbered steps as {title, body} dicts."""
110
- lines = response.split("\n")
111
- items: list[dict[str, str]] = []
112
- for line in lines:
113
- stripped = line.strip()
114
- m = re.match(r"^(\d+)[.)]\s+(.+)$", stripped)
115
- if m:
116
- text = m.group(2).strip()
117
- if ":" in text:
118
- parts = text.split(":", 1)
119
- items.append({"title": parts[0].strip(), "body": parts[1].strip()})
120
- else:
121
- items.append({"title": f"Step {m.group(1)}", "body": text})
122
- if len(items) >= 8:
123
- break
124
- return items or [{"title": "Step 1", "body": response[:200]}]
125
-
126
-
127
- def _parse_questions(response: str) -> list[str]:
128
- """Extract clarifying questions from response text."""
129
- lines = response.split("\n")
130
- qs: list[str] = []
131
- for line in lines:
132
- stripped = line.strip()
133
- stripped = re.sub(r"^[-•*\d.)]+\s*", "", stripped)
134
- if "?" in stripped and len(stripped) > 10:
135
- qs.append(stripped)
136
- if len(qs) >= 4:
137
- break
138
- return qs or ["Can you tell me more about what you're looking for?"]
139
-
140
-
141
- def _parse_sections(response: str) -> list[dict[str, str]]:
142
- """Split response into 2-3 labelled sections for tabs."""
143
- paragraphs = [p.strip() for p in re.split(r"\n{2,}", response) if p.strip()]
144
- if len(paragraphs) >= 3:
145
- return [
146
- {"label": "Summary", "body": paragraphs[0]},
147
- {"label": "Details", "body": " ".join(paragraphs[1:-1])},
148
- {"label": "Takeaways", "body": paragraphs[-1]},
149
- ]
150
- if len(paragraphs) == 2:
151
- return [
152
- {"label": "Overview", "body": paragraphs[0]},
153
- {"label": "Details", "body": paragraphs[1]},
154
- ]
155
- return [{"label": "Response", "body": response[:600]}]
156
-
157
-
158
- def _parse_markdown_table(response: str) -> tuple[list[str], list[list[str]]] | None:
159
- lines = [ln.strip() for ln in response.splitlines() if ln.strip()]
160
- if len(lines) < 2:
161
- return None
162
-
163
- def split_row(line: str) -> list[str]:
164
- txt = line.strip()
165
- if txt.startswith("|"):
166
- txt = txt[1:]
167
- if txt.endswith("|"):
168
- txt = txt[:-1]
169
- return [c.strip() for c in txt.split("|")]
170
-
171
- def is_sep(line: str) -> bool:
172
- cells = split_row(line)
173
- if not cells:
174
- return False
175
- return all(bool(re.match(r"^:?-{3,}:?$", c)) for c in cells)
176
-
177
- for i in range(len(lines) - 1):
178
- head_ln = lines[i]
179
- sep_ln = lines[i + 1]
180
- if "|" not in head_ln or not is_sep(sep_ln):
181
- continue
182
- cols = split_row(head_ln)
183
- if not cols:
184
- continue
185
-
186
- rows: list[list[str]] = []
187
- for ln in lines[i + 2:]:
188
- if "|" not in ln:
189
- break
190
- row = split_row(ln)
191
- if not row:
192
- break
193
- while len(row) < len(cols):
194
- row.append("")
195
- rows.append(row[: len(cols)])
196
- if rows:
197
- return cols, rows
198
- return None
199
-
200
-
201
- # ── Fallback template builders ─────────────────────────────────────────────
202
-
203
- def _fallback_comparison(user_message: str, response: str) -> str:
204
- parsed_table = _parse_markdown_table(response)
205
- if parsed_table:
206
- cols, rows = parsed_table
207
- table_head = "".join(f"<th>{_e(c)}</th>" for c in cols)
208
- body_rows = []
209
- option_idx = 0
210
- if cols:
211
- c0 = cols[0].strip().lower()
212
- if c0 in {"option", "plan", "item", "choice", "name"}:
213
- option_idx = 0
214
- for row in rows[:8]:
215
- item_name = row[option_idx] if option_idx < len(row) else row[0]
216
- cells = "".join(f"<td>{_e(v)}</td>" for v in row)
217
- body_rows.append(
218
- f"""<tr class="clickable"
219
- onclick="sendPrompt('Tell me more about {_e(item_name)} from this comparison table')">{cells}</tr>"""
220
- )
221
- table_html = f"""
222
- <table>
223
- <thead><tr>{table_head}</tr></thead>
224
- <tbody>{''.join(body_rows)}</tbody>
225
- </table>
226
- """
227
- return f"""<html><head></head><body><div class="widget-root">
228
- <div class="tabs">
229
- <button class="tab active" onclick="showTab(this,'t-compare')">Compare</button>
230
- <button class="tab" onclick="showTab(this,'t-verdict')">Verdict</button>
231
- </div>
232
- <div id="t-compare" class="panel active">
233
- <div class="card">
234
- <div class="card-title">Comparison</div>
235
- {table_html}
236
- </div>
237
- </div>
238
- <div id="t-verdict" class="panel">
239
- <div class="card">
240
- <div class="card-title">Verdict</div>
241
- <div style="font-size:13px;line-height:1.7;color:var(--text)">{_e(response[:360])}</div>
242
- </div>
243
- <button class="ask-btn" onclick="sendPrompt('Based on this table, which option fits my goal best: {_e(user_message[:80])}?')">Ask about this ↗</button>
244
- </div>
245
- <script>
246
- function showTab(btn,id){{
247
- document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
248
- document.querySelectorAll('.panel').forEach(p=>p.classList.remove('active'));
249
- btn.classList.add('active');
250
- document.getElementById(id).classList.add('active');
251
- }}
252
- </script>
253
- </div></body></html>"""
254
-
255
- lines = [l.strip() for l in response.split("\n") if l.strip()]
256
- nouns: list[str] = []
257
- for line in lines:
258
- m = re.search(r"\*\*(.+?)\*\*", line)
259
- if m:
260
- nouns.append(m.group(1))
261
- if len(nouns) >= 2:
262
- break
263
- if len(nouns) < 2:
264
- nouns = ["Option A", "Option B"]
265
-
266
- bullets = _parse_bullets(response)
267
- rows_a = bullets[:3]
268
- rows_b = bullets[3:6] or bullets[:3]
269
-
270
- card_a = "\n".join(
271
- f'<div class="raised" style="margin-bottom:8px" onclick="sendPrompt(\'Tell me more about {_e(nouns[0])} — {_e(r["title"])}\')"><div style="font-size:12px;font-weight:500">{_e(r["title"])}</div><div style="font-size:11px;color:var(--text2);margin-top:3px">{_e(r["body"][:80])}</div></div>'
272
- for r in rows_a
273
- )
274
- card_b = "\n".join(
275
- f'<div class="raised" style="margin-bottom:8px" onclick="sendPrompt(\'Tell me more about {_e(nouns[1])} — {_e(r["title"])}\')"><div style="font-size:12px;font-weight:500">{_e(r["title"])}</div><div style="font-size:11px;color:var(--text2);margin-top:3px">{_e(r["body"][:80])}</div></div>'
276
- for r in rows_b
277
- )
278
-
279
- return f"""<html><head></head><body><div class="widget-root">
280
- <div class="tabs">
281
- <button class="tab active" onclick="showTab(this,'t-compare')">Compare</button>
282
- <button class="tab" onclick="showTab(this,'t-verdict')">Verdict</button>
283
- </div>
284
- <div id="t-compare" class="panel active">
285
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
286
- <div>
287
- <div class="card-title">{_e(nouns[0])}</div>
288
- {card_a}
289
- </div>
290
- <div>
291
- <div class="card-title">{_e(nouns[1])}</div>
292
- {card_b}
293
- </div>
294
- </div>
295
- </div>
296
- <div id="t-verdict" class="panel">
297
- <div class="card">
298
- <div class="card-title">Verdict</div>
299
- <div style="font-size:13px;line-height:1.7;color:var(--text)">{_e(response[:300])}</div>
300
- </div>
301
- <button class="ask-btn" onclick="sendPrompt('Which is better for me — {_e(nouns[0])} or {_e(nouns[1])}? My goal is...')">Ask about this ↗</button>
302
- </div>
303
- <script>
304
- function showTab(btn,id){{
305
- document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
306
- document.querySelectorAll('.panel').forEach(p=>p.classList.remove('active'));
307
- btn.classList.add('active');
308
- document.getElementById(id).classList.add('active');
309
- }}
310
- </script>
311
- </div></body></html>"""
312
-
313
-
314
- def _fallback_chart(user_message: str, response: str) -> str:
315
- nums = re.findall(r"\b(\d+\.?\d*)\b", response)
316
- labels_raw = re.findall(r"\b([A-Z][a-z]{2,}(?:\s[A-Z][a-z]+)?)\b", response)
317
- values = [float(n) for n in nums[:8] if float(n) > 0][:8]
318
- labels = list(dict.fromkeys(labels_raw))[:len(values)]
319
- while len(labels) < len(values):
320
- labels.append(f"Item {len(labels)+1}")
321
- if not values:
322
- values = [10, 20, 15, 30, 25]
323
- labels = ["A", "B", "C", "D", "E"]
324
-
325
- js_labels = json.dumps(labels)
326
- js_values = json.dumps(values)
327
-
328
- return f"""<html><head><script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js"></script></head>
329
- <body><div class="widget-root">
330
- <div class="metric-grid" style="margin-bottom:12px">
331
- <div class="metric"><div class="metric-lbl">Data points</div><div class="metric-val">{len(values)}</div></div>
332
- <div class="metric"><div class="metric-lbl">Max value</div><div class="metric-val">{max(values):.1f}</div></div>
333
- <div class="metric"><div class="metric-lbl">Average</div><div class="metric-val">{sum(values)/len(values):.1f}</div></div>
334
- </div>
335
- <div style="position:relative;height:220px">
336
- <canvas id="ch"></canvas>
337
- </div>
338
- <button class="ask-btn" style="margin-top:12px" onclick="sendPrompt('Explain the trend shown in this chart for {_e(user_message[:60])}')">Ask about this ↗</button>
339
- <script>
340
- const dark=matchMedia('(prefers-color-scheme:dark)').matches;
341
- const tc=dark?'#8d93aa':'#5a5f72';
342
- const gc=dark?'rgba(255,255,255,0.06)':'rgba(0,0,0,0.06)';
343
- new Chart(document.getElementById('ch'),{{
344
- type:'bar',
345
- data:{{labels:{js_labels},datasets:[{{data:{js_values},backgroundColor:'rgba(55,138,221,0.7)',borderRadius:6,borderSkipped:false}}]}},
346
- options:{{responsive:true,maintainAspectRatio:false,plugins:{{legend:{{display:false}}}},scales:{{x:{{ticks:{{color:tc}},grid:{{color:gc}}}},y:{{ticks:{{color:tc}},grid:{{color:gc}}}}}}}}
347
- }});
348
- </script>
349
- </div></body></html>"""
350
-
351
-
352
- def _fallback_steps(user_message: str, response: str) -> str:
353
- steps = _parse_steps(response)
354
- rows = "\n".join(
355
- f"""<div class="step-row" onclick="sendPrompt('Tell me more about step {i+1}: {_e(s['title'])}')">
356
- <div class="step-num">{i+1}</div>
357
- <div><div class="step-title">{_e(s['title'])}</div>
358
- <div class="step-desc">{_e(s['body'][:100])}</div></div>
359
- </div>"""
360
- for i, s in enumerate(steps)
361
- )
362
- return f"""<html><head></head><body><div class="widget-root">
363
- <div class="card">
364
- <div class="card-title">{len(steps)} Steps</div>
365
- {rows}
366
- </div>
367
- <button class="ask-btn" onclick="sendPrompt('I completed the steps for {_e(user_message[:60])}. What should I do next?')">What next ↗</button>
368
- </div></body></html>"""
369
-
370
-
371
- def _fallback_bullets(user_message: str, response: str) -> str:
372
- items = _parse_bullets(response)
373
- cards = "\n".join(
374
- f"""<div class="raised" onclick="sendPrompt('Tell me more about: {_e(b['title'])}')">
375
- <div style="font-size:11px;font-weight:600;color:var(--accent);margin-bottom:4px">#{i+1}</div>
376
- <div style="font-size:13px;font-weight:500">{_e(b['title'])}</div>
377
- <div style="font-size:12px;color:var(--text2);margin-top:4px">{_e(b['body'][:100])}</div>
378
- </div>"""
379
- for i, b in enumerate(items)
380
- )
381
- return f"""<html><head></head><body><div class="widget-root">
382
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
383
- {cards}
384
- </div>
385
- <button class="ask-btn" onclick="sendPrompt('Which of these points matters most for {_e(user_message[:60])}?')">Explore further ↗</button>
386
- </div></body></html>"""
387
-
388
-
389
- def _fallback_narrative(user_message: str, response: str) -> str:
390
- sections = _parse_sections(response)
391
- tabs_html = "\n".join(
392
- f'<button class="tab{" active" if i==0 else ""}" onclick="showTab(this,\'s{i}\')">{_e(s["label"])}</button>'
393
- for i, s in enumerate(sections)
394
- )
395
- panels_html = "\n".join(
396
- f'<div id="s{i}" class="panel{" active" if i==0 else ""}"><div class="card"><div style="font-size:13px;line-height:1.75;color:var(--text)">{_e(s["body"])}</div></div></div>'
397
- for i, s in enumerate(sections)
398
- )
399
- return f"""<html><head></head><body><div class="widget-root">
400
- <div class="tabs">{tabs_html}</div>
401
- {panels_html}
402
- <button class="ask-btn" onclick="sendPrompt('I want to explore {_e(user_message[:60])} in more detail')">Go deeper ↗</button>
403
- <script>
404
- function showTab(btn,id){{
405
- document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
406
- document.querySelectorAll('.panel').forEach(p=>p.classList.remove('active'));
407
- btn.classList.add('active');
408
- document.getElementById(id).classList.add('active');
409
- }}
410
- </script>
411
- </div></body></html>"""
412
-
413
-
414
- def _fallback_concise(user_message: str, response: str) -> str:
415
- short = response.strip()[:280]
416
- return f"""<html><head></head><body><div class="widget-root">
417
- <div class="result-box">
418
- <div class="result-lbl">Answer</div>
419
- <div style="font-size:15px;font-weight:500;line-height:1.6;color:var(--text)">{_e(short)}</div>
420
- </div>
421
- <button class="ask-btn" onclick="sendPrompt('Can you explain this in more detail: {_e(user_message[:60])}')">Explain more ↗</button>
422
- </div></body></html>"""
423
-
424
-
425
- def _fallback_socratic(user_message: str, response: str) -> str:
426
- questions = _parse_questions(response)
427
- icons = ["🧮", "📈", "🎯", "💡", "🔍", "📊"]
428
-
429
- options = "\n".join(
430
- f"""
431
- <div class="step-row" style="cursor:pointer" onclick="sendPrompt({json.dumps(q)})">
432
- <div class="step-num">{icons[i % len(icons)]}</div>
433
- <div>
434
- <div class="step-title">{_e(q[:80])}</div>
435
- <div class="step-desc" style="margin-top:3px">
436
- {_e("Click to answer this and I will adapt the next explanation or calculator to that choice.")}
437
- </div>
438
- </div>
439
- </div>"""
440
- for i, q in enumerate(questions)
441
- )
442
-
443
- return f"""<html><head></head><body><div class="widget-root">
444
- <div class="card">
445
- <div class="card-title">Help me understand</div>
446
- <div style="font-size:13px;color:var(--text2);margin-bottom:12px">
447
- Click the option that best matches your situation. I will use your choice to decide whether to show a calculator, comparison table, or step-by-step guide next.
448
- </div>
449
- {options}
450
- </div>
451
- </div></body></html>"""
452
-
453
-
454
- def _fallback_generic(user_message: str, response: str) -> str:
455
- return _fallback_concise(user_message, response)
456
-
457
-
458
- _FALLBACK_BUILDERS = {
459
- "comparison": _fallback_comparison,
460
- "chart": _fallback_chart,
461
- "steps": _fallback_steps,
462
- "bullets": _fallback_bullets,
463
- "narrative": _fallback_narrative,
464
- "concise": _fallback_concise,
465
- "socratic": _fallback_socratic,
466
- "generic": _fallback_generic,
467
- }
468
-
469
-
470
- def build_primitive_fallback(strategy_id: str, user_message: str, response: str) -> str:
471
- """Build a deterministic Claude-quality fallback widget for the given primitive."""
472
- spec = get_primitive_spec(strategy_id)
473
- fallback_type = spec.get("fallback_type", "generic")
474
- builder = _FALLBACK_BUILDERS.get(fallback_type, _fallback_generic)
475
- raw_html = builder(user_message, response)
476
- return inject_design_system(raw_html)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
anupa/backend/primitives.json DELETED
@@ -1,51 +0,0 @@
1
- {
2
- "comparison_table": {
3
- "description": "Side-by-side comparison of 2 or more options with pros, cons, and recommendation.",
4
- "required_components": ["comparison_cards", "tabs"],
5
- "forbidden_components": ["range_slider", "calculator", "step_navigator"],
6
- "extra_context": "Build: tabs at top (Compare / Details / Verdict) + 2-column comparison cards side by side. Highlight recommended option with accent border and 'Recommended' badge. Every card must call sendPrompt with that option's name and key metric. Add a Verdict tab with a clear winner summary and ask button.",
7
- "fallback_type": "comparison"
8
- },
9
- "visualization": {
10
- "description": "Chart or visual representation of data — bar, line, or distribution.",
11
- "required_components": ["chart"],
12
- "forbidden_components": ["range_slider", "step_navigator", "comparison_cards"],
13
- "extra_context": "Build an enterprise mini-dashboard: (1) a metric cards row at top with key numbers, (2) a primary chart area, (3) a short insights section below. Prefer D3.js for charts (SVG) with a hover tooltip; use a line chart for time-series trends and a bar chart for comparisons. Add period toggle buttons (1Y/3Y/5Y/All) that filter the plotted data locally (no extra API calls). Include a brief explanatory paragraph above the chart and 2-4 insight bullets below it so the widget is a self-contained answer. Use only real numbers/entities from assistant_response — no placeholder data.",
14
- "fallback_type": "chart"
15
- },
16
- "step_by_step": {
17
- "description": "Numbered sequential steps — how-to guide or process walkthrough.",
18
- "required_components": ["step_navigator"],
19
- "forbidden_components": ["range_slider", "calculator", "comparison_cards", "chart"],
20
- "extra_context": "Build: a step navigator showing all steps as clickable rows with step number circle, title, and brief description. Each step row calls sendPrompt('Tell me more about step N: [title]'). Show step count badge. Add a final ask button summarizing the process.",
21
- "fallback_type": "steps"
22
- },
23
- "structured_bullets": {
24
- "description": "3-5 key bullet points — structured summary with clear takeaways.",
25
- "required_components": ["insight_cards"],
26
- "forbidden_components": ["range_slider", "chart", "step_navigator"],
27
- "extra_context": "Build: a grid of insight cards (2 per row) where each card shows one bullet point with an icon area, title extracted from the bullet, and body text. Each card is clickable and calls sendPrompt with that insight's title. Add a summary metric card at top if numbers are present.",
28
- "fallback_type": "bullets"
29
- },
30
- "narrative_prose": {
31
- "description": "Flowing paragraphs — explanatory or contextual narrative response.",
32
- "required_components": ["tabs", "insight_card"],
33
- "forbidden_components": ["range_slider", "step_navigator"],
34
- "extra_context": "Build: tabs splitting the narrative into 2-3 logical sections (Summary / Details / Key Takeaways). Each tab shows a clean card with formatted text. Add key metric highlights as small badges if numbers appear. Final tab has an ask button for follow-up.",
35
- "fallback_type": "narrative"
36
- },
37
- "concise_direct": {
38
- "description": "Short direct answer — 1-3 sentences, no fluff.",
39
- "required_components": ["result_box"],
40
- "forbidden_components": ["range_slider", "chart", "step_navigator", "tabs"],
41
- "extra_context": "Build: a single clean result card with a prominent headline answer and 1-2 supporting lines of context. Keep compact. Include one ask button below for follow-up exploration.",
42
- "fallback_type": "concise"
43
- },
44
- "socratic_questions": {
45
- "description": "Clarifying questions to understand user intent before answering.",
46
- "required_components": ["prompt_chips"],
47
- "forbidden_components": ["range_slider", "calculator", "chart", "step_navigator"],
48
- "extra_context": "Build: a card with a brief acknowledgement line, then 2-4 clickable question chips styled as pills. Each chip calls sendPrompt with that exact clarifying answer as a full natural language sentence. No extra controls needed.",
49
- "fallback_type": "socratic"
50
- }
51
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
anupa/backend/server.py DELETED
@@ -1,650 +0,0 @@
1
- """HTTP server and request handlers for the Adaptive Presentation Engine backend."""
2
-
3
- import json
4
- import os
5
- import re
6
- import time
7
- from http.server import HTTPServer, BaseHTTPRequestHandler
8
- from socketserver import ThreadingMixIn
9
-
10
- import numpy as np
11
-
12
- from . import config, llm
13
- from .engine import engine, USERB_ID
14
- from .widget_prompt import (
15
- estimate_widget_height,
16
- inject_design_system,
17
- )
18
- from .combined_prompt import (
19
- build_combined_system_prompt,
20
- build_combined_user_prompt,
21
- parse_combined_output,
22
- )
23
- from .utils import (
24
- fast_valence,
25
- enforce_response,
26
- detect_format_override,
27
- detect_explore_trigger,
28
- negative_strength,
29
- )
30
-
31
- def _looks_truncated_widget_html(html: str) -> bool:
32
- """Heuristic check for obviously cut-off widget HTML."""
33
- if not html:
34
- return True
35
- lower = html.lower()
36
- # Missing critical closures often indicates token truncation.
37
- if lower.count("<style") > lower.count("</style>"):
38
- return True
39
- if lower.count("<script") > lower.count("</script>"):
40
- return True
41
- if lower.count("<body") > lower.count("</body>"):
42
- return True
43
- if lower.count("<html") > lower.count("</html>"):
44
- return True
45
- # Rarely, response ends mid-token; catch abrupt ending.
46
- if re.search(r"[<{(]$", html.strip()):
47
- return True
48
- return False
49
-
50
-
51
- def _bool_env(name: str, default: bool) -> bool:
52
- raw = os.getenv(name)
53
- if raw is None:
54
- return default
55
- return raw.strip().lower() in {"1", "true", "yes", "on"}
56
-
57
-
58
- class Handler(BaseHTTPRequestHandler):
59
- def log_message(self, *a):
60
- pass
61
-
62
- def _cors(self):
63
- self.send_header("Access-Control-Allow-Origin", "*")
64
- self.send_header("Access-Control-Allow-Headers", "Content-Type")
65
- self.send_header("Access-Control-Allow-Methods", "GET,POST,OPTIONS")
66
-
67
- def _json(self, data, status=200):
68
- body = json.dumps(data).encode()
69
- self.send_response(status)
70
- self.send_header("Content-Type", "application/json")
71
- self.send_header("Content-Length", len(body))
72
- self._cors()
73
- self.end_headers()
74
- self.wfile.write(body)
75
-
76
- def _html(self):
77
- try:
78
- html = config.INDEX_HTML.read_bytes()
79
- except FileNotFoundError:
80
- self.send_response(404); self.end_headers(); return
81
- self.send_response(200)
82
- self.send_header("Content-Type", "text/html; charset=utf-8")
83
- self.send_header("Content-Length", len(html))
84
- self.end_headers()
85
- self.wfile.write(html)
86
-
87
- def _body(self):
88
- n = int(self.headers.get("Content-Length", 0))
89
- if not n:
90
- return {}
91
- raw = self.rfile.read(n)
92
- if not raw:
93
- return {}
94
- try:
95
- return json.loads(raw)
96
- except json.JSONDecodeError:
97
- # Don't crash the handler thread; return a sentinel the caller can handle.
98
- try:
99
- preview = raw[:500].decode("utf-8", errors="replace")
100
- except Exception:
101
- preview = repr(raw[:200])
102
- return {"__invalid_json__": True, "__raw_preview__": preview}
103
-
104
- def do_OPTIONS(self):
105
- self.send_response(204)
106
- self._cors()
107
- self.end_headers()
108
-
109
- def do_GET(self):
110
- p = self.path.split("?")[0]
111
- if p == "/":
112
- self._html(); return
113
-
114
- if p == "/api/health":
115
- if config.LLM_MODE == "openai_compat":
116
- h = llm.openai_health()
117
- self._json({"server": "ok", "mode": config.LLM_MODE, "openai_base_url": config.OPENAI_BASE_URL, "model": config.OPENAI_MODEL, **h})
118
- elif config.LLM_MODE == "anthropic":
119
- h = llm.anthropic_health()
120
- self._json({"server": "ok", "mode": config.LLM_MODE, **h})
121
- else:
122
- self._json({"server": "ok", "mode": config.LLM_MODE, "ok": False, "reachable": False, "error": "Unsupported LLM_MODE (expected openai_compat or anthropic)"})
123
- return
124
-
125
- if p == "/api/state":
126
- uid = self.path.split("uid=")[-1] if "uid=" in self.path else "demo"
127
- user = engine.get_user(uid)
128
- x = np.ones(config.D) * 0.5
129
- ub = engine.get_user(USERB_ID)
130
- self._json({
131
- "posterior": engine.user_posterior(uid, x),
132
- "global": engine.global_posterior(x),
133
- "userb": engine.posterior_summary(ub["mu"], ub["sigma_inv"], x),
134
- "global_n": engine.global_n,
135
- "n_users": len(engine.users),
136
- "msg_count": user["msg_count"],
137
- }); return
138
-
139
- self.send_response(404); self.end_headers()
140
-
141
- def do_POST(self):
142
- p = self.path.split("?")[0]
143
- body = self._body()
144
- if isinstance(body, dict) and body.get("__invalid_json__"):
145
- self._json({"error": "invalid_json", "preview": body.get("__raw_preview__", "")}, 400)
146
- return
147
-
148
- if p == "/api/chat_plain":
149
- uid = body.get("uid", "demo") + "_plain"
150
- msg = body.get("message", "").strip()
151
- if not msg:
152
- self._json({"error": "empty message"}, 400); return
153
-
154
- user = engine.get_user(uid)
155
-
156
- # Build a simple conversation prompt (no bandit, no enforced format).
157
- ctx = []
158
- for t in user["history"][-6:]:
159
- ctx += [f"User: {t['user']}", f"Assistant: {t['assistant']}"]
160
- ctx.append(f"User: {msg}")
161
- prompt = "\n".join(ctx)
162
-
163
- system = "You are a helpful AI assistant."
164
-
165
- try:
166
- base_mode = (config.BASELINE_LLM_MODE or config.LLM_MODE).lower()
167
- if base_mode == "openai_compat":
168
- response, elapsed, mode = llm.call_openai_compat(prompt, system, timeout=120)
169
- elif base_mode == "anthropic":
170
- response, elapsed, mode = llm.call_anthropic(prompt, system, timeout=120)
171
- else:
172
- raise RuntimeError("Unsupported BASELINE_LLM_MODE (expected openai_compat or anthropic)")
173
- except Exception as e:
174
- self._json({"error": f"LLM error: {str(e)}"}, 500)
175
- return
176
-
177
- if not response:
178
- self._json({"error": "LLM returned empty response. Check model/service."}, 500)
179
- return
180
-
181
- user["history"].append({"user": msg, "assistant": response})
182
- user["history"] = user["history"][-20:]
183
-
184
- self._json({
185
- "response": response,
186
- "elapsed": elapsed,
187
- "llm_mode": mode,
188
- }); return
189
-
190
- if p == "/api/chat":
191
- uid = body.get("uid", "demo")
192
- msg = body.get("message", "").strip()
193
- if not msg:
194
- self._json({"error": "empty message"}, 400); return
195
-
196
- user = engine.get_user(uid)
197
-
198
- # Auto-reward previous turn using valence heuristic.
199
- ev = fast_valence(msg, user["last_response"])
200
- auto_detected = False
201
- auto_r = None
202
-
203
- if user["last_response"] and user["last_x"] is not None and user["last_strategy"]:
204
- reward = float(np.clip(0.5 + 0.45*ev["pos"] - 0.45*ev["neg"], 0.05, 0.95))
205
- engine.update(uid, user["last_strategy"], np.array(user["last_x"]), reward)
206
- auto_detected = True
207
- auto_r = reward
208
-
209
- # --- NEW: explicit overrides + corrective exploration ---
210
- explicit = detect_format_override(msg, config.STRATEGY_NAMES)
211
- force_explore = bool(detect_explore_trigger(msg) or (ev.get("neg", 0.0) >= config.NEG_EXPLORE_THRESHOLD))
212
- neg_s = negative_strength(ev)
213
-
214
- strat, scores, x, prev = engine.select(
215
- uid, msg,
216
- force_explore=force_explore,
217
- neg_strength=neg_s,
218
- explicit_strategy=explicit,
219
- )
220
-
221
- format_rule = config.STRATEGIES.get(strat, "Be helpful and clear.")
222
-
223
- # ── Single-call combined prompt (Claude-style) ─────────────────
224
- combined_max_tokens = getattr(config, "COMBINED_MAX_TOKENS", 2800)
225
- # Keep the UI responsive by failing fast by default.
226
- combined_timeout = getattr(config, "COMBINED_TIMEOUT_SECONDS", 30)
227
-
228
- combined_system = build_combined_system_prompt(
229
- strategy_id=strat,
230
- format_rule=format_rule,
231
- primitive_extra_context="",
232
- user_message=msg,
233
- forbidden_components=None,
234
- required_components=None,
235
- )
236
- combined_prompt = build_combined_user_prompt(
237
- user_message=msg,
238
- history=user["history"],
239
- )
240
-
241
- try:
242
- adapt_mode = (config.ADAPTIVE_LLM_MODE or config.LLM_MODE).lower()
243
- if adapt_mode == "openai_compat":
244
- raw_combined, elapsed, mode = llm.call_openai_compat(
245
- combined_prompt,
246
- combined_system,
247
- timeout=combined_timeout,
248
- max_tokens=combined_max_tokens,
249
- temperature=0.2,
250
- )
251
- elif adapt_mode == "anthropic":
252
- raw_combined, elapsed, mode = llm.call_anthropic(
253
- combined_prompt,
254
- combined_system,
255
- timeout=combined_timeout,
256
- max_tokens=combined_max_tokens,
257
- temperature=0.2,
258
- )
259
- else:
260
- raise RuntimeError("Unsupported ADAPTIVE_LLM_MODE (expected openai_compat or anthropic)")
261
- except Exception as e:
262
- self._json({"error": f"LLM error: {str(e)}"}, 500)
263
- return
264
-
265
- if not raw_combined:
266
- self._json({"error": "LLM returned empty response. Check model/service."}, 500)
267
- return
268
-
269
- # Parse combined output into text response + widget payload (HTML or JSON schema).
270
- response, widget_payload_raw = parse_combined_output(raw_combined)
271
-
272
- if not response:
273
- response = raw_combined.strip()
274
-
275
- if config.STRICT_PRIMITIVES:
276
- response = enforce_response(strat, response)
277
-
278
- # ── Validate widget from single call ───────────────────────────
279
- widget_html = ""
280
- widget_schema = ""
281
- widget_height = 0
282
- widget_debug = ""
283
- widget_mode = getattr(config, "WIDGET_MODE", "json").strip().lower()
284
-
285
- if widget_payload_raw:
286
- if widget_mode == "json":
287
- widget_schema = widget_payload_raw
288
- widget_debug = widget_debug or "combined_schema_ok"
289
- else:
290
- if _looks_truncated_widget_html(widget_payload_raw):
291
- widget_debug = "combined_widget_truncated"
292
- else:
293
- widget_html = widget_payload_raw
294
- widget_height = estimate_widget_height(widget_payload_raw)
295
- widget_debug = widget_debug or "combined_widget_ok"
296
- else:
297
- widget_debug = widget_debug or ("combined_no_schema" if widget_mode == "json" else "combined_no_widget_tag")
298
- raw_preview = (raw_combined or "")[:800]
299
- if widget_mode != "json":
300
- # Never return a blank iframe: serve a safe interactive placeholder mini-app.
301
- placeholder = f"""
302
- <html><head></head><body>
303
- <div class="widget-root card">
304
- <div class="card-title">Interactive widget</div>
305
- <div style="color:var(--text2);font-size:13px;line-height:1.6">
306
- I couldn't generate a full widget for this turn. This placeholder stays interactive and can request the missing data.
307
- </div>
308
- <div class="ctrl-row" style="margin-top:12px">
309
- <div class="ctrl-lbl">Assumption</div>
310
- <input id="assump" type="range" min="0" max="100" step="1" value="50" style="flex:1" />
311
- <div class="ctrl-val" id="assumpv">50</div>
312
- </div>
313
- <div id="chart" class="raised" style="height:260px;margin-top:10px"></div>
314
- <div class="btn-group" style="margin-top:10px">
315
- <button class="btn" id="ask_range">Ask for date range</button>
316
- <button class="btn" id="ask_source">Ask for data source</button>
317
- </div>
318
- </div>
319
- <script src="https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js"></script>
320
- <script>
321
- const data = [
322
- {{name:'Series A', base: 100}},
323
- {{name:'Series B', base: 100}}
324
- ];
325
- const state = {{assumption: 50}};
326
- function compute(state, data) {{
327
- const k = (state.assumption/50);
328
- return {{
329
- labels: data.map(d=>d.name),
330
- values: data.map((d,i)=>Math.round(d.base*(i? (1.15*k):(1.05*k))))
331
- }};
332
- }}
333
- let chart;
334
- function render() {{
335
- const c = compute(state, data);
336
- document.getElementById('assumpv').textContent = String(state.assumption);
337
- if (!chart) chart = echarts.init(document.getElementById('chart'));
338
- chart.setOption({{
339
- backgroundColor:'transparent',
340
- tooltip:{{trigger:'axis'}},
341
- xAxis:{{type:'category',data:c.labels,axisLabel:{{color:'#5a5f72'}}}},
342
- yAxis:{{type:'value',axisLabel:{{color:'#5a5f72'}}}},
343
- series:[{{type:'bar',data:c.values}}]
344
- }});
345
- }}
346
- document.getElementById('assump').addEventListener('input', (e)=>{{ state.assumption = +e.target.value; render(); }});
347
- document.getElementById('ask_range').onclick = ()=> sendPrompt('Provide a date range to plot (e.g., 2020-01-01 to 2024-12-31).');
348
- document.getElementById('ask_source').onclick = ()=> sendPrompt('Which data source should we use for SPY and QQQ prices (yfinance, stooq, other)?');
349
- render();
350
- </script>
351
- </body></html>
352
- """.strip()
353
- widget_html = inject_design_system(placeholder)
354
- widget_height = estimate_widget_height(widget_html)
355
- widget_debug = "fallback_widget_generated"
356
-
357
- # No primitive fallback. If widget is missing/invalid, return text-only.
358
-
359
- user["history"].append({"user": msg, "assistant": response})
360
- user["history"] = user["history"][-20:]
361
- user["last_message"] = msg
362
- user["last_response"] = response
363
- user["last_strategy"] = strat
364
- user["last_x"] = x.tolist()
365
- user["msg_count"] += 1
366
-
367
- ub = engine.get_user(USERB_ID)
368
- self._json({
369
- "response": response,
370
- "strategy": strat,
371
- "prev_strategy": prev,
372
- "explicit": explicit,
373
- "force_explore": force_explore and (explicit is None),
374
- "instruction": config.STRATEGIES[strat],
375
- "elapsed": elapsed,
376
- "llm_mode": mode,
377
- "scores": {k: round(v, 4) for k, v in scores.items()},
378
- "x_vec": x.tolist(),
379
- "posterior": engine.user_posterior(uid, x),
380
- "global": engine.global_posterior(x),
381
- "userb": engine.posterior_summary(ub["mu"], ub["sigma_inv"], x),
382
- "global_n": engine.global_n,
383
- "auto_detected": auto_detected,
384
- "auto_r": auto_r,
385
- "auto_reason": ev["reason"],
386
- "widget_html": widget_html,
387
- "widget_schema": widget_schema,
388
- "widget_height": widget_height,
389
- "widget_debug": widget_debug,
390
- "widget_raw_preview": raw_preview if not (widget_html or widget_schema) else "",
391
- }); return
392
-
393
- if p == "/api/reward":
394
- uid = body.get("uid", "demo")
395
- strategy = body.get("strategy")
396
- x_vec = body.get("x_vec")
397
- reward = float(body.get("reward", 0.5))
398
- if strategy not in config.STRATEGY_NAMES or x_vec is None:
399
- self._json({"error": "bad request"}, 400); return
400
- x = np.array(x_vec, dtype=float)
401
- engine.update(uid, strategy, x, reward)
402
- ub = engine.get_user(USERB_ID)
403
- self._json({
404
- "posterior": engine.user_posterior(uid, x),
405
- "global": engine.global_posterior(x),
406
- "userb": engine.posterior_summary(ub["mu"], ub["sigma_inv"], x),
407
- "global_n": engine.global_n,
408
- }); return
409
-
410
- if p == "/api/preference":
411
- uid = body.get("uid", "demo")
412
- strategies = body.get("strategies", [])
413
- lock = bool(body.get("lock", False))
414
- engine.apply_preferences(uid, strategies, lock=lock)
415
- self._json({"posterior": engine.user_posterior(uid)}); return
416
-
417
- if p == "/api/reset":
418
- engine.reset_user(body.get("uid", "demo"))
419
- self._json({"ok": True}); return
420
-
421
- # ── NEW: adaptive streaming endpoint (Claude-like) ────────────────
422
- if p == "/api/chat_stream":
423
- uid = body.get("uid", "demo")
424
- msg = body.get("message", "").strip()
425
- if not msg:
426
- self._json({"error": "empty message"}, 400); return
427
-
428
- user = engine.get_user(uid)
429
-
430
- # Auto-reward previous turn using valence heuristic.
431
- ev = fast_valence(msg, user["last_response"])
432
- auto_detected = False
433
- auto_r = None
434
-
435
- if user["last_response"] and user["last_x"] is not None and user["last_strategy"]:
436
- reward = float(np.clip(0.5 + 0.45*ev["pos"] - 0.45*ev["neg"], 0.05, 0.95))
437
- engine.update(uid, user["last_strategy"], np.array(user["last_x"]), reward)
438
- auto_detected = True
439
- auto_r = reward
440
-
441
- explicit = detect_format_override(msg, config.STRATEGY_NAMES)
442
- force_explore = bool(detect_explore_trigger(msg) or (ev.get("neg", 0.0) >= config.NEG_EXPLORE_THRESHOLD))
443
- neg_s = negative_strength(ev)
444
-
445
- strat, scores, x, prev = engine.select(
446
- uid, msg,
447
- force_explore=force_explore,
448
- neg_strength=neg_s,
449
- explicit_strategy=explicit,
450
- )
451
-
452
- format_rule = config.STRATEGIES.get(strat, "Be helpful and clear.")
453
-
454
- combined_max_tokens = getattr(config, "COMBINED_MAX_TOKENS", 2800)
455
- combined_timeout = getattr(config, "COMBINED_TIMEOUT_SECONDS", 30)
456
-
457
- combined_system = build_combined_system_prompt(
458
- strategy_id=strat,
459
- format_rule=format_rule,
460
- primitive_extra_context="",
461
- user_message=msg,
462
- forbidden_components=None,
463
- required_components=None,
464
- )
465
- combined_prompt = build_combined_user_prompt(
466
- user_message=msg,
467
- history=user["history"],
468
- )
469
-
470
- adapt_mode = (config.ADAPTIVE_LLM_MODE or config.LLM_MODE).lower()
471
-
472
- # Start NDJSON stream.
473
- self.send_response(200)
474
- self.send_header("Content-Type", "application/x-ndjson; charset=utf-8")
475
- self._cors()
476
- self.end_headers()
477
-
478
- ub = engine.get_user(USERB_ID)
479
-
480
- def send_nd(evt: dict):
481
- try:
482
- line = json.dumps(evt, ensure_ascii=False)
483
- self.wfile.write(line.encode("utf-8") + b"\n")
484
- self.wfile.flush()
485
- except Exception:
486
- pass
487
-
488
- def send_done_error(err: str):
489
- send_nd({
490
- "type": "done",
491
- "strategy": strat,
492
- "elapsed": None,
493
- "llm_mode": adapt_mode,
494
- "response": "",
495
- "widget_html": "",
496
- "widget_schema": "",
497
- "widget_height": 0,
498
- "widget_debug": f"stream_error:{err}",
499
- "error": err,
500
- "force_explore": force_explore,
501
- "scores": {k: round(v, 4) for k, v in scores.items()},
502
- "x_vec": x.tolist(),
503
- "posterior": engine.user_posterior(uid, x),
504
- "global": engine.global_posterior(x),
505
- "userb": engine.posterior_summary(ub["mu"], ub["sigma_inv"], x),
506
- "global_n": engine.global_n,
507
- "prev_strategy": prev,
508
- "explicit": explicit,
509
- "auto_detected": auto_detected,
510
- "auto_r": auto_r,
511
- "auto_reason": ev["reason"],
512
- })
513
-
514
- # Initial strategy event so UI updates immediately.
515
- send_nd({
516
- "type": "strategy",
517
- "strategy": strat,
518
- "instruction": config.STRATEGIES[strat],
519
- "elapsed": None,
520
- "force_explore": force_explore,
521
- "scores": {k: round(v, 4) for k, v in scores.items()},
522
- "x_vec": x.tolist(),
523
- "posterior": engine.user_posterior(uid, x),
524
- "global": engine.global_posterior(x),
525
- "userb": engine.posterior_summary(ub["mu"], ub["sigma_inv"], x),
526
- "global_n": engine.global_n,
527
- "prev_strategy": prev,
528
- "explicit": explicit,
529
- "auto_detected": auto_detected,
530
- "auto_r": auto_r,
531
- "auto_reason": ev["reason"],
532
- })
533
-
534
- try:
535
- if adapt_mode == "openai_compat":
536
- raw_combined, elapsed, mode = llm.call_openai_compat(
537
- combined_prompt,
538
- combined_system,
539
- timeout=combined_timeout,
540
- max_tokens=combined_max_tokens,
541
- temperature=0.2,
542
- )
543
- elif adapt_mode == "anthropic":
544
- # Use non-streaming call with wall-clock timeout (more robust than the SDK stream
545
- # when the provider stalls), then optionally "replay" deltas to the UI.
546
- raw_combined, elapsed, mode = llm.call_anthropic(
547
- combined_prompt,
548
- combined_system,
549
- timeout=combined_timeout,
550
- max_tokens=combined_max_tokens,
551
- temperature=0.2,
552
- )
553
- else:
554
- raise RuntimeError("Unsupported ADAPTIVE_LLM_MODE (expected openai_compat or anthropic)")
555
- except Exception as e:
556
- msg = str(e).strip()
557
- if not msg:
558
- msg = repr(e)
559
- send_done_error(f"LLM error ({type(e).__name__}): {msg}")
560
- return
561
-
562
- response, widget_payload_raw = parse_combined_output(raw_combined)
563
- if not response:
564
- response = raw_combined.strip()
565
- if config.STRICT_PRIMITIVES:
566
- response = enforce_response(strat, response)
567
-
568
- widget_debug = "nonstream"
569
-
570
- widget_html = ""
571
- widget_schema = ""
572
- widget_height = 0
573
- widget_mode = getattr(config, "WIDGET_MODE", "json").strip().lower()
574
- if widget_payload_raw:
575
- if widget_mode == "json":
576
- widget_schema = widget_payload_raw
577
- else:
578
- if _looks_truncated_widget_html(widget_payload_raw):
579
- widget_debug = "stream_widget_truncated"
580
- else:
581
- widget_html = widget_payload_raw
582
- widget_height = estimate_widget_height(widget_payload_raw)
583
- else:
584
- if widget_mode != "json":
585
- placeholder = "<html><head></head><body><div class='widget-root card'><div class='card-title'>Interactive widget</div><div class='empty'>No widget returned; click to request missing data.</div><div class='btn-group'><button class='btn' onclick=\"sendPrompt('Provide a date range for the requested plot.')\">Ask for date range</button></div></div></body></html>"
586
- widget_html = inject_design_system(placeholder)
587
- widget_height = estimate_widget_height(widget_html)
588
- widget_debug = "fallback_widget_generated"
589
-
590
- # Replay deltas so the UI doesn't look frozen (best-effort).
591
- # The final "done" still contains the full response/widget.
592
- try:
593
- if response:
594
- for i in range(0, len(response), 180):
595
- send_nd({"type": "response_delta", "delta": response[i : i + 180]})
596
- payload = widget_schema if widget_mode == "json" else widget_html
597
- if payload:
598
- for i in range(0, len(payload), 900):
599
- send_nd({"type": "widget_delta", "delta": payload[i : i + 900]})
600
- except Exception:
601
- pass
602
-
603
- # Update history as in /api/chat.
604
- user["history"].append({"user": msg, "assistant": response})
605
- user["history"] = user["history"][-20:]
606
- user["last_message"] = msg
607
- user["last_response"] = response
608
- user["last_strategy"] = strat
609
- user["last_x"] = x.tolist()
610
- user["msg_count"] += 1
611
-
612
- send_nd({
613
- "type": "done",
614
- "strategy": strat,
615
- "elapsed": elapsed,
616
- "llm_mode": mode,
617
- "response": response,
618
- "widget_html": widget_html or "",
619
- "widget_schema": widget_schema or "",
620
- "widget_height": widget_height,
621
- "widget_debug": widget_debug,
622
- "force_explore": force_explore,
623
- "scores": {k: round(v, 4) for k, v in scores.items()},
624
- "x_vec": x.tolist(),
625
- "posterior": engine.user_posterior(uid, x),
626
- "global": engine.global_posterior(x),
627
- "userb": engine.posterior_summary(ub["mu"], ub["sigma_inv"], x),
628
- "global_n": engine.global_n,
629
- "prev_strategy": prev,
630
- "explicit": explicit,
631
- "auto_detected": auto_detected,
632
- "auto_r": auto_r,
633
- "auto_reason": ev["reason"],
634
- })
635
- return
636
-
637
- self.send_response(404); self.end_headers()
638
-
639
-
640
- class ThreadedServer(ThreadingMixIn, HTTPServer):
641
- daemon_threads = True
642
-
643
-
644
- def run_server():
645
- print("=" * 60)
646
- print(f" http://localhost:5051 mode: {config.LLM_MODE}")
647
- print("=" * 60)
648
-
649
- PORT = int(os.getenv("PORT", "5051"))
650
- ThreadedServer(("0.0.0.0", PORT), Handler).serve_forever()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
vivek/.env.example ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ---------------------------------------------------------------------------
2
+ # Adaptive Presentation Engine — environment config
3
+ # Copy to `.env` and fill in the values. Never commit real secrets.
4
+ # ---------------------------------------------------------------------------
5
+
6
+ # LLM provider: "anthropic" or "openai_compat"
7
+ LLM_MODE=anthropic
8
+ BASELINE_LLM_MODE=anthropic
9
+ ADAPTIVE_LLM_MODE=anthropic
10
+
11
+ # --- Anthropic (Claude) ---
12
+ ANTHROPIC_API_KEY=REPLACE_ME
13
+ ANTHROPIC_MODEL=claude-sonnet-4-6
14
+ ANTHROPIC_FAST_MODE_ENABLED=1
15
+
16
+ # --- OpenAI-compatible (OpenAI, Groq, etc.) ---
17
+ OPENAI_API_KEY=
18
+ OPENAI_BASE_URL=https://api.openai.com/v1
19
+ OPENAI_MODEL=gpt-4.1
20
+
21
+ # --- Generation budgets ---
22
+ COMBINED_TIMEOUT_SECONDS=45
23
+ COMBINED_MAX_TOKENS=4000
24
+
25
+ # --- Widget output mode: "json" (preferred) or "html" ---
26
+ WIDGET_MODE=json
27
+
28
+ # --- Bandit / format enforcement ---
29
+ STRICT_PRIMITIVES=1
30
+
31
+ # --- Admin bootstrap (required for primitives / strategy management) ---
32
+ ADMIN_USERNAME=admin
33
+ ADMIN_EMAIL=admin@example.com
34
+ ADMIN_PASSWORD=CHANGE_ME_STRONG_PASSWORD
35
+
36
+ # --- Auth ---
37
+ # Generate a strong random value for production (e.g. `python -c "import secrets;print(secrets.token_urlsafe(64))"`).
38
+ JWT_SECRET=change-me-to-a-long-random-secret
39
+ JWT_EXPIRE_SECONDS=604800
40
+
41
+ # --- Runtime env ---
42
+ # "development" exposes password-reset tokens to the caller for local testing.
43
+ # Must be "production" in real deployments.
44
+ ENV=development
{anupa → vivek}/.gitattributes RENAMED
File without changes
{anupa → vivek}/.gitignore RENAMED
@@ -27,3 +27,6 @@ htmlcov/
27
  # OS Specific
28
  .DS_Store
29
  Thumbs.db
 
 
 
 
27
  # OS Specific
28
  .DS_Store
29
  Thumbs.db
30
+
31
+ # adaptiveui.sqlite3 is tracked for GitHub (demo_vivek). Do not push that branch to
32
+ # Hugging Face origin — their hook rejects binary sqlite in git.
{anupa → vivek}/Dockerfile RENAMED
File without changes
{anupa → vivek}/README.md RENAMED
@@ -6,6 +6,27 @@ app_port: 7860
6
 
7
  A web demo of the Contextual Hierarchical Bayesian Architecture pipeline.
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  ## What it shows
10
  - **Live strategy selection** via Thompson Sampling over the Bayesian posterior
11
  - **Posterior updating in real-time** as you rate responses (👍 / 👎)
@@ -18,11 +39,17 @@ A web demo of the Contextual Hierarchical Bayesian Architecture pipeline.
18
 
19
  ### 1. Install Python dependencies
20
  ```bash
21
- pip install numpy
22
- # (flask is not needed the server is built-in)
 
 
 
 
 
 
 
23
  ```
24
 
25
- ### 2. Configure model (optional)
26
  This app supports:
27
  - OpenAI-compatible providers (via `OPENAI_BASE_URL`, `OPENAI_API_KEY`, `OPENAI_MODEL`)
28
  - Anthropic Claude (via `ANTHROPIC_API_KEY`, `ANTHROPIC_MODEL`)
@@ -34,7 +61,7 @@ python app.py
34
 
35
  ### 4. Open in browser
36
  ```
37
- http://localhost:5000
38
  ```
39
 
40
  ---
 
6
 
7
  A web demo of the Contextual Hierarchical Bayesian Architecture pipeline.
8
 
9
+ ## Project structure (modular layout)
10
+
11
+ ```
12
+ vivek/
13
+ ├── frontend/ # UI assets
14
+ │ ├── index.html # Single-page app (chat, widgets, posterior viz)
15
+ │ └── README.md # Frontend documentation
16
+ ├── backend/ # Python API and logic
17
+ │ ├── config.py # Env vars, LLM modes, Thompson Sampling params
18
+ │ ├── server.py # HTTP server, API routes, serves frontend at GET /
19
+ │ ├── llm.py # Anthropic and OpenAI-compatible LLM calls
20
+ │ ├── engine.py # Bayesian engine (Thompson Sampling)
21
+ │ ├── widget_prompt.py
22
+ │ ├── combined_prompt.py
23
+ │ └── primitives.json # Strategy definitions
24
+ ├── app.py # Entry point
25
+ ├── requirements.txt
26
+ ├── .env.example # Template for API keys (copy to .env)
27
+ └── .env # Your keys (gitignored)
28
+ ```
29
+
30
  ## What it shows
31
  - **Live strategy selection** via Thompson Sampling over the Bayesian posterior
32
  - **Posterior updating in real-time** as you rate responses (👍 / 👎)
 
39
 
40
  ### 1. Install Python dependencies
41
  ```bash
42
+ pip install -r requirements.txt
43
+ # (numpy, python-dotenv, anthropic — server is built-in, no Flask)
44
+ ```
45
+
46
+ ### 2. Configure environment
47
+ Copy the example env file and add your API keys:
48
+ ```bash
49
+ cp .env.example .env
50
+ # Edit .env and add your OPENAI_API_KEY and/or ANTHROPIC_API_KEY
51
  ```
52
 
 
53
  This app supports:
54
  - OpenAI-compatible providers (via `OPENAI_BASE_URL`, `OPENAI_API_KEY`, `OPENAI_MODEL`)
55
  - Anthropic Claude (via `ANTHROPIC_API_KEY`, `ANTHROPIC_MODEL`)
 
61
 
62
  ### 4. Open in browser
63
  ```
64
+ http://localhost:5051 # or PORT from env (Docker uses 7860)
65
  ```
66
 
67
  ---
{anupa → vivek}/UpdatedReadme.md RENAMED
@@ -1,4 +1,4 @@
1
- # Anupa: Adaptive Presentation Engine — Complete Reference
2
 
3
  A professional, modular backend demonstrating Bayesian strategy selection and real-time posterior updating. This app learns user preferences and adapts its response format (bullet points, prose, questions, etc.) based on observed rewards.
4
 
@@ -28,7 +28,8 @@ backend/
28
 
29
  Root folder
30
  ├── app.py # Launcher (imports backend.server.run_server)
31
- ├── index.html # Frontend demo UI
 
32
  ├── Dockerfile # Container image
33
  ├── requirements.txt # Python dependencies
34
  ├── README.md # Original readme (kept for reference)
 
1
+ # Vivek: Adaptive Presentation Engine — Complete Reference
2
 
3
  A professional, modular backend demonstrating Bayesian strategy selection and real-time posterior updating. This app learns user preferences and adapts its response format (bullet points, prose, questions, etc.) based on observed rewards.
4
 
 
28
 
29
  Root folder
30
  ├── app.py # Launcher (imports backend.server.run_server)
31
+ ├── frontend/
32
+ │ └── index.html # Frontend demo UI
33
  ├── Dockerfile # Container image
34
  ├── requirements.txt # Python dependencies
35
  ├── README.md # Original readme (kept for reference)
vivek/app.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Entry point for the Adaptive Presentation Engine.
2
+
3
+ Starts the HTTP server that serves the frontend (GET /) and API endpoints
4
+ (/api/chat, /api/chat_plain, /api/health, etc.). Run from the project root (vivek/).
5
+
6
+ Usage:
7
+ pip install -r requirements.txt
8
+ cp .env.example .env # then add your API keys
9
+ python app.py
10
+ open http://localhost:5051 # or PORT from env (default 5051)
11
+
12
+ Project layout:
13
+ vivek/
14
+ ├── frontend/ # UI assets (index.html)
15
+ ├── backend/ # Python API, LLM calls, engine
16
+ └── app.py # this file
17
+ """
18
+
19
+ from backend.server import run_server
20
+
21
+
22
+ if __name__ == "__main__":
23
+ run_server()
{anupa → vivek}/backend/__init__.py RENAMED
File without changes
vivek/backend/auth.py ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import uuid
4
+ from dataclasses import dataclass
5
+ from threading import Lock
6
+ from typing import Any, Dict, Optional
7
+
8
+ import bcrypt
9
+ import jwt
10
+
11
+ from . import config
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class UserRecord:
16
+ user_id: str
17
+ username: str
18
+ email: str
19
+ password_hash: bytes
20
+
21
+
22
+ _lock = Lock()
23
+ _users_by_id: Dict[str, UserRecord] = {}
24
+ _user_id_by_username: Dict[str, str] = {}
25
+ _user_id_by_email: Dict[str, str] = {}
26
+
27
+
28
+ def _normalize_username(s: str) -> str:
29
+ return (s or "").strip().lower()
30
+
31
+
32
+ def _normalize_email(s: str) -> str:
33
+ return (s or "").strip().lower()
34
+
35
+
36
+ def register_user(*, username: str, email: str, password: str) -> UserRecord:
37
+ username_n = _normalize_username(username)
38
+ email_n = _normalize_email(email)
39
+
40
+ if not username_n:
41
+ raise ValueError("username required")
42
+ if not email_n or "@" not in email_n:
43
+ raise ValueError("valid email required")
44
+ if not password or len(password) < 6:
45
+ raise ValueError("password must be at least 6 chars")
46
+
47
+ with _lock:
48
+ if username_n in _user_id_by_username:
49
+ raise ValueError("username already exists")
50
+ if email_n in _user_id_by_email:
51
+ raise ValueError("email already exists")
52
+
53
+ password_hash = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
54
+ user_id = uuid.uuid4().hex
55
+ rec = UserRecord(user_id=user_id, username=username_n, email=email_n, password_hash=password_hash)
56
+
57
+ _users_by_id[user_id] = rec
58
+ _user_id_by_username[username_n] = user_id
59
+ _user_id_by_email[email_n] = user_id
60
+
61
+ # Persist users so bandit state can be recovered across restarts.
62
+ try:
63
+ from . import db as persistence
64
+
65
+ persistence.persist_user(rec)
66
+ except Exception:
67
+ # For local dev, allow auth to work even if DB isn't ready yet.
68
+ pass
69
+
70
+ return rec
71
+
72
+
73
+ def authenticate_login(*, username_or_email: str, password: str) -> Optional[UserRecord]:
74
+ ident = (username_or_email or "").strip()
75
+ if not ident or not password:
76
+ return None
77
+
78
+ username_n = _normalize_username(ident)
79
+ email_n = _normalize_email(ident)
80
+
81
+ with _lock:
82
+ user_id = _user_id_by_email.get(email_n) or _user_id_by_username.get(username_n)
83
+ if not user_id:
84
+ return None
85
+
86
+ rec = _users_by_id.get(user_id)
87
+
88
+ if not rec:
89
+ return None
90
+
91
+ ok = bcrypt.checkpw(password.encode("utf-8"), rec.password_hash)
92
+ return rec if ok else None
93
+
94
+
95
+ def get_user_by_id(user_id: str) -> Optional[UserRecord]:
96
+ if not user_id:
97
+ return None
98
+ with _lock:
99
+ return _users_by_id.get(user_id)
100
+
101
+
102
+ def get_user_by_email(email: str) -> Optional[UserRecord]:
103
+ email_n = _normalize_email(email)
104
+ if not email_n:
105
+ return None
106
+ with _lock:
107
+ user_id = _user_id_by_email.get(email_n)
108
+ return _users_by_id.get(user_id) if user_id else None
109
+
110
+
111
+ def update_password(*, user_id: str, new_password: str) -> bool:
112
+ """
113
+ Update password hash in-memory and persist to SQLite.
114
+ Returns True if updated, False if user not found.
115
+ """
116
+ if not user_id or not new_password or len(new_password) < 6:
117
+ raise ValueError("password must be at least 6 chars")
118
+
119
+ new_hash = bcrypt.hashpw(new_password.encode("utf-8"), bcrypt.gensalt())
120
+ with _lock:
121
+ rec = _users_by_id.get(user_id)
122
+ if not rec:
123
+ return False
124
+
125
+ updated = UserRecord(user_id=rec.user_id, username=rec.username, email=rec.email, password_hash=new_hash)
126
+ _users_by_id[user_id] = updated
127
+
128
+ # Persist outside the lock.
129
+ try:
130
+ from . import db as persistence
131
+
132
+ persistence.persist_user(updated)
133
+ except Exception:
134
+ # Allow dev/test if DB isn't ready.
135
+ pass
136
+
137
+ return True
138
+
139
+
140
+ def seed_users_from_db(rows: list[dict[str, Any]]) -> None:
141
+ """
142
+ Replace the in-memory user store with persisted users from SQLite.
143
+ Intended to be called once during FastAPI startup.
144
+ """
145
+ with _lock:
146
+ _users_by_id.clear()
147
+ _user_id_by_username.clear()
148
+ _user_id_by_email.clear()
149
+
150
+ for r in rows:
151
+ rec = UserRecord(
152
+ user_id=str(r["user_id"]),
153
+ username=str(r["username"]),
154
+ email=str(r["email"]),
155
+ password_hash=r["password_hash"],
156
+ )
157
+ _users_by_id[rec.user_id] = rec
158
+ _user_id_by_username[_normalize_username(rec.username)] = rec.user_id
159
+ _user_id_by_email[_normalize_email(rec.email)] = rec.user_id
160
+
161
+
162
+ def create_access_token(*, user_id: str) -> str:
163
+ import time as _time
164
+ now = int(_time.time())
165
+ exp = now + int(getattr(config, "JWT_EXPIRE_SECONDS", 60 * 60 * 24 * 7))
166
+
167
+ payload = {"sub": user_id, "iat": now, "exp": exp}
168
+ return jwt.encode(payload, config.JWT_SECRET, algorithm=getattr(config, "JWT_ALGORITHM", "HS256"))
169
+
170
+
171
+ def decode_access_token(token: str) -> str:
172
+ if not token:
173
+ raise ValueError("missing token")
174
+ payload = jwt.decode(token, config.JWT_SECRET, algorithms=[getattr(config, "JWT_ALGORITHM", "HS256")])
175
+ sub = payload.get("sub")
176
+ if not sub:
177
+ raise ValueError("invalid token payload")
178
+ return str(sub)
179
+
{anupa → vivek}/backend/combined_prompt.py RENAMED
@@ -17,12 +17,223 @@ This matches Claude's architecture: one model, one generation, no sequential del
17
 
18
  from __future__ import annotations
19
 
 
20
  import re
21
- from typing import Tuple
22
 
23
  from . import config
24
  from .widget_prompt import inject_design_system
25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  _JSON_WIDGET_RULE = """
27
  WIDGET JSON SCHEMA MODE (WIDGET_MODE=json):
28
  - The content inside <WIDGET> MUST be valid JSON (no markdown fences, no comments).
@@ -39,13 +250,21 @@ WIDGET JSON SCHEMA MODE (WIDGET_MODE=json):
39
  { "type": "table", "id": "...", "title": "...", "columns": [ ... ], "rows": [ [ ... ], ... ] }
40
  - action_row:
41
  { "type": "action_row", "id": "...", "buttons": [ { "id": "...", "label": "...", "intent": "..." }, ... ] }
 
 
42
 
43
  Data grounding:
44
- - Every entity/name/number used in the schema must come from the user message OR the numbers you include in <RESPONSE>.
45
- - Do not fabricate time series. If you cannot produce real points, use KPIs + a table.
46
 
47
  Interactivity:
48
  - Use action_row buttons to request follow-ups via intent strings (e.g., "explain_methodology", "show_risks").
 
 
 
 
 
 
 
49
  """
50
 
51
 
@@ -87,50 +306,57 @@ Universal reactive mini-app contract (follow for every widget):
87
  5) sendPrompt: ONLY when new knowledge/data is required. Include current state in the prompt.
88
 
89
  Charts:
90
- - MUST use ECharts as the primary visualization library.
91
- - Allowed CDN (preferred): https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js
92
- - ECharts background must be transparent.
 
93
 
94
  Forbidden (never include):
95
  - fetch / XMLHttpRequest / WebSocket
96
  - eval / new Function
97
  """
98
 
 
 
 
 
 
 
 
 
99
  _OUTPUT_CONTRACT_STRICT = """
100
  OUTPUT CONTRACT (STRICT — MUST FOLLOW)
101
- You MUST return EXACTLY two sections, in this exact order:
102
 
103
  <RESPONSE>
104
  ...text...
105
  </RESPONSE>
106
  <WIDGET>
107
- ...widget...
108
  </WIDGET>
109
 
110
  Rules:
111
- - NEVER omit <WIDGET>.
112
- - NEVER return only text.
113
- - If you are uncertain or missing data, STILL return a valid widget that:
114
- - clearly labels any values as approximate, and
115
- - includes controls + an ECharts chart driven by embedded (approximate) data, and
116
- - includes one or more sendPrompt() buttons asking the user for the missing data (e.g., date range / source).
117
  If you fail to follow this contract, the system will break.
118
  """
119
 
120
  _LIBRARIES_RULE = """
121
- Allowed public libraries (CDN) for <WIDGET>:
122
- - ECharts (BI-grade charts): https://cdnjs.cloudflare.com/ajax/libs/echarts/5.5.0/echarts.min.js
123
- - Plotly.js (high-interactivity charts): https://cdn.plot.ly/plotly-2.30.0.min.js
124
- - D3.js (bespoke/custom visuals): https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js
125
- - Tabulator JS (tables): https://cdn.jsdelivr.net/npm/tabulator-tables@6.2.5/dist/js/tabulator.min.js
126
- - Tabulator CSS: https://cdn.jsdelivr.net/npm/tabulator-tables@6.2.5/dist/css/tabulator.min.css
127
- - Chart.js (only if truly needed): https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js
128
 
129
- Rules:
130
- - Do NOT use any other external libraries.
131
- - Choose exactly ONE chart engine for the main visualization: ECharts OR Plotly OR D3.
132
- - You may use Tabulator concurrently for detail tables.
133
- - Never mix multiple chart engines for the same chart area.
 
 
 
 
 
 
134
 
135
  Color + theming baseline (applies to every engine):
136
  - Always define a JS palette (array of hex colors) and apply it explicitly to series/marks.
@@ -148,7 +374,7 @@ Library choice guidance (use the best fit; do not force the same layout every ti
148
  - Hierarchies: treemap only when parent/child is explicit; otherwise use grouped table.
149
  - Many series: avoid clutter; use small multiples or series toggles (do not plot >6 lines by default).
150
  - Tables: Tabulator always for scan/sort/filter when it helps (rows > 8 or user asked for a breakdown).
151
- - Prose/conceptual answers with no extractable dataset: return <WIDGET></WIDGET> or a compact visual card (no chart/table shells).
152
 
153
  Engine-specific rendering requirements:
154
  - ECharts: option.backgroundColor must be 'transparent'; set textStyle/axis/grid colors from theme.
@@ -159,14 +385,13 @@ Engine-specific rendering requirements:
159
  _ANALYTICS_DEFAULTS_RULE = """
160
  Dashboard decision policy (data-driven; do this internally—do not output the reasoning):
161
  1) DATASET EXTRACTION:
162
- - Extract ONE canonical dataset in JS: `const data = [...]` derived from extractable numeric anchors from this conversation:
163
- (a) numeric values explicitly present in the user request/context you were given, and
164
- (b) any numeric values you include inside <RESPONSE>.
165
- - Every KPI/table/chart label and every numeric value used in the widget must be present somewhere in this conversation context (either in the user request/context or in <RESPONSE>).
166
  2) DATA-SHAPE DETECTION:
167
  - Determine shape: time-series, categorical ranking, composition, distribution, correlation, hierarchy, steps/process, or other.
168
  3) WIDGET WARRANT:
169
- - If no extractable dataset (or too few points): return <WIDGET></WIDGET> or a compact single-card visual (no chart/table).
 
170
  4) BI LAYOUT (only when warranted):
171
  - KPI row (3–6 tiles) → optional Controls row → Primary visualization → optional detail table → Insights (2–4).
172
  5) CROSS-VIEW INTERACTION:
@@ -196,12 +421,87 @@ Color & theming (best-in-class readability + polish):
196
  - Grid/labels must always be visible: explicitly set label/text/grid colors for the chart engine.
197
  """
198
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
 
200
  def build_combined_system_prompt(
201
  strategy_id: str,
202
  format_rule: str,
203
  primitive_extra_context: str,
204
  user_message: str,
 
205
  forbidden_components: list[str] | None = None,
206
  required_components: list[str] | None = None,
207
  ) -> str:
@@ -239,40 +539,27 @@ def build_combined_system_prompt(
239
  "If your HTML is missing any of these, the widget will be rejected and replaced.\n"
240
  )
241
 
242
- response_rule_line = (
243
- "Follow this exactly for the text inside <RESPONSE>."
244
- if getattr(config, "STRICT_PRIMITIVES", False)
245
- else "Treat this as a style hint for <RESPONSE> (do not be rigid)."
246
- )
247
-
248
- um = (user_message or "").lower()
249
- explicit_visual = any(
250
- k in um
251
- for k in (
252
- "plot",
253
- "chart",
254
- "graph",
255
- "visualize",
256
- "visualisation",
257
- "visualization",
258
- "dashboard",
259
- "scatter",
260
- "line chart",
261
- "bar chart",
262
- "histogram",
263
- "heatmap",
264
- "candlestick",
265
  )
266
- )
267
- visual_override_block = ""
268
- if explicit_visual:
269
- visual_override_block = """
 
 
 
 
270
  ═══════════════════════════════════════════════════════
271
- EXPLICIT VISUAL OVERRIDE (from user message)
 
 
 
 
272
  ═══════════════════════════════════════════════════════
273
- The user explicitly asked for a visualization. Prioritize a chart/graph in <WIDGET>.
274
- If the Strategy/Rule suggests a table-only response, you may still answer in a chart-friendly structure and should include an interactive chart in <WIDGET>.
275
- Do not refuse a chart just because a "comparison_table" style was selected.
276
  """
277
 
278
  widget_mode = getattr(config, "WIDGET_MODE", "json").strip().lower()
@@ -288,15 +575,22 @@ Do not refuse a chart just because a "comparison_table" style was selected.
288
  else "WIDGET RULES — for the JSON schema inside <WIDGET>"
289
  )
290
 
 
 
 
 
 
 
291
  widget_rules_body = (
292
  f"""- Hard output contract (never violate):
293
  - You MUST output BOTH tags exactly once: <RESPONSE>...</RESPONSE> and <WIDGET>...</WIDGET>.
294
- - Never omit <WIDGET> tags. If you choose “no widget”, output literally `<WIDGET></WIDGET>` (empty but present).
295
- - Widget CONTENT is OPTIONAL (based on data), but the <WIDGET> tags are REQUIRED.
296
  - Choose the UI based on the content in <RESPONSE> (data-driven). Do not follow any fixed template.
297
  - IMPORTANT: In HTML mode, the content inside <WIDGET> MUST be HTML (not JSON). It must contain opening <html> and closing </html>.
298
  - Return a COMPLETE, self-contained HTML document (opening <html> to closing </html>).
299
- - Inline ALL CSS in <style> and ALL JS in <script>. The only external files allowed are the CDNs below (and Tabulator CSS via <link>).
 
300
  - {_LIBRARIES_RULE.strip()}
301
  - {_ANALYTICS_DEFAULTS_RULE.strip()}
302
  - {_COLOR_THEMING_RULE.strip()}
@@ -307,10 +601,13 @@ Do not refuse a chart just because a "comparison_table" style was selected.
307
  - No markdown fences/backticks inside <WIDGET>. Use ONLY raw HTML/CSS/JS.
308
  - Always call your main render/calc function once on page load so output is never empty (e.g., call `init()` or `render()` at the end of <script>).
309
  - Charts/tables must be drawn from the embedded dataset immediately after the first render call.
 
310
  - Slider/input changes → local calc() only (never sendPrompt on drag).
311
  - Slider initial values MUST match the exact numbers in your <RESPONSE>. Never invent defaults.
312
  - Use 0.5px solid borders — never 1px solid.
313
  - UI (HTML/CSS): use CSS variables only (no hardcoded hex/rgb). Charts (ECharts/Plotly/Chart.js): you MAY use hex colors in JS configs for palettes/series.
 
 
314
  {_REACTIVE_RUNTIME_RULE}
315
  {_DESIGN_SYSTEM_REMINDER}
316
  {_SENDPROMPT_RULE}"""
@@ -318,33 +615,57 @@ Do not refuse a chart just because a "comparison_table" style was selected.
318
  else _JSON_WIDGET_RULE.strip()
319
  )
320
 
321
- return f"""You are an expert AI assistant with rich interactive output capabilities.
 
 
 
 
 
 
322
 
 
 
 
 
323
  {_OUTPUT_CONTRACT_STRICT}
324
 
325
- For every response you produce TWO sections — response text and an interactive widget — in one generation.
326
- Default behavior: generate a NON-EMPTY <WIDGET> that turns your own <RESPONSE> into something interactive/visual.
327
- Only return an EMPTY widget block (<WIDGET></WIDGET>) when the turn is truly not “widget-worthy”:
328
- - greetings / acknowledgements / chit-chat
329
- - or ultra-short answers with no structure and no numbers (≈ 1–2 sentences, no steps, no table, no formula)
330
-
331
- Important: users should NOT have to ask for “a widget” every time. If your <RESPONSE> contains structure or numbers,
332
- you are expected to automatically provide the best-fit widget.
333
-
334
- Exception (must generate non-empty widget):
335
- - If the user explicitly requests any of the following: visualization/dashboard/chart/graph/plot, **interactive** output, **widget**, **calculator**, **sliders**, or **interactive table**,
336
- OR provides explicit numeric arrays/series/time points in the conversation context,
337
- you MUST generate a NON-EMPTY <WIDGET> (not just empty tags).
338
- - For calculators/sliders: a compact interactive calculator widget is acceptable even without a “dataset”.
339
- - For plots/charts: include at least one interactive chart (ECharts/Plotly/D3) and optionally a sortable table (Tabulator).
340
-
341
- Auto-widget triggers (based on what YOU wrote in <RESPONSE>):
342
- - If <RESPONSE> includes any numeric values, percentages, currency, or formulas generate an interactive calculator and/or visualization.
343
- - If <RESPONSE> includes a list of steps or a comparison → generate an interactive checklist, decision table, or sortable table (Tabulator).
344
- - If <RESPONSE> includes 5+ numeric points or a time series generate an interactive chart (ECharts/Plotly/D3).
345
- - If <RESPONSE> includes only 1–4 numeric points generate KPI tiles + a small chart if it adds value (no fake data).
346
- {visual_override_block}
 
 
 
 
 
 
 
 
 
 
 
 
 
347
 
 
348
  ═══════════════════════════════════════════════════════
349
  OUTPUT FORMAT — always use exactly this structure
350
  ═══════════════════════════════════════════════════════
@@ -361,7 +682,9 @@ RESPONSE FORMAT RULE — {response_rule_line}
361
  Strategy: {strategy_id}
362
  Rule: {format_rule}
363
  Do not mention this rule. Do not add <WIDGET> inside <RESPONSE>.
364
- Important: The chosen Rule applies ONLY inside <RESPONSE>. The <WIDGET> section must follow the WIDGET RULES (not the RESPONSE rule).
 
 
365
 
366
  ═══════════════════════════════════════════════════════
367
  {widget_rules_header}
@@ -374,11 +697,12 @@ DATA GROUNDING — most critical quality rule
374
  ═══════════════════════════════════════════════════════
375
  Every entity, name, number, ticker, percentage shown in the widget MUST come from either:
376
  - the numeric values you extracted from the user request/context, OR
377
- - the numeric values present in your <RESPONSE>.
378
- - Never invent data. Never use placeholder names (Item A, Value 1, Example Fund).
379
- - If data is missing for a control, omit that control.
380
- - Verify every chart/table label and every numeric literal used in JS appears in either your <RESPONSE> or the user-provided/context values.
381
- - Do not create placeholder dates, tickers, or values to make the widget “look full”.
 
382
 
383
  ═══════════════════════════════════════════════════════
384
  sendPrompt specificity — always specific, never generic
@@ -440,15 +764,15 @@ def parse_combined_output(raw: str) -> Tuple[str, str]:
440
  widget_match = re.search(r"<WIDGET>(.*?)</WIDGET>", raw, re.DOTALL | re.IGNORECASE)
441
  if widget_match:
442
  raw_widget = widget_match.group(1).strip()
443
- # Strip markdown fences if model wrapped in ```...```
444
- if "```" in raw_widget:
445
- fence = re.search(r"```(?:json|html)?\s*(.*?)```", raw_widget, re.DOTALL | re.IGNORECASE)
446
- raw_widget = fence.group(1).strip() if fence else re.sub(r"```\w*", "", raw_widget).strip()
447
 
448
  widget_mode = getattr(config, "WIDGET_MODE", "json").strip().lower()
449
  if widget_mode == "json":
450
- widget_payload = raw_widget
451
  else:
 
 
 
 
452
  if "<" in raw_widget and ">" in raw_widget:
453
  if "<html" not in raw_widget.lower():
454
  raw_widget = f"<html><head></head><body>{raw_widget}</body></html>"
 
17
 
18
  from __future__ import annotations
19
 
20
+ import json
21
  import re
22
+ from typing import Any, Tuple
23
 
24
  from . import config
25
  from .widget_prompt import inject_design_system
26
 
27
+
28
+ def strip_widget_markdown_fences(raw: str) -> str:
29
+ """Remove ```json ... ``` wrappers often emitted inside <WIDGET> despite instructions."""
30
+ s = (raw or "").strip()
31
+ if not s or "```" not in s:
32
+ return s
33
+ fence = re.search(r"```(?:json|html|javascript|js)?\s*(.*?)```", s, re.DOTALL | re.IGNORECASE)
34
+ if fence:
35
+ return fence.group(1).strip()
36
+ return re.sub(r"```\w*", "", s).strip()
37
+
38
+
39
+ def parse_widget_schema_object(s: str) -> Any | None:
40
+ """Parse first JSON value; tolerate leading/trailing prose via JSONDecoder.raw_decode."""
41
+ s = strip_widget_markdown_fences(s).strip()
42
+ if not s:
43
+ return None
44
+ dec = json.JSONDecoder()
45
+ for start_ch in ("{", "["):
46
+ idx = s.find(start_ch)
47
+ if idx == -1:
48
+ continue
49
+ try:
50
+ obj, _ = dec.raw_decode(s, idx)
51
+ return obj
52
+ except json.JSONDecodeError:
53
+ continue
54
+ try:
55
+ return json.loads(s)
56
+ except json.JSONDecodeError:
57
+ return None
58
+
59
+
60
+ _BLOCK_TYPES = frozenset({"text", "kpi_row", "chart", "table", "action_row", "image"})
61
+
62
+ _TYPE_ALIASES: dict[str, str] = {
63
+ "markdown": "text",
64
+ "md": "text",
65
+ "paragraph": "text",
66
+ "rich_text": "text",
67
+ "kpi": "kpi_row",
68
+ "kpis": "kpi_row",
69
+ "kpirow": "kpi_row",
70
+ "metrics": "kpi_row",
71
+ "metric_row": "kpi_row",
72
+ "data_table": "table",
73
+ "grid": "table",
74
+ "actions": "action_row",
75
+ "action": "action_row",
76
+ "plot": "chart",
77
+ "graph": "chart",
78
+ "line_chart": "chart",
79
+ "bar_chart": "chart",
80
+ "photo": "image",
81
+ "picture": "image",
82
+ "img": "image",
83
+ "illustration": "image",
84
+ }
85
+
86
+
87
+ def _normalize_layout_block(entry: Any) -> dict[str, Any]:
88
+ """Turn primitives, malformed entries, and common aliases into valid block dicts."""
89
+ if entry is None:
90
+ return {"type": "text", "content": ""}
91
+ if isinstance(entry, (str, int, float, bool)):
92
+ return {"type": "text", "content": str(entry)}
93
+ if isinstance(entry, list):
94
+ return {"type": "text", "content": json.dumps(entry, ensure_ascii=False)}
95
+ if not isinstance(entry, dict):
96
+ return {"type": "text", "content": str(entry)}
97
+
98
+ b: dict[str, Any] = dict(entry)
99
+ raw_t = str(b.get("type") or "").strip().lower().replace("-", "_")
100
+ t = _TYPE_ALIASES.get(raw_t, raw_t)
101
+ if t in _BLOCK_TYPES:
102
+ b["type"] = t
103
+ return b
104
+
105
+ items = b.get("items")
106
+ if isinstance(items, list) and items:
107
+ first = items[0]
108
+ if isinstance(first, dict) and isinstance(first.get("label"), str):
109
+ v = first.get("value")
110
+ if isinstance(v, (str, int, float, bool)):
111
+ b["type"] = "kpi_row"
112
+ return b
113
+ if isinstance(b.get("chart"), dict):
114
+ b["type"] = "chart"
115
+ return b
116
+ src_val = b.get("src")
117
+ if isinstance(src_val, str) and src_val.strip():
118
+ b["type"] = "image"
119
+ return b
120
+ if isinstance(b.get("rows"), list):
121
+ b["type"] = "table"
122
+ return b
123
+ if isinstance(b.get("buttons"), list):
124
+ b["type"] = "action_row"
125
+ return b
126
+ for key in ("content", "body", "text", "markdown", "md"):
127
+ v = b.get(key)
128
+ if isinstance(v, str):
129
+ return {"type": "text", "content": v}
130
+
131
+ return {"type": "text", "content": json.dumps(b, ensure_ascii=False)}
132
+
133
+
134
+ def _sanitize_layout(layout: list[Any]) -> list[dict[str, Any]]:
135
+ return [_normalize_layout_block(e) for e in layout]
136
+
137
+
138
+ def coerce_widget_schema_root(obj: Any) -> dict[str, Any] | None:
139
+ """Ensure root has layout: [] (alias blocks/components; wrap bare arrays)."""
140
+ if obj is None:
141
+ return None
142
+ if isinstance(obj, list):
143
+ return {"version": "1.0", "layout": obj}
144
+ if not isinstance(obj, dict):
145
+ return None
146
+ out = dict(obj)
147
+ # Unwrap { "widget": { "layout": [...] } } or { "schema": {...} }
148
+ if isinstance(out.get("widget"), dict):
149
+ inner = dict(out.pop("widget"))
150
+ out = {**out, **inner}
151
+ if isinstance(out.get("schema"), dict):
152
+ inner = dict(out.pop("schema"))
153
+ out = {**out, **inner}
154
+ if "layout" not in out or not isinstance(out.get("layout"), list):
155
+ if isinstance(out.get("blocks"), list):
156
+ out["layout"] = out["blocks"]
157
+ elif isinstance(out.get("components"), list):
158
+ out["layout"] = out["components"]
159
+ elif isinstance(out.get("Layout"), list):
160
+ out["layout"] = out.pop("Layout")
161
+ # layout is a single block object (common model mistake)
162
+ lay = out.get("layout")
163
+ if isinstance(lay, dict) and lay.get("type"):
164
+ out["layout"] = [lay]
165
+ # Root is one block with no layout key
166
+ if not isinstance(out.get("layout"), list) and out.get("type") in _BLOCK_TYPES:
167
+ ver = out.pop("version", None)
168
+ block = dict(out)
169
+ out = {"version": str(ver or "1.0"), "layout": [block]}
170
+ if isinstance(out.get("layout"), list):
171
+ out["layout"] = _sanitize_layout(out["layout"])
172
+ return out
173
+
174
+
175
+ def widget_schema_json_is_valid(schema_str: str) -> bool:
176
+ if not (schema_str or "").strip():
177
+ return False
178
+ try:
179
+ o = json.loads(schema_str)
180
+ except json.JSONDecodeError:
181
+ return False
182
+ return isinstance(o, dict) and isinstance(o.get("layout"), list)
183
+
184
+
185
+ def extract_embeddable_html_document(raw: str) -> str | None:
186
+ """
187
+ If the model put HTML/JS widgets inside <WIDGET> while WIDGET_MODE=json, return a full HTML
188
+ document suitable for inject_design_system + iframe. Otherwise None.
189
+ """
190
+ s = strip_widget_markdown_fences((raw or "").strip())
191
+ if not s or "<" not in s or ">" not in s:
192
+ return None
193
+ st = s.lstrip()
194
+ if st.startswith("{") and "<div" not in s.lower() and "<html" not in s.lower():
195
+ return None
196
+ low = s.lower()
197
+ if "<html" in low or "<!doctype" in low:
198
+ return re.sub(r"<!DOCTYPE[^>]*>", "", s, flags=re.IGNORECASE).strip()
199
+ if any(
200
+ tag in low
201
+ for tag in (
202
+ "<script",
203
+ "<body",
204
+ "<div",
205
+ "<canvas",
206
+ "<iframe",
207
+ "<form",
208
+ "<input",
209
+ "<button",
210
+ "<style",
211
+ )
212
+ ):
213
+ inner = re.sub(r"<!DOCTYPE[^>]*>", "", s, flags=re.IGNORECASE).strip()
214
+ if "<html" not in inner.lower():
215
+ return f"<html><head></head><body>{inner}</body></html>"
216
+ return inner
217
+ return None
218
+
219
+
220
+ def finalize_widget_schema_json(raw: str) -> str:
221
+ """
222
+ Normalize model output for the Vue renderer: strip fences, extract JSON, coerce layout.
223
+ Returns a string safe for JSON.parse on the client (or best-effort stripped text).
224
+ """
225
+ raw = (raw or "").strip()
226
+ if not raw:
227
+ return ""
228
+ obj = parse_widget_schema_object(raw)
229
+ if obj is None:
230
+ return strip_widget_markdown_fences(raw)
231
+ coerced = coerce_widget_schema_root(obj)
232
+ if coerced is None:
233
+ return json.dumps(obj, ensure_ascii=False)
234
+ return json.dumps(coerced, ensure_ascii=False)
235
+
236
+
237
  _JSON_WIDGET_RULE = """
238
  WIDGET JSON SCHEMA MODE (WIDGET_MODE=json):
239
  - The content inside <WIDGET> MUST be valid JSON (no markdown fences, no comments).
 
250
  { "type": "table", "id": "...", "title": "...", "columns": [ ... ], "rows": [ [ ... ], ... ] }
251
  - action_row:
252
  { "type": "action_row", "id": "...", "buttons": [ { "id": "...", "label": "...", "intent": "..." }, ... ] }
253
+ - image (photos, diagrams, icons, illustrations — any raster or SVG via URL/data URI):
254
+ { "type": "image", "id": "...", "title": "...", "src": "https://... OR data:image/png;base64,...", "alt": "accessible description", "caption": "optional caption", "fit": "contain|cover" }
255
 
256
  Data grounding:
257
+ - Prefer data from user message or <RESPONSE>. When real data is unavailable, use illustrative/mock data and label it clearly (e.g. "Example data", "Mock data").
 
258
 
259
  Interactivity:
260
  - Use action_row buttons to request follow-ups via intent strings (e.g., "explain_methodology", "show_risks").
261
+ - GAMES, TOYS, and CUSTOM APPS (tic-tac-toe, puzzles, interactive demos, any playable UI): you MUST output a **complete HTML document** (`<html>...</html>` with CSS/JS) inside `<WIDGET>`, NOT a JSON schema. JSON blocks cannot represent a real game board — never dump raw index arrays like `[0,1,2]` as layout items.
262
+ - If the user needs true controls (sliders, inputs, live calculator), rich HTML/SVG/canvas, or complex layouts that JSON blocks cannot express, you MAY put a complete mini HTML document (with inline JS) inside <WIDGET> instead of JSON — the app will still render it. Prefer JSON when charts/KPIs/tables/images suffice.
263
+ - For photographs, diagrams, or icons in JSON mode, use the `image` block with a valid https:// URL or a data: URI. Combine `image` with `text`, `chart`, and `table` blocks as needed.
264
+
265
+ Dynamic layout (JSON) — avoid static, single-block dashboards:
266
+ - Shape `layout` like a short story: context first, then metrics, then detail, then actions. Mix block types (text, kpi_row, chart, table, image, action_row) whenever it improves scanning; do not default to one lonely chart if KPIs or a sentence of framing would help.
267
+ - Use `action_row` for obvious follow-up intents; keep blocks ordered top-to-bottom by importance so the widget feels purposeful, not generic.
268
  """
269
 
270
 
 
306
  5) sendPrompt: ONLY when new knowledge/data is required. Include current state in the prompt.
307
 
308
  Charts:
309
+ - Use any public chart/library CDN from cdnjs.cloudflare.com, cdn.jsdelivr.net, unpkg.com, or cdn.plot.ly.
310
+ - Chart backgrounds must be transparent for iframe embedding.
311
+ - ECharts (preferred): https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js
312
+ - Plotly, D3, Chart.js, ApexCharts, and other public viz libraries are allowed.
313
 
314
  Forbidden (never include):
315
  - fetch / XMLHttpRequest / WebSocket
316
  - eval / new Function
317
  """
318
 
319
+ _DYNAMIC_WIDGET_UX_RULE = """
320
+ Dynamic, responsive widgets (not static posters):
321
+ - Layout: use flex/grid with wrap, minmax(), and clamp() so content reflows when the iframe is narrow or wide. Prefer fluid widths (%, fr, max-width) over fixed pixel widths for main columns.
322
+ - Motion & feedback: add CSS transitions on hover/focus for cards, buttons, and controls; subtle transform (translateY) on hover where it aids affordance. Enable chart library animation (e.g. ECharts animation / animationDuration) so series draw in smoothly.
323
+ - Interactivity: expose meaningful controls — tabs, toggles, filters, sliders, dataZoom/brush on charts when data density warrants it. When state changes, re-render charts/tables immediately (same reactive pattern as sliders).
324
+ - Depth: combine visuals (chart + KPI strip + short table + optional image/diagram) when the answer benefits; vary structure by use case instead of repeating one template every turn.
325
+ """
326
+
327
  _OUTPUT_CONTRACT_STRICT = """
328
  OUTPUT CONTRACT (STRICT — MUST FOLLOW)
329
+ You MUST return these sections in this exact order:
330
 
331
  <RESPONSE>
332
  ...text...
333
  </RESPONSE>
334
  <WIDGET>
335
+ ...widget OR empty...
336
  </WIDGET>
337
 
338
  Rules:
339
+ - Never omit the <WIDGET> tags, but content may be empty when a widget is not warranted.
340
+ - If widget is warranted, return a valid interactive widget (not placeholders).
341
+ - If widget is not warranted (simple chit-chat / conceptual text-only), return exactly <WIDGET></WIDGET>.
 
 
 
342
  If you fail to follow this contract, the system will break.
343
  """
344
 
345
  _LIBRARIES_RULE = """
346
+ Allowed libraries for <WIDGET> — use any public CDN from cdnjs.cloudflare.com, cdn.jsdelivr.net, unpkg.com, or cdn.plot.ly.
347
+ Use whichever library produces the best visual for the use case. You may combine libraries (e.g. ECharts + Tabulator).
 
 
 
 
 
348
 
349
+ Recommended (pick the best fit):
350
+ - ECharts: https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js
351
+ - Plotly.js: https://cdn.plot.ly/plotly-2.30.0.min.js
352
+ - D3.js: https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js
353
+ - Chart.js: https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js
354
+ - ApexCharts: https://cdn.jsdelivr.net/npm/apexcharts
355
+ - Tabulator (tables): https://cdn.jsdelivr.net/npm/tabulator-tables/dist/js/tabulator.min.js + CSS
356
+ - Mermaid (flowcharts, sequence diagrams): https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js
357
+ - Three.js (simple 3D): https://cdn.jsdelivr.net/npm/three/build/three.min.js
358
+
359
+ You may use any other public library from these CDNs. Choose the library that creates the best, most accurate visualization.
360
 
361
  Color + theming baseline (applies to every engine):
362
  - Always define a JS palette (array of hex colors) and apply it explicitly to series/marks.
 
374
  - Hierarchies: treemap only when parent/child is explicit; otherwise use grouped table.
375
  - Many series: avoid clutter; use small multiples or series toggles (do not plot >6 lines by default).
376
  - Tables: Tabulator always for scan/sort/filter when it helps (rows > 8 or user asked for a breakdown).
377
+ - Prose/conceptual answers with no extractable dataset: still generate a widget using illustrative/mock data and label it clearly (e.g. "Example", "Illustrative", "Mock data").
378
 
379
  Engine-specific rendering requirements:
380
  - ECharts: option.backgroundColor must be 'transparent'; set textStyle/axis/grid colors from theme.
 
385
  _ANALYTICS_DEFAULTS_RULE = """
386
  Dashboard decision policy (data-driven; do this internally—do not output the reasoning):
387
  1) DATASET EXTRACTION:
388
+ - Prefer data from: (a) user request/context, (b) numeric values in <RESPONSE>.
389
+ - If sufficient data exists, use it. If not, use illustrative/mock data — but you MUST clearly label it (e.g. "Example data", "Mock data", "Illustrative") in the widget title or a visible subtitle.
 
 
390
  2) DATA-SHAPE DETECTION:
391
  - Determine shape: time-series, categorical ranking, composition, distribution, correlation, hierarchy, steps/process, or other.
392
  3) WIDGET WARRANT:
393
+ - Generate a widget wherever there is possibility. If extractable data exists, use it.
394
+ - If no extractable dataset (or too few points): use illustrative/mock data and clearly label it (e.g. "Example data", "Mock data", "Illustrative"). Do NOT return empty <WIDGET></WIDGET> when a chart/calculator/table would help.
395
  4) BI LAYOUT (only when warranted):
396
  - KPI row (3–6 tiles) → optional Controls row → Primary visualization → optional detail table → Insights (2–4).
397
  5) CROSS-VIEW INTERACTION:
 
421
  - Grid/labels must always be visible: explicitly set label/text/grid colors for the chart engine.
422
  """
423
 
424
+ # Strict bandit primitive: extra lines for <RESPONSE> when STRICT_PRIMITIVES is on (prompt-only).
425
+ _STRICT_PRIMITIVE_EXTRAS: dict[str, str] = {
426
+ "structured_bullets": (
427
+ "Use only the format in the Rule: 3–5 lines, each starting with '- '. "
428
+ "Do not use numbered lists or paragraph prose as the main answer."
429
+ ),
430
+ "narrative_prose": (
431
+ "Use only short paragraphs (no '- ' bullets, no numbered list as the main answer)."
432
+ ),
433
+ "concise_direct": (
434
+ "Obey the sentence limit in the Rule. No bullet or numbered lists unless the Rule allows."
435
+ ),
436
+ "socratic_questions": (
437
+ "Match the Rule: brief acknowledgement, then 1–2 questions only — not a full tutorial."
438
+ ),
439
+ "step_by_step": (
440
+ "Use only a numbered list (3–6 steps). Do not use '-' bullets for the main steps."
441
+ ),
442
+ "comparison_table": (
443
+ "Output ONLY a GitHub-flavored markdown table: one header row, a |---| separator row, then data rows. "
444
+ "No section titles, bullets, or paragraphs outside the table. Do not put JSON in <RESPONSE>."
445
+ ),
446
+ "visualization": (
447
+ "Output ONLY one markdown fenced code block (triple backticks). Inside: ASCII bar chart (#) and/or "
448
+ "aligned text columns. Do not put raw JSON in <RESPONSE>. No prose outside that single code block."
449
+ ),
450
+ }
451
+
452
+
453
+ def build_strict_response_rule_line(strategy_id: str) -> str:
454
+ """Instructions so <RESPONSE> matches the selected bandit strategy (strict primitive)."""
455
+ base = (
456
+ "MANDATORY for <RESPONSE> — follow the Strategy and Rule below exactly. "
457
+ "Do not substitute a different format; the UI label must match what you write."
458
+ )
459
+ extra = _STRICT_PRIMITIVE_EXTRAS.get(strategy_id, "").strip()
460
+ if extra:
461
+ return f"{base} {extra}"
462
+ return base
463
+
464
+
465
+ def is_social_or_greeting_turn(user_message: str) -> bool:
466
+ """Short greeting/thanks/goodbye only — no bandit strategy format (used for prompt routing only)."""
467
+ t = (user_message or "").strip()
468
+ if not t or len(t) > 160:
469
+ return False
470
+ tl = " ".join(t.lower().split())
471
+ if "?" in t and len(t) > 25:
472
+ return False
473
+ if re.fullmatch(
474
+ r"(hi|hello|hey|yo|sup|hiya|bye|goodbye|ok|okay|k|cheers|thx|ty|thanks|thank you)"
475
+ r"([!?.])*",
476
+ tl,
477
+ ):
478
+ return True
479
+ if re.fullmatch(
480
+ r"(thanks|thank you)( a lot| so much| again)?([!?.])*",
481
+ tl,
482
+ ):
483
+ return True
484
+ if re.fullmatch(
485
+ r"(good )?(morning|afternoon|evening|night)([!?.])*",
486
+ tl,
487
+ ):
488
+ return True
489
+ if re.fullmatch(r"got it([!?.])*", tl):
490
+ return True
491
+ if re.fullmatch(
492
+ r"(hi|hello|hey)\s+(there|everyone|all|team)([!?.])*",
493
+ tl,
494
+ ):
495
+ return True
496
+ return False
497
+
498
 
499
  def build_combined_system_prompt(
500
  strategy_id: str,
501
  format_rule: str,
502
  primitive_extra_context: str,
503
  user_message: str,
504
+ widget_required: bool = True,
505
  forbidden_components: list[str] | None = None,
506
  required_components: list[str] | None = None,
507
  ) -> str:
 
539
  "If your HTML is missing any of these, the widget will be rejected and replaced.\n"
540
  )
541
 
542
+ social_only = is_social_or_greeting_turn(user_message)
543
+ if social_only:
544
+ response_rule_line = (
545
+ "GENERAL / SOCIAL TURN ignore Strategy and Rule below. "
546
+ "Reply in 1–2 short, natural sentences only. No bullets, tables, numbered lists, or fenced code blocks."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
547
  )
548
+ elif getattr(config, "STRICT_PRIMITIVES", False):
549
+ response_rule_line = build_strict_response_rule_line(strategy_id)
550
+ else:
551
+ response_rule_line = "Treat this as a style hint for <RESPONSE> (do not be rigid)."
552
+
553
+ social_turn_banner = ""
554
+ if social_only:
555
+ social_turn_banner = """
556
  ═══════════════════════════════════════════════════════
557
+ GENERAL QUESTION GREETING / THANKS / GOODBYE (no bandit strategy)
558
+ ═══════════════════════════════════════════════════════
559
+ The user's message is only a greeting, thanks, acknowledgement, or goodbye.
560
+ - <RESPONSE>: Brief, friendly, natural text. Do NOT apply the bandit Strategy or Rule shown below.
561
+ - <WIDGET>: Return exactly <WIDGET></WIDGET> (empty). No chart, no dashboard, no placeholder HTML.
562
  ═══════════════════════════════════════════════════════
 
 
 
563
  """
564
 
565
  widget_mode = getattr(config, "WIDGET_MODE", "json").strip().lower()
 
575
  else "WIDGET RULES — for the JSON schema inside <WIDGET>"
576
  )
577
 
578
+ widget_requirement_block = (
579
+ "WIDGET REQUIRED for this user turn: return a NON-EMPTY widget."
580
+ if widget_required
581
+ else "WIDGET OPTIONAL for this user turn: return <WIDGET></WIDGET> if the turn is better as text-only."
582
+ )
583
+
584
  widget_rules_body = (
585
  f"""- Hard output contract (never violate):
586
  - You MUST output BOTH tags exactly once: <RESPONSE>...</RESPONSE> and <WIDGET>...</WIDGET>.
587
+ - Never omit <WIDGET> tags. For text-only turns, output `<WIDGET></WIDGET>`.
588
+ - {widget_requirement_block}
589
  - Choose the UI based on the content in <RESPONSE> (data-driven). Do not follow any fixed template.
590
  - IMPORTANT: In HTML mode, the content inside <WIDGET> MUST be HTML (not JSON). It must contain opening <html> and closing </html>.
591
  - Return a COMPLETE, self-contained HTML document (opening <html> to closing </html>).
592
+ - Inline ALL CSS in <style> and ALL JS in <script>. External files: use any public CDN (cdnjs, jsdelivr, unpkg, cdn.plot.ly) for charts, tables, and other libraries.
593
+ - Visuals — use freely when they clarify the answer: <img> (https:// or data:image/...), inline <svg>, <figure>/<figcaption>, <picture>, <canvas> for drawings, and background-image in CSS (url() to https or data URIs). For diagrams/flowcharts you may embed SVG markup or use canvas/D3/Mermaid via CDN. Attribute image sources when the license requires it.
594
  - {_LIBRARIES_RULE.strip()}
595
  - {_ANALYTICS_DEFAULTS_RULE.strip()}
596
  - {_COLOR_THEMING_RULE.strip()}
 
601
  - No markdown fences/backticks inside <WIDGET>. Use ONLY raw HTML/CSS/JS.
602
  - Always call your main render/calc function once on page load so output is never empty (e.g., call `init()` or `render()` at the end of <script>).
603
  - Charts/tables must be drawn from the embedded dataset immediately after the first render call.
604
+ - {_DYNAMIC_WIDGET_UX_RULE.strip()}
605
  - Slider/input changes → local calc() only (never sendPrompt on drag).
606
  - Slider initial values MUST match the exact numbers in your <RESPONSE>. Never invent defaults.
607
  - Use 0.5px solid borders — never 1px solid.
608
  - UI (HTML/CSS): use CSS variables only (no hardcoded hex/rgb). Charts (ECharts/Plotly/Chart.js): you MAY use hex colors in JS configs for palettes/series.
609
+ - NO EMOJIS — never use emojis in widgets or labels. Use text only.
610
+ - Neat and clean for any data: light backgrounds (#F5F5F5, #F2F2F2), clear typography, generous spacing. Minimal, professional layout. No decorative icons or clutter.
611
  {_REACTIVE_RUNTIME_RULE}
612
  {_DESIGN_SYSTEM_REMINDER}
613
  {_SENDPROMPT_RULE}"""
 
615
  else _JSON_WIDGET_RULE.strip()
616
  )
617
 
618
+ combined_max_tokens = getattr(config, "COMBINED_MAX_TOKENS", 7500)
619
+ token_limit_block = f"""
620
+ TOKEN LIMIT — you have ~{combined_max_tokens} tokens total for <RESPONSE> + <WIDGET>.
621
+ - Prioritize completing the widget. Never stop mid-widget or truncate. A complete, functional widget is required.
622
+ - When space is tight: shorten <RESPONSE> (2–5 sentences), not the <WIDGET>. The widget must always be full and working.
623
+ - For comparison tables in <RESPONSE>: keep focused so the widget has room. Both must fit.
624
+ """
625
 
626
+ return f"""You are an expert AI assistant with rich, dynamic interactive output — widgets should feel alive, responsive, and tailored to each question (not repetitive templates).
627
+ {social_turn_banner}
628
+ Output style: No emojis. Neat, clean, professional — in both <RESPONSE> text and <WIDGET>.
629
+ {token_limit_block}
630
  {_OUTPUT_CONTRACT_STRICT}
631
 
632
+ For every response you produce TWO sections in one generation.
633
+ The widget block may be empty for text-only turns where interactivity is not helpful.
634
+
635
+ CRITICAL — Never describe a widget you do not generate. If your <RESPONSE> mentions "the dashboard below", "interactive chart", "explore visually", or anything that implies a visualization exists, you MUST output a complete, non-empty <WIDGET>. Do NOT say "the dashboard below" if you return empty <WIDGET></WIDGET>. Either generate the full widget HTML or do not mention it in the text at all.
636
+
637
+ Only return an EMPTY widget block (<WIDGET></WIDGET>) when the turn is not “widget-worthy”:
638
+ - greetings (hi, hello, hey), acknowledgements (thanks, ok, got it), goodbyes (bye, goodbye), pure chit-chat — on these turns do NOT apply the bandit Strategy/Rule to <RESPONSE>; use a short natural reply
639
+ - conceptual Q&A with no dataset/comparison/actionable metrics
640
+ - planning/roadmap/implementation-step requests where prose is the primary output
641
+
642
+ Infer from the user's question and your <RESPONSE> content whether a widget would help. If your <RESPONSE> has structure, numbers, comparisons, or decision support, generate the best-fit widget.
643
+
644
+ Widget warrant decision checklist:
645
+ - Generate NON-EMPTY widget if the user asks for charts, dashboard, analytics, comparison, trends, KPIs, forecasting, ranking, tabular breakdown, numeric exploration, diagrams, illustrations, or images that support the answer.
646
+ - Generate NON-EMPTY widget if your response includes measurable values that benefit from visual or interactive interpretation.
647
+ - Return EMPTY widget for pure explanation/definition/planning where a chart would be decorative noise.
648
+
649
+ Understand the user's intent. When the question implies visualization, calculation, comparison, or learning, generate a NON-EMPTY <WIDGET>.
650
+
651
+ Chart/visual selection pick the right type for the data and use case:
652
+ | Data / use case | Best widget type | Library |
653
+ |-----------------|------------------|---------|
654
+ | Time-series, trends | Line or area chart | ECharts, Plotly, Chart.js |
655
+ | Categorical comparison | Bar chart (horizontal) | ECharts, Chart.js, ApexCharts |
656
+ | Part of whole | Pie, donut, stacked bar | ECharts, Chart.js |
657
+ | Correlation, x-y | Scatter plot | ECharts, Plotly, D3 |
658
+ | Adjustable numbers, formulas | Interactive calculator with sliders | Plain JS + Chart.js/ECharts |
659
+ | Rows > 8, breakdown | Tabulator table | Tabulator |
660
+ | KPI metrics | KPI tiles + optional chart | CSS + ECharts |
661
+ | Process, flow | Diagram, sankey, funnel | D3, ECharts |
662
+ | Photo, illustration, icon | <img> / inline SVG / image block (JSON) | https or data URI |
663
+
664
+ Libraries: ECharts (cdn.jsdelivr.net/npm/echarts), Plotly (cdn.plot.ly), Chart.js (cdnjs), Tabulator (tabulator.info). Pick what fits.
665
+
666
+ One chart vs multiple: Use one chart/widget when it suffices. Add more only when each adds distinct value. Never duplicate the same data in multiple chart types.
667
 
668
+ CRITICAL — Complete the widget: Never truncate or stop mid-generation. The <WIDGET> must be a complete, functional HTML document. If space is tight, shorten <RESPONSE> — the widget must always finish.
669
  ═══════════════════════════════════════════════════════
670
  OUTPUT FORMAT — always use exactly this structure
671
  ═══════════════════════════════════════════════════════
 
682
  Strategy: {strategy_id}
683
  Rule: {format_rule}
684
  Do not mention this rule. Do not add <WIDGET> inside <RESPONSE>.
685
+ CRITICAL Primitives vs Widget (never confuse these):
686
+ - The Strategy/Rule above applies ONLY to <RESPONSE> (text format: bullets, table, prose, etc.). It does NOT constrain <WIDGET>.
687
+ - <WIDGET> is SEPARATE and INDEPENDENT. Generate a widget wherever there is possibility, based on content — never skip a widget because the text format is "table" or "prose". Widget choice (chart, calculator, table) depends on content, not on the primitive.
688
 
689
  ═══════════════════════════════════════════════════════
690
  {widget_rules_header}
 
697
  ═══════════════════════════════════════════════════════
698
  Every entity, name, number, ticker, percentage shown in the widget MUST come from either:
699
  - the numeric values you extracted from the user request/context, OR
700
+ - the numeric values present in your <RESPONSE>, OR
701
+ - (for educational/concept demos only) reasonable illustrative values you choose (e.g. P=1000, r=5%, t=10 for compound interest).
702
+ - Use meaningful labels (Principal, Rate, Years). When using mock data, clearly label it (e.g. "Example", "Mock data").
703
+ - When real data is missing: you MAY use illustrative/mock data; label it clearly in the widget.
704
+ - Prefer data from <RESPONSE> or user context; when unavailable, use mock/illustrative data and label it.
705
+ - When using mock/illustrative data (dates, tickers, values), label it clearly “e.g. Example data, Mock data”.
706
 
707
  ═══════════════════════════════════════════════════════
708
  sendPrompt specificity — always specific, never generic
 
764
  widget_match = re.search(r"<WIDGET>(.*?)</WIDGET>", raw, re.DOTALL | re.IGNORECASE)
765
  if widget_match:
766
  raw_widget = widget_match.group(1).strip()
 
 
 
 
767
 
768
  widget_mode = getattr(config, "WIDGET_MODE", "json").strip().lower()
769
  if widget_mode == "json":
770
+ widget_payload = finalize_widget_schema_json(raw_widget)
771
  else:
772
+ # HTML mode: strip markdown fences if model wrapped widget in ```...```
773
+ if "```" in raw_widget:
774
+ fence = re.search(r"```(?:json|html)?\s*(.*?)```", raw_widget, re.DOTALL | re.IGNORECASE)
775
+ raw_widget = fence.group(1).strip() if fence else re.sub(r"```\w*", "", raw_widget).strip()
776
  if "<" in raw_widget and ">" in raw_widget:
777
  if "<html" not in raw_widget.lower():
778
  raw_widget = f"<html><head></head><body>{raw_widget}</body></html>"
vivek/backend/config.py ADDED
@@ -0,0 +1,264 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Configuration and constants for the Adaptive Presentation Engine backend.
2
+
3
+ Loads environment variables from .env (via python-dotenv) and defines runtime
4
+ configuration: LLM provider selection, API keys, Thompson Sampling params, paths.
5
+ Restart the server after changing .env.
6
+ """
7
+
8
+ from pathlib import Path
9
+ from dotenv import load_dotenv
10
+ import json
11
+ import os
12
+ import threading
13
+
14
+ # Load .env into os.environ; override=True lets existing env vars win (Docker/CI).
15
+ load_dotenv(override=True)
16
+
17
+ # -----------------------------------------------------------------------------
18
+ # LLM provider and endpoint routing
19
+ # -----------------------------------------------------------------------------
20
+ # Primary mode: "anthropic" (Claude) or "openai_compat" (OpenAI/Groq).
21
+ LLM_MODE = os.getenv("LLM_MODE", "anthropic").lower()
22
+
23
+ # When true (default), strategy primitives are mandatory in prompts and
24
+ # enforce_response() reshapes <RESPONSE> to match. Set STRICT_PRIMITIVES=0 for soft hints only.
25
+ STRICT_PRIMITIVES = os.getenv("STRICT_PRIMITIVES", "1").strip().lower() in {"1", "true", "yes", "on"}
26
+
27
+ # Per-endpoint overrides: /api/chat_plain -> BASELINE, /api/chat -> ADAPTIVE.
28
+ BASELINE_LLM_MODE = os.getenv("BASELINE_LLM_MODE", LLM_MODE).lower()
29
+ ADAPTIVE_LLM_MODE = os.getenv("ADAPTIVE_LLM_MODE", LLM_MODE).lower()
30
+
31
+ # Widget output: "html" (full HTML in iframe) or "json" (UI schema for frontend renderer).
32
+ WIDGET_MODE = os.getenv("WIDGET_MODE", "json").strip().lower()
33
+
34
+ # Max time (sec) and tokens for combined response+widget generation.
35
+ # Tight defaults so latency stays predictable; raise only when the UI shows truncation.
36
+ COMBINED_TIMEOUT_SECONDS = int(os.getenv("COMBINED_TIMEOUT_SECONDS", "45"))
37
+ COMBINED_MAX_TOKENS = int(os.getenv("COMBINED_MAX_TOKENS", "4000"))
38
+
39
+ # OpenAI-compatible API (Groq, OpenAI, or any chat/completions provider)
40
+ OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL", "https://api.groq.com/openai/v1")
41
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
42
+ OPENAI_MODEL = os.getenv("OPENAI_MODEL", "llama-3.1-8b-instant")
43
+
44
+ # Anthropic Claude API
45
+ ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY", "")
46
+ ANTHROPIC_MODEL = os.getenv("ANTHROPIC_MODEL", "claude-opus-4-6")
47
+
48
+ # Anthropic Fast Mode: beta header for faster inference; we retry on failure.
49
+ ANTHROPIC_FAST_MODE_ENABLED = os.getenv("ANTHROPIC_FAST_MODE_ENABLED", "1").strip().lower() in {
50
+ "1", "true", "yes", "on"
51
+ }
52
+ ANTHROPIC_FAST_MODE_SPEED = os.getenv("ANTHROPIC_FAST_MODE_SPEED", "fast") # "fast" or "standard"
53
+ ANTHROPIC_FAST_MODE_BETA = os.getenv("ANTHROPIC_FAST_MODE_BETA", "fast-mode-2026-02-01")
54
+
55
+ # Thompson Sampling: D=feature dim, LAMBDA=reg, GAMMA=decay, ALPHA_G=global prior strength.
56
+ D = 10
57
+ LAMBDA = 0.01
58
+ GAMMA = 0.99
59
+ ALPHA_G = 0.05
60
+
61
+ # Base temperature for Thompson Sampling (exploration).
62
+ TS_TEMPERATURE = float(os.getenv("TS_TEMPERATURE", "2.0"))
63
+
64
+ # On strong negative feedback: boost temp, penalize last strategy, damp its posterior.
65
+ NEG_EXPLORE_THRESHOLD = float(os.getenv("NEG_EXPLORE_THRESHOLD", "0.40"))
66
+ EXPLORE_TEMP_BOOST = float(os.getenv("EXPLORE_TEMP_BOOST", "2.0"))
67
+ EXPLORE_SCORE_PENALTY = float(os.getenv("EXPLORE_SCORE_PENALTY", "1.75"))
68
+
69
+ NEG_MU_SHRINK = float(os.getenv("NEG_MU_SHRINK", "0.25"))
70
+ NEG_SINV_SHRINK = float(os.getenv("NEG_SINV_SHRINK", "0.35"))
71
+
72
+ # Strategy primitives: style constraints injected into LLM prompts.
73
+ # These are bootstrapped defaults. At runtime we may override them from
74
+ # `strategies.json` to make the bandit "strategy bars" admin-manageable.
75
+ DEFAULT_STRATEGIES = {
76
+ "structured_bullets": "Use 3-5 bullet points only (start each line with '- '). No intro sentence. Do NOT ask questions. Do NOT use numbered lists.",
77
+ "narrative_prose": "Write 2-3 short paragraphs. No bullet points.",
78
+ "concise_direct": "Reply in at most 3 sentences. Be direct.",
79
+ "socratic_questions": "Brief acknowledgement, then ask 1-2 clarifying questions.",
80
+ "step_by_step": "Numbered list of 3-6 steps only.",
81
+ # NEW
82
+ "comparison_table": "Return a single MARKDOWN TABLE only. Use columns that help compare options (e.g., Option | Pros | Cons | Best for). No bullets outside the table.",
83
+ "visualization": "Return a simple TEXT visualization only (ASCII bar chart or small table-of-values). Put it in a fenced code block. No extra prose outside the code block.",
84
+ }
85
+
86
+ # Module-level strategy state (may be overwritten by `reload_strategies()`).
87
+ # - `STRATEGY_ITEMS`: all strategies (enabled + disabled), keyed by id.
88
+ # - `STRATEGIES`: instruction map for all strategies, keyed by id.
89
+ # - `STRATEGY_NAMES`: enabled strategy ids (used by bandit selection/summary loops).
90
+ STRATEGY_ITEMS: dict[str, dict] = {}
91
+ STRATEGIES: dict[str, str] = dict(DEFAULT_STRATEGIES)
92
+ STRATEGY_NAMES: list[str] = list(DEFAULT_STRATEGIES.keys())
93
+ STRATEGY_LABELS: dict[str, str] = {
94
+ "structured_bullets": "Structured Bullets",
95
+ "narrative_prose": "Narrative Prose",
96
+ "concise_direct": "Concise & Direct",
97
+ "socratic_questions": "Socratic Questions",
98
+ "step_by_step": "Step-by-Step",
99
+ "comparison_table": "Comparison Table",
100
+ "visualization": "Visualization",
101
+ }
102
+ K = len(STRATEGY_NAMES)
103
+
104
+ # Paths: HERE = backend/, parent = vivek/ (project root).
105
+ HERE = Path(__file__).resolve().parent
106
+ # Frontend entry point served at GET /. Prefer the Vite production build.
107
+ _FRONTEND_VUE_DIST_INDEX = HERE.parent / "frontend-vue" / "dist" / "index.html"
108
+ INDEX_HTML = (
109
+ _FRONTEND_VUE_DIST_INDEX
110
+ if _FRONTEND_VUE_DIST_INDEX.exists()
111
+ else HERE.parent / "frontend" / "index.html"
112
+ )
113
+
114
+ # Skills document — high-level chart/widget guidance injected into LLM prompts.
115
+ # Override with SKILLS_PATH env var if needed.
116
+ SKILLS_PATH = Path(os.getenv("SKILLS_PATH", str(HERE.parent / "skills.md")))
117
+ SKILLS_CONTENT = SKILLS_PATH.read_text(encoding="utf-8") if SKILLS_PATH.exists() else ""
118
+
119
+ # -----------------------------------------------------------------------------
120
+ # Dynamic strategy store (admin-manageable bandit bars)
121
+ # -----------------------------------------------------------------------------
122
+ STRATEGIES_JSON_PATH = os.getenv("STRATEGIES_JSON_PATH", str(HERE.parent / "strategies.json"))
123
+ # RLock because `persist_strategies()` calls `reload_strategies()` which also acquires the lock.
124
+ _STRATEGIES_LOCK = threading.RLock()
125
+
126
+ def _read_strategies_json() -> dict:
127
+ """
128
+ strategies.json format:
129
+ {
130
+ "version": 1,
131
+ "items": [
132
+ { "id": "...", "label": "...", "instruction": "...", "enabled": true, "event_name": "Decision" }
133
+ ]
134
+ }
135
+ """
136
+ try:
137
+ with open(STRATEGIES_JSON_PATH, "r", encoding="utf-8") as f:
138
+ return json.load(f)
139
+ except FileNotFoundError:
140
+ return {}
141
+ except Exception:
142
+ return {}
143
+
144
+
145
+ def reload_strategies() -> None:
146
+ """
147
+ Reload `strategies.json` into module-level globals.
148
+ Safe to call multiple times.
149
+ """
150
+ global STRATEGY_ITEMS, STRATEGIES, STRATEGY_NAMES, STRATEGY_LABELS, K
151
+
152
+ with _STRATEGIES_LOCK:
153
+ data = _read_strategies_json()
154
+ items = data.get("items") if isinstance(data, dict) else None
155
+ if not isinstance(items, list) or not items:
156
+ # Bootstrapped defaults (no dynamic strategies yet).
157
+ STRATEGY_ITEMS = {k: {"id": k, "instruction": v, "label": STRATEGY_LABELS.get(k, k), "enabled": True, "event_name": ""} for k, v in DEFAULT_STRATEGIES.items()}
158
+ STRATEGIES = dict(DEFAULT_STRATEGIES)
159
+ STRATEGY_NAMES = list(DEFAULT_STRATEGIES.keys())
160
+ K = len(STRATEGY_NAMES)
161
+ return
162
+
163
+ next_items: dict[str, dict] = {}
164
+ next_strategies: dict[str, str] = {}
165
+ next_labels: dict[str, str] = {}
166
+ enabled_ids: list[str] = []
167
+
168
+ for it in items:
169
+ if not isinstance(it, dict):
170
+ continue
171
+ sid = str(it.get("id") or "").strip()
172
+ instruction = str(it.get("instruction") or "").strip()
173
+ if not sid or not instruction:
174
+ continue
175
+ label = str(it.get("label") or sid).strip()
176
+ enabled = bool(it.get("enabled", True))
177
+ event_name = str(it.get("event_name") or "").strip()
178
+
179
+ next_items[sid] = {"id": sid, "label": label, "instruction": instruction, "enabled": enabled, "event_name": event_name}
180
+ next_strategies[sid] = instruction
181
+ next_labels[sid] = label
182
+ if enabled:
183
+ enabled_ids.append(sid)
184
+
185
+ # If admin disabled everything, keep at least the bootstrap defaults enabled to avoid breaking TS loops.
186
+ if not enabled_ids:
187
+ enabled_ids = list(DEFAULT_STRATEGIES.keys())
188
+ next_items = {k: {"id": k, "label": STRATEGY_LABELS.get(k, k), "instruction": v, "enabled": True, "event_name": ""} for k, v in DEFAULT_STRATEGIES.items()}
189
+ next_strategies = dict(DEFAULT_STRATEGIES)
190
+ next_labels = dict(STRATEGY_LABELS)
191
+
192
+ STRATEGY_ITEMS = next_items
193
+ STRATEGIES = next_strategies
194
+ STRATEGY_LABELS = next_labels
195
+ STRATEGY_NAMES = enabled_ids
196
+ K = len(STRATEGY_NAMES)
197
+
198
+
199
+ def _atomic_write_strategies_json(data: dict) -> None:
200
+ tmp = f"{STRATEGIES_JSON_PATH}.tmp"
201
+ with open(tmp, "w", encoding="utf-8") as f:
202
+ json.dump(data, f, ensure_ascii=False, indent=2)
203
+ os.replace(tmp, STRATEGIES_JSON_PATH)
204
+
205
+
206
+ def persist_strategies(items: list[dict]) -> None:
207
+ """
208
+ Persist `strategies.json` then reload module globals.
209
+ `items` should be a list of:
210
+ {id, label, instruction, enabled, event_name}
211
+ """
212
+ payload = {"version": 1, "items": items}
213
+ with _STRATEGIES_LOCK:
214
+ _atomic_write_strategies_json(payload)
215
+ reload_strategies()
216
+
217
+
218
+ # Initial load at import time.
219
+ reload_strategies()
220
+
221
+ # -----------------------------------------------------------------------------
222
+ # Auth (JWT)
223
+ # -----------------------------------------------------------------------------
224
+ JWT_SECRET = os.getenv("JWT_SECRET", "dev-change-me-please")
225
+ JWT_ALGORITHM = os.getenv("JWT_ALGORITHM", "HS256")
226
+ JWT_EXPIRE_SECONDS = int(os.getenv("JWT_EXPIRE_SECONDS", str(60 * 60 * 24 * 7)))
227
+
228
+ # -----------------------------------------------------------------------------
229
+ # Admin authorization (primitives are admin-only)
230
+ # -----------------------------------------------------------------------------
231
+ # Admins can manage primitives and have their primitives injected into the LLM prompt.
232
+ #
233
+ # Setup options:
234
+ # 1) Provide a single admin to auto-create on startup:
235
+ # - ADMIN_USERNAME
236
+ # - ADMIN_EMAIL
237
+ # - ADMIN_PASSWORD
238
+ # 2) Or mark existing users as admin via lists:
239
+ # - ADMIN_USERNAMES (comma-separated)
240
+ # - ADMIN_EMAILS (comma-separated)
241
+ # 3) Or mark specific users by id:
242
+ # - ADMIN_USER_IDS (comma-separated)
243
+ #
244
+ # If no admin config is provided, the backend keeps the previous behavior
245
+ # (any authenticated user can access primitives).
246
+ ADMIN_USERNAME = os.getenv("ADMIN_USERNAME", "").strip()
247
+ ADMIN_EMAIL = os.getenv("ADMIN_EMAIL", "").strip()
248
+ ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "").strip()
249
+
250
+ def _split_csv(s: str) -> list[str]:
251
+ return [x.strip() for x in (s or "").split(",") if x.strip()]
252
+
253
+ ADMIN_USERNAMES = _split_csv(os.getenv("ADMIN_USERNAMES", "")) or ([ADMIN_USERNAME] if ADMIN_USERNAME else [])
254
+ ADMIN_EMAILS = _split_csv(os.getenv("ADMIN_EMAILS", "")) or ([ADMIN_EMAIL] if ADMIN_EMAIL else [])
255
+ ADMIN_USER_IDS = _split_csv(os.getenv("ADMIN_USER_IDS", ""))
256
+
257
+ # Used by server-side gating logic.
258
+ ADMIN_CONFIGURED = bool(ADMIN_USERNAMES or ADMIN_EMAILS or ADMIN_USER_IDS)
259
+
260
+ # SQLite persistence
261
+ DB_PATH = os.getenv("DB_PATH", str(HERE.parent / "adaptiveui.sqlite3"))
262
+
263
+ # JSON persistence for user primitives (when DB/SQL is not desired).
264
+ PRIMITIVES_JSON_PATH = os.getenv("PRIMITIVES_JSON_PATH", str(HERE.parent / "user_primitives.json"))
vivek/backend/db.py ADDED
@@ -0,0 +1,774 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import threading
6
+ import time
7
+ from typing import Any, Dict, List, Optional
8
+
9
+ import numpy as np
10
+ from sqlalchemy import Column, Integer, LargeBinary, String, Text, create_engine, func, select, text
11
+ from sqlalchemy.orm import declarative_base, sessionmaker
12
+
13
+ from . import config
14
+
15
+ Base = declarative_base()
16
+
17
+
18
+ class UserRow(Base):
19
+ __tablename__ = "users"
20
+
21
+ user_id = Column(String, primary_key=True)
22
+ username = Column(String, unique=True, nullable=False)
23
+ email = Column(String, unique=True, nullable=False)
24
+ password_hash = Column(LargeBinary, nullable=False)
25
+
26
+
27
+ class BanditGlobalRow(Base):
28
+ __tablename__ = "bandit_global"
29
+
30
+ # single row
31
+ id = Column(Integer, primary_key=True, default=1)
32
+ global_n = Column(Integer, nullable=False, default=0)
33
+ global_mu_json = Column(Text, nullable=False)
34
+ global_sinv_json = Column(Text, nullable=False)
35
+
36
+
37
+ class BanditUserRow(Base):
38
+ __tablename__ = "bandit_users"
39
+
40
+ user_id = Column(String, primary_key=True)
41
+ mu_json = Column(Text, nullable=False)
42
+ sigma_inv_json = Column(Text, nullable=False)
43
+ history_json = Column(Text, nullable=False)
44
+ reward_log_json = Column(Text, nullable=False)
45
+
46
+ last_message = Column(Text, default="", nullable=False)
47
+ last_response = Column(Text, default="", nullable=False)
48
+ last_strategy = Column(String, nullable=True)
49
+ last_x_json = Column(Text, nullable=True)
50
+
51
+ msg_count = Column(Integer, nullable=False, default=0)
52
+ prefs_json = Column(Text, nullable=False, default="[]")
53
+ locked_strategy = Column(String, nullable=True)
54
+ pending_strategy = Column(String, nullable=True)
55
+
56
+
57
+ class ConversationLogRow(Base):
58
+ __tablename__ = "conversation_logs"
59
+
60
+ id = Column(Integer, primary_key=True, autoincrement=True)
61
+ user_id = Column(String, nullable=False, index=True)
62
+ pane = Column(String, nullable=False) # "adaptive" | "baseline"
63
+ role = Column(String, nullable=False) # "user" | "assistant"
64
+ content = Column(Text, nullable=False)
65
+
66
+ strategy = Column(String, nullable=True)
67
+ elapsed = Column(String, nullable=True)
68
+ widget = Column(Integer, nullable=False, default=0)
69
+ ts = Column(Integer, nullable=False, default=0)
70
+ # Persisted widget payload so SPA reload can restore interactive widgets.
71
+ widget_html = Column(Text, nullable=False, default="")
72
+ widget_schema = Column(Text, nullable=False, default="")
73
+ widget_height = Column(Integer, nullable=False, default=0)
74
+
75
+
76
+ class UserPrimitiveRow(Base):
77
+ __tablename__ = "user_primitives"
78
+
79
+ id = Column(Integer, primary_key=True, autoincrement=True)
80
+ user_id = Column(String, nullable=False, index=True)
81
+ name = Column(String, nullable=False)
82
+ instruction = Column(Text, nullable=False)
83
+ created_ts = Column(Integer, nullable=False, default=0)
84
+ updated_ts = Column(Integer, nullable=False, default=0)
85
+
86
+
87
+ _PRIMITIVES_LOCK = threading.Lock()
88
+
89
+
90
+ def _read_primitives_json() -> dict:
91
+ """
92
+ primitives.json format:
93
+ {
94
+ "<user_id>": {
95
+ "next_id": 1,
96
+ "items": [{ id, name, instruction, created_ts, updated_ts }, ...]
97
+ }
98
+ }
99
+ """
100
+ path = getattr(config, "PRIMITIVES_JSON_PATH", str(config.HERE.parent / "user_primitives.json"))
101
+ try:
102
+ with open(path, "r", encoding="utf-8") as f:
103
+ data = json.load(f)
104
+ except FileNotFoundError:
105
+ return {}
106
+ except Exception:
107
+ return {}
108
+ if not isinstance(data, dict):
109
+ return {}
110
+ return data
111
+
112
+
113
+ def _atomic_write_primitives_json(data: dict) -> None:
114
+ path = getattr(config, "PRIMITIVES_JSON_PATH", str(config.HERE.parent / "user_primitives.json"))
115
+ tmp = f"{path}.tmp"
116
+ with open(tmp, "w", encoding="utf-8") as f:
117
+ json.dump(data, f, ensure_ascii=False, indent=2)
118
+ os.replace(tmp, path)
119
+
120
+
121
+ _engine = None
122
+ _SessionLocal = None
123
+
124
+
125
+ def _json_dumps(obj: Any) -> str:
126
+ return json.dumps(obj, ensure_ascii=False, separators=(",", ":"))
127
+
128
+
129
+ def _json_loads(s: str | None, default: Any) -> Any:
130
+ if not s:
131
+ return default
132
+ return json.loads(s)
133
+
134
+
135
+ def get_engine():
136
+ global _engine, _SessionLocal
137
+ if _engine is None:
138
+ _engine = create_engine(
139
+ f"sqlite:///{config.DB_PATH}",
140
+ connect_args={"check_same_thread": False},
141
+ )
142
+ _SessionLocal = sessionmaker(bind=_engine, autoflush=False, autocommit=False)
143
+ return _engine
144
+
145
+
146
+ def init_db() -> None:
147
+ eng = get_engine()
148
+ Base.metadata.create_all(bind=eng)
149
+ _migrate_conversation_log_widget_columns()
150
+
151
+ # Ensure bandit_global exists so we can upsert cleanly later.
152
+ Session = _SessionLocal
153
+ assert Session is not None
154
+ with Session() as session:
155
+ row = session.execute(select(BanditGlobalRow)).scalar_one_or_none()
156
+ if row is None:
157
+ # Keep empty defaults; engine startup will already have proper values.
158
+ # We'll overwrite the row on first persist.
159
+ strategy_ids = set(getattr(config, "STRATEGY_ITEMS", {}) or {}).union(set(config.STRATEGY_NAMES))
160
+ dummy_mu = {k: [0.0] * config.D for k in strategy_ids}
161
+ dummy_sinv = {k: (np.eye(config.D) * 0.1).tolist() for k in strategy_ids}
162
+ session.add(
163
+ BanditGlobalRow(
164
+ id=1,
165
+ global_n=0,
166
+ global_mu_json=_json_dumps(dummy_mu),
167
+ global_sinv_json=_json_dumps(dummy_sinv),
168
+ )
169
+ )
170
+ session.commit()
171
+
172
+
173
+ def _migrate_conversation_log_widget_columns() -> None:
174
+ """SQLite: add widget payload columns to existing DBs (create_all does not ALTER)."""
175
+ eng = get_engine()
176
+ with eng.connect() as conn:
177
+ rows = conn.execute(text("PRAGMA table_info(conversation_logs)")).fetchall()
178
+ colnames = {str(r[1]) for r in rows}
179
+ stmts: list[str] = []
180
+ if "widget_html" not in colnames:
181
+ stmts.append(
182
+ "ALTER TABLE conversation_logs ADD COLUMN widget_html TEXT NOT NULL DEFAULT ''"
183
+ )
184
+ if "widget_schema" not in colnames:
185
+ stmts.append(
186
+ "ALTER TABLE conversation_logs ADD COLUMN widget_schema TEXT NOT NULL DEFAULT ''"
187
+ )
188
+ if "widget_height" not in colnames:
189
+ stmts.append(
190
+ "ALTER TABLE conversation_logs ADD COLUMN widget_height INTEGER NOT NULL DEFAULT 0"
191
+ )
192
+ for s in stmts:
193
+ conn.execute(text(s))
194
+ if stmts:
195
+ conn.commit()
196
+
197
+
198
+ def log_conversation_message(
199
+ *,
200
+ user_id: str,
201
+ pane: str,
202
+ role: str,
203
+ content: str,
204
+ strategy: str | None = None,
205
+ elapsed: float | None = None,
206
+ widget: bool = False,
207
+ widget_html: str = "",
208
+ widget_schema: str = "",
209
+ widget_height: int = 0,
210
+ ts: int | None = None,
211
+ ) -> None:
212
+ Session = _SessionLocal
213
+ assert Session is not None
214
+ now = int(ts if ts is not None else time.time())
215
+ with Session() as session:
216
+ session.add(
217
+ ConversationLogRow(
218
+ user_id=user_id,
219
+ pane=str(pane),
220
+ role=str(role),
221
+ content=str(content or ""),
222
+ strategy=str(strategy) if strategy else None,
223
+ elapsed=str(elapsed) if elapsed is not None else None,
224
+ widget=1 if widget else 0,
225
+ ts=now,
226
+ widget_html=str(widget_html or ""),
227
+ widget_schema=str(widget_schema or ""),
228
+ widget_height=int(widget_height or 0),
229
+ )
230
+ )
231
+ session.commit()
232
+
233
+
234
+ def get_recent_conversation_messages(*, user_id: str, pane: str, limit: int = 80) -> List[Dict[str, Any]]:
235
+ Session = _SessionLocal
236
+ assert Session is not None
237
+ lim = max(1, min(int(limit), 400))
238
+ with Session() as session:
239
+ rows = (
240
+ session.execute(
241
+ select(ConversationLogRow)
242
+ .where(ConversationLogRow.user_id == user_id)
243
+ .where(ConversationLogRow.pane == pane)
244
+ .order_by(ConversationLogRow.id.desc())
245
+ .limit(lim)
246
+ )
247
+ .scalars()
248
+ .all()
249
+ )
250
+ rows = list(reversed(rows))
251
+ return [
252
+ {
253
+ "role": r.role,
254
+ "content": r.content,
255
+ "strategy": r.strategy,
256
+ "elapsed": r.elapsed,
257
+ "widget": bool(r.widget),
258
+ "ts": r.ts,
259
+ "widget_html": getattr(r, "widget_html", None) or "",
260
+ "widget_schema": getattr(r, "widget_schema", None) or "",
261
+ "widget_height": int(getattr(r, "widget_height", None) or 0),
262
+ }
263
+ for r in rows
264
+ ]
265
+
266
+
267
+ def delete_conversation_logs(*, user_id: str, pane: str | None = None) -> None:
268
+ Session = _SessionLocal
269
+ assert Session is not None
270
+ with Session() as session:
271
+ q = ConversationLogRow.__table__.delete().where(ConversationLogRow.user_id == user_id) # type: ignore[attr-defined]
272
+ if pane:
273
+ q = q.where(ConversationLogRow.pane == pane)
274
+ session.execute(q)
275
+ session.commit()
276
+
277
+
278
+ def aggregate_strategy_usage() -> Dict[str, Dict[str, Any]]:
279
+ """
280
+ Aggregate strategy usage stats for admin dashboards.
281
+
282
+ Returns:
283
+ {
284
+ "<strategy_id>": {"wins": int, "trials": int, "last_ts": int | None},
285
+ ...
286
+ }
287
+ """
288
+ Session = _SessionLocal
289
+ assert Session is not None
290
+
291
+ by_id: Dict[str, Dict[str, Any]] = {}
292
+
293
+ with Session() as session:
294
+ user_rows = session.execute(select(BanditUserRow)).scalars().all()
295
+ for row in user_rows:
296
+ reward_log = _json_loads(row.reward_log_json, [])
297
+ if not isinstance(reward_log, list):
298
+ continue
299
+ for entry in reward_log:
300
+ if not isinstance(entry, (list, tuple)) or len(entry) < 2:
301
+ continue
302
+ sid = str(entry[0] or "").strip()
303
+ if not sid:
304
+ continue
305
+ try:
306
+ reward = float(entry[1])
307
+ except Exception:
308
+ continue
309
+
310
+ bucket = by_id.setdefault(sid, {"wins": 0, "trials": 0, "last_ts": None})
311
+ bucket["trials"] += 1
312
+ if reward >= 0.5:
313
+ bucket["wins"] += 1
314
+
315
+ last_rows = session.execute(
316
+ select(ConversationLogRow.strategy, func.max(ConversationLogRow.ts))
317
+ .where(ConversationLogRow.pane == "adaptive")
318
+ .where(ConversationLogRow.role == "assistant")
319
+ .where(ConversationLogRow.strategy.isnot(None))
320
+ .group_by(ConversationLogRow.strategy)
321
+ ).all()
322
+
323
+ for sid_raw, max_ts in last_rows:
324
+ sid = str(sid_raw or "").strip()
325
+ if not sid:
326
+ continue
327
+ bucket = by_id.setdefault(sid, {"wins": 0, "trials": 0, "last_ts": None})
328
+ bucket["last_ts"] = int(max_ts) if max_ts is not None else None
329
+
330
+ return by_id
331
+
332
+
333
+ def get_strategy_analytics(strategy_id: str, days: int = 30) -> Dict[str, Any]:
334
+ """
335
+ Return per-day usage series and summary for a strategy.
336
+ """
337
+ Session = _SessionLocal
338
+ assert Session is not None
339
+
340
+ sid = str(strategy_id or "").strip()
341
+ days_n = max(7, min(int(days), 180))
342
+ now = int(time.time())
343
+ day_seconds = 86400
344
+ start_ts = now - (days_n - 1) * day_seconds
345
+
346
+ day_buckets = {}
347
+ for i in range(days_n):
348
+ ts = start_ts + i * day_seconds
349
+ day_key = time.strftime("%Y-%m-%d", time.localtime(ts))
350
+ day_buckets[day_key] = 0
351
+
352
+ with Session() as session:
353
+ rows = (
354
+ session.execute(
355
+ select(ConversationLogRow.ts)
356
+ .where(ConversationLogRow.pane == "adaptive")
357
+ .where(ConversationLogRow.role == "assistant")
358
+ .where(ConversationLogRow.strategy == sid)
359
+ .where(ConversationLogRow.ts >= start_ts)
360
+ )
361
+ .scalars()
362
+ .all()
363
+ )
364
+
365
+ for ts in rows:
366
+ if ts is None:
367
+ continue
368
+ key = time.strftime("%Y-%m-%d", time.localtime(int(ts)))
369
+ if key in day_buckets:
370
+ day_buckets[key] += 1
371
+
372
+ labels = list(day_buckets.keys())
373
+ usage_series = [int(day_buckets[k]) for k in labels]
374
+ total_usage = int(sum(usage_series))
375
+
376
+ usage_all = aggregate_strategy_usage().get(sid, {})
377
+ wins = int(usage_all.get("wins") or 0)
378
+ trials = int(usage_all.get("trials") or 0)
379
+
380
+ return {
381
+ "strategy_id": sid,
382
+ "days": days_n,
383
+ "labels": labels,
384
+ "usage_series": usage_series,
385
+ "summary": {
386
+ "total_usage": total_usage,
387
+ "wins": wins,
388
+ "trials": trials,
389
+ },
390
+ }
391
+
392
+
393
+ def list_user_primitives(user_id: str) -> List[Dict[str, Any]]:
394
+ with _PRIMITIVES_LOCK:
395
+ data = _read_primitives_json()
396
+ box = data.get(user_id) or {}
397
+ items = box.get("items") if isinstance(box, dict) else None
398
+ if not isinstance(items, list):
399
+ return []
400
+ out: List[Dict[str, Any]] = []
401
+ for it in items:
402
+ if not isinstance(it, dict):
403
+ continue
404
+ prim_id = int(it.get("id") or 0)
405
+ name = str(it.get("name") or "")
406
+ inst = str(it.get("instruction") or "")
407
+ if prim_id <= 0 or not name or not inst:
408
+ continue
409
+ out.append(
410
+ {
411
+ "id": prim_id,
412
+ "name": name,
413
+ "instruction": inst,
414
+ "created_ts": int(it.get("created_ts") or 0),
415
+ "updated_ts": int(it.get("updated_ts") or 0),
416
+ }
417
+ )
418
+ out.sort(key=lambda x: x["id"])
419
+ # Migration fallback:
420
+ # If the user has primitives in the legacy SQLite table, but JSON is empty,
421
+ # automatically copy them into JSON so the UI can show everything.
422
+ if out:
423
+ return out
424
+
425
+ try:
426
+ Session = _SessionLocal
427
+ if Session is None:
428
+ # Ensure engine/session exists (init_db should have done this).
429
+ get_engine()
430
+ Session = _SessionLocal
431
+ assert Session is not None
432
+ with Session() as session:
433
+ rows = (
434
+ session.execute(
435
+ select(UserPrimitiveRow)
436
+ .where(UserPrimitiveRow.user_id == user_id)
437
+ .order_by(UserPrimitiveRow.id.asc())
438
+ )
439
+ .scalars()
440
+ .all()
441
+ )
442
+ if not rows:
443
+ return []
444
+ migrated: List[Dict[str, Any]] = []
445
+ max_id = 0
446
+ for r in rows:
447
+ prim_id = int(getattr(r, "id") or 0)
448
+ if prim_id <= 0:
449
+ continue
450
+ max_id = max(max_id, prim_id)
451
+ migrated.append(
452
+ {
453
+ "id": prim_id,
454
+ "name": str(getattr(r, "name") or ""),
455
+ "instruction": str(getattr(r, "instruction") or ""),
456
+ "created_ts": int(getattr(r, "created_ts") or 0),
457
+ "updated_ts": int(getattr(r, "updated_ts") or 0),
458
+ }
459
+ )
460
+
461
+ if not migrated:
462
+ return []
463
+
464
+ # Write into JSON.
465
+ data = _read_primitives_json()
466
+ data[user_id] = {"next_id": max_id + 1, "items": migrated}
467
+ _atomic_write_primitives_json(data)
468
+ return migrated
469
+ except Exception:
470
+ # If migration fails for any reason, return the JSON (empty) result.
471
+ return []
472
+
473
+
474
+ def create_user_primitive(*, user_id: str, name: str, instruction: str) -> Dict[str, Any]:
475
+ now = int(time.time())
476
+ nm = str(name).strip()
477
+ inst = str(instruction).strip()
478
+
479
+ with _PRIMITIVES_LOCK:
480
+ data = _read_primitives_json()
481
+ box = data.get(user_id) or {"next_id": 1, "items": []}
482
+ if not isinstance(box, dict):
483
+ box = {"next_id": 1, "items": []}
484
+ items = box.get("items") or []
485
+ if not isinstance(items, list):
486
+ items = []
487
+
488
+ next_id = int(box.get("next_id") or 1)
489
+ if items:
490
+ max_id = max([int(it.get("id") or 0) for it in items if isinstance(it, dict)], default=0)
491
+ next_id = max(next_id, max_id + 1)
492
+
493
+ new_item = {"id": next_id, "name": nm, "instruction": inst, "created_ts": now, "updated_ts": now}
494
+ items.append(new_item)
495
+ box["items"] = items
496
+ box["next_id"] = next_id + 1
497
+ data[user_id] = box
498
+ _atomic_write_primitives_json(data)
499
+ return new_item
500
+
501
+
502
+ def update_user_primitive(*, user_id: str, prim_id: int, name: str, instruction: str) -> Dict[str, Any] | None:
503
+ now = int(time.time())
504
+ nm = str(name).strip()
505
+ inst = str(instruction).strip()
506
+ with _PRIMITIVES_LOCK:
507
+ data = _read_primitives_json()
508
+ box = data.get(user_id) or {}
509
+ if not isinstance(box, dict):
510
+ return None
511
+ items = box.get("items") or []
512
+ if not isinstance(items, list):
513
+ return None
514
+
515
+ found = False
516
+ created_ts = 0
517
+ for it in items:
518
+ if not isinstance(it, dict):
519
+ continue
520
+ if int(it.get("id") or 0) != int(prim_id):
521
+ continue
522
+ it["name"] = nm
523
+ it["instruction"] = inst
524
+ it["updated_ts"] = now
525
+ created_ts = int(it.get("created_ts") or 0)
526
+ found = True
527
+ break
528
+
529
+ if not found:
530
+ return None
531
+
532
+ box["items"] = items
533
+ data[user_id] = box
534
+ _atomic_write_primitives_json(data)
535
+ return {"id": int(prim_id), "name": nm, "instruction": inst, "created_ts": created_ts, "updated_ts": now}
536
+
537
+
538
+ def delete_user_primitive(*, user_id: str, prim_id: int) -> bool:
539
+ with _PRIMITIVES_LOCK:
540
+ data = _read_primitives_json()
541
+ box = data.get(user_id)
542
+ if not isinstance(box, dict):
543
+ return False
544
+ items = box.get("items") or []
545
+ if not isinstance(items, list):
546
+ return False
547
+ before = len(items)
548
+ items = [it for it in items if not (isinstance(it, dict) and int(it.get("id") or 0) == int(prim_id))]
549
+ if len(items) == before:
550
+ return False
551
+ box["items"] = items
552
+ data[user_id] = box
553
+ _atomic_write_primitives_json(data)
554
+ return True
555
+
556
+
557
+ def load_users_from_db() -> List[Dict[str, Any]]:
558
+ Session = _SessionLocal
559
+ assert Session is not None
560
+ with Session() as session:
561
+ rows = session.execute(select(UserRow)).scalars().all()
562
+ return [
563
+ {
564
+ "user_id": r.user_id,
565
+ "username": r.username,
566
+ "email": r.email,
567
+ "password_hash": r.password_hash,
568
+ }
569
+ for r in rows
570
+ ]
571
+
572
+
573
+ def persist_user(user_rec: Any) -> None:
574
+ """
575
+ Upsert a user record into `users`.
576
+ `user_rec` can be `backend.auth.UserRecord` or any object with
577
+ {user_id, username, email, password_hash}.
578
+ """
579
+ Session = _SessionLocal
580
+ assert Session is not None
581
+
582
+ with Session() as session:
583
+ existing = session.execute(select(UserRow).where(UserRow.user_id == user_rec.user_id)).scalar_one_or_none()
584
+ if existing is None:
585
+ session.add(
586
+ UserRow(
587
+ user_id=user_rec.user_id,
588
+ username=user_rec.username,
589
+ email=user_rec.email,
590
+ password_hash=user_rec.password_hash,
591
+ )
592
+ )
593
+ else:
594
+ existing.username = user_rec.username
595
+ existing.email = user_rec.email
596
+ existing.password_hash = user_rec.password_hash
597
+ session.commit()
598
+
599
+
600
+ def persist_global_state(bandit_engine: Any) -> None:
601
+ Session = _SessionLocal
602
+ assert Session is not None
603
+
604
+ # Persist ALL keys present in the engine's posterior state (enabled + disabled).
605
+ global_mu = {k: np.asarray(v).tolist() for k, v in (bandit_engine.global_mu or {}).items()}
606
+ global_sinv = {k: np.asarray(v).tolist() for k, v in (bandit_engine.global_sinv or {}).items()}
607
+
608
+ with Session() as session:
609
+ row = session.execute(select(BanditGlobalRow).where(BanditGlobalRow.id == 1)).scalar_one_or_none()
610
+ if row is None:
611
+ row = BanditGlobalRow(id=1)
612
+ session.add(row)
613
+ row.global_n = int(bandit_engine.global_n)
614
+ row.global_mu_json = _json_dumps(global_mu)
615
+ row.global_sinv_json = _json_dumps(global_sinv)
616
+ session.commit()
617
+
618
+
619
+ def load_global_state(bandit_engine: Any) -> None:
620
+ Session = _SessionLocal
621
+ assert Session is not None
622
+ with Session() as session:
623
+ row = session.execute(select(BanditGlobalRow).where(BanditGlobalRow.id == 1)).scalar_one_or_none()
624
+ if not row:
625
+ return
626
+
627
+ global_mu = _json_loads(row.global_mu_json, {})
628
+ global_sinv = _json_loads(row.global_sinv_json, {})
629
+ bandit_engine.global_n = int(row.global_n or 0)
630
+ desired_ids = set(getattr(config, "STRATEGY_ITEMS", {}) or {}).union(set(bandit_engine.global_mu.keys()))
631
+ default_sinv = (np.eye(config.D) * 0.1).tolist()
632
+ for k in desired_ids:
633
+ bandit_engine.global_mu[k] = np.array(global_mu.get(k, [0.0] * config.D), dtype=float)
634
+ bandit_engine.global_sinv[k] = np.array(global_sinv.get(k, default_sinv), dtype=float)
635
+
636
+
637
+ def _serialize_user(user: dict) -> Dict[str, Any]:
638
+ # Persist ALL keys present in the user's posterior state (enabled + disabled).
639
+ mu_dict = user.get("mu") or {}
640
+ sinv_dict = user.get("sigma_inv") or {}
641
+ mu = {k: np.asarray(v).tolist() for k, v in mu_dict.items()}
642
+ sinv = {k: np.asarray(v).tolist() for k, v in sinv_dict.items()}
643
+
644
+ reward_log = [[str(s), float(r)] for (s, r) in (user.get("reward_log") or [])]
645
+
646
+ prefs = sorted(list(user.get("prefs") or []))
647
+ last_x = user.get("last_x")
648
+
649
+ return {
650
+ "mu": mu,
651
+ "sigma_inv": sinv,
652
+ "history": user.get("history") or [],
653
+ "reward_log": reward_log,
654
+ "last_message": user.get("last_message") or "",
655
+ "last_response": user.get("last_response") or "",
656
+ "last_strategy": user.get("last_strategy"),
657
+ "last_x": last_x,
658
+ "msg_count": int(user.get("msg_count") or 0),
659
+ "prefs": prefs,
660
+ "locked_strategy": user.get("locked_strategy"),
661
+ "pending_strategy": user.get("pending_strategy"),
662
+ }
663
+
664
+
665
+ def _deserialize_user(user_id: str, payload: Dict[str, Any]) -> dict:
666
+ desired_ids = set(getattr(config, "STRATEGY_ITEMS", {}) or {}).union(set(config.STRATEGY_NAMES))
667
+ default_sinv = (np.eye(config.D) * 0.1).tolist()
668
+
669
+ mu_payload = payload.get("mu") if isinstance(payload.get("mu"), dict) else {}
670
+ sinv_payload = payload.get("sigma_inv") if isinstance(payload.get("sigma_inv"), dict) else {}
671
+
672
+ mu = {}
673
+ sinv = {}
674
+ for k in desired_ids:
675
+ mu[k] = np.array(mu_payload.get(k, [0.0] * config.D), dtype=float)
676
+ sinv[k] = np.array(sinv_payload.get(k, default_sinv), dtype=float)
677
+
678
+ user = {
679
+ "mu": mu,
680
+ "sigma_inv": sinv,
681
+ "history": payload.get("history") or [],
682
+ "reward_log": payload.get("reward_log") or [],
683
+ "last_message": payload.get("last_message") or "",
684
+ "last_response": payload.get("last_response") or "",
685
+ "last_strategy": payload.get("last_strategy"),
686
+ "last_x": payload.get("last_x"),
687
+ "msg_count": int(payload.get("msg_count") or 0),
688
+ "prefs": set(payload.get("prefs") or []),
689
+ "locked_strategy": payload.get("locked_strategy"),
690
+ "pending_strategy": payload.get("pending_strategy"),
691
+ }
692
+ return user
693
+
694
+
695
+ def persist_user_state(bandit_engine: Any, user_id: str) -> None:
696
+ Session = _SessionLocal
697
+ assert Session is not None
698
+
699
+ user = bandit_engine.get_user(user_id)
700
+ payload = _serialize_user(user)
701
+
702
+ with Session() as session:
703
+ row = session.execute(select(BanditUserRow).where(BanditUserRow.user_id == user_id)).scalar_one_or_none()
704
+ if row is None:
705
+ row = BanditUserRow(user_id=user_id)
706
+ session.add(row)
707
+
708
+ row.mu_json = _json_dumps(payload["mu"])
709
+ row.sigma_inv_json = _json_dumps(payload["sigma_inv"])
710
+ row.history_json = _json_dumps(payload["history"])
711
+ row.reward_log_json = _json_dumps(payload["reward_log"])
712
+
713
+ row.last_message = payload["last_message"]
714
+ row.last_response = payload["last_response"]
715
+ row.last_strategy = payload["last_strategy"]
716
+ row.last_x_json = _json_dumps(payload["last_x"]) if payload["last_x"] is not None else None
717
+
718
+ row.msg_count = payload["msg_count"]
719
+ row.prefs_json = _json_dumps(payload["prefs"])
720
+ row.locked_strategy = payload["locked_strategy"]
721
+ row.pending_strategy = payload["pending_strategy"]
722
+
723
+ session.commit()
724
+
725
+
726
+ def delete_user_state(user_id: str) -> None:
727
+ Session = _SessionLocal
728
+ assert Session is not None
729
+ with Session() as session:
730
+ session.execute(
731
+ BanditUserRow.__table__.delete().where(BanditUserRow.user_id == user_id) # type: ignore[attr-defined]
732
+ )
733
+ session.commit()
734
+
735
+
736
+ def load_user_states(bandit_engine: Any) -> None:
737
+ """
738
+ Load all persisted `bandit_users` into the in-memory engine.
739
+ """
740
+ Session = _SessionLocal
741
+ assert Session is not None
742
+
743
+ with Session() as session:
744
+ rows = session.execute(select(BanditUserRow)).scalars().all()
745
+ for r in rows:
746
+ mu = _json_loads(r.mu_json, {})
747
+ sinv = _json_loads(r.sigma_inv_json, {})
748
+ history = _json_loads(r.history_json, [])
749
+ reward_log = _json_loads(r.reward_log_json, [])
750
+ last_x = _json_loads(r.last_x_json, None)
751
+ prefs = _json_loads(r.prefs_json, [])
752
+
753
+ payload = {
754
+ "mu": mu,
755
+ "sigma_inv": sinv,
756
+ "history": history,
757
+ "reward_log": reward_log,
758
+ "last_message": r.last_message or "",
759
+ "last_response": r.last_response or "",
760
+ "last_strategy": r.last_strategy,
761
+ "last_x": last_x,
762
+ "msg_count": r.msg_count or 0,
763
+ "prefs": prefs,
764
+ "locked_strategy": r.locked_strategy,
765
+ "pending_strategy": r.pending_strategy,
766
+ }
767
+
768
+ # Ensure engine creates structure, then overwrite fields.
769
+ engine_user = bandit_engine.get_user(r.user_id)
770
+ loaded = _deserialize_user(r.user_id, payload)
771
+
772
+ engine_user.update(loaded)
773
+
774
+
{anupa → vivek}/backend/engine.py RENAMED
@@ -19,15 +19,69 @@ from .utils import sigmoid, mean_uncertainty
19
 
20
  class BayesianEngine:
21
  def __init__(self):
22
- self.global_mu = {k: np.zeros(config.D) for k in config.STRATEGY_NAMES}
23
- self.global_sinv = {k: np.eye(config.D) * 0.1 for k in config.STRATEGY_NAMES}
 
 
 
24
  self.users = {}
25
  self.global_n = 0
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  def _new_user(self):
 
28
  return {
29
- "mu": {k: self.global_mu[k].copy() for k in config.STRATEGY_NAMES},
30
- "sigma_inv": {k: self.global_sinv[k].copy() for k in config.STRATEGY_NAMES},
31
  "history": [],
32
  "reward_log": [],
33
  "last_message": "",
@@ -56,7 +110,12 @@ class BayesianEngine:
56
  rl = user["reward_log"]
57
  avg_r = sum(r for _, r in rl[-5:]) / max(len(rl[-5:]), 1) if rl else 0.5
58
  msg_num = min(user["msg_count"] / 20.0, 1.0)
59
- si = config.STRATEGY_NAMES.index(user["last_strategy"]) / (config.K-1) if user["last_strategy"] else 0.5
 
 
 
 
 
60
  trend = 0.0
61
  if len(rl) >= 3:
62
  ys = [r for _, r in rl[-5:]]
@@ -71,7 +130,7 @@ class BayesianEngine:
71
 
72
  def _damp_posterior(self, user: dict, strategy: str, strength: float):
73
  """Option B: reduce confidence in the last chosen strategy."""
74
- if strategy not in config.STRATEGY_NAMES:
75
  return
76
  s = float(np.clip(strength, 0.0, 1.0))
77
  if s <= 0:
@@ -96,7 +155,7 @@ class BayesianEngine:
96
  # One-time override: honor the upfront user-selected format once,
97
  # then return to adaptive Thompson Sampling on later turns.
98
  pending = user.get("pending_strategy")
99
- if pending in config.STRATEGY_NAMES:
100
  user["pending_strategy"] = None
101
  return pending, {k: 0.0 for k in config.STRATEGY_NAMES}, x, prev
102
 
@@ -104,7 +163,7 @@ class BayesianEngine:
104
  # Hard-lock disables exploration, but should NOT block an explicit request like
105
  # "compare X vs Y" or "put it in a table".
106
  locked = user.get("locked_strategy")
107
- if locked in config.STRATEGY_NAMES:
108
  force_explore = False
109
  if explicit_strategy is None:
110
  explicit_strategy = locked
@@ -136,7 +195,7 @@ class BayesianEngine:
136
  # previously hard-locked a default style. The lock only disables exploration.
137
  if explicit_strategy in config.STRATEGY_NAMES:
138
  chosen = explicit_strategy
139
- elif locked in config.STRATEGY_NAMES:
140
  chosen = locked
141
  else:
142
  chosen = max(scores, key=scores.get)
@@ -145,6 +204,9 @@ class BayesianEngine:
145
 
146
  def update(self, uid: str, strategy: str, x: np.ndarray, reward: float):
147
  user = self.get_user(uid)
 
 
 
148
  mu_old = user["mu"][strategy]
149
  si_old = user["sigma_inv"][strategy]
150
  r_hat = sigmoid(float(x @ mu_old))
@@ -205,6 +267,15 @@ class BayesianEngine:
205
  def reset_user(self, uid: str):
206
  self.users.pop(uid, None)
207
 
 
 
 
 
 
 
 
 
 
208
 
209
  engine = BayesianEngine()
210
  USERB_ID = "__user_b__"
 
19
 
20
  class BayesianEngine:
21
  def __init__(self):
22
+ # Keep posterior state for all known strategies (enabled + disabled).
23
+ # Selection/visualization should still use `config.STRATEGY_NAMES` (enabled only).
24
+ initial_ids = set(getattr(config, "STRATEGY_ITEMS", {}) or {}).union(set(config.STRATEGY_NAMES))
25
+ self.global_mu = {k: np.zeros(config.D) for k in initial_ids}
26
+ self.global_sinv = {k: np.eye(config.D) * 0.1 for k in initial_ids}
27
  self.users = {}
28
  self.global_n = 0
29
 
30
+ def _all_strategy_ids(self) -> set[str]:
31
+ items = getattr(config, "STRATEGY_ITEMS", {}) or {}
32
+ return set(items.keys()) if items else set(config.STRATEGY_NAMES)
33
+
34
+ def reconcile_strategies(self) -> None:
35
+ """
36
+ Reconcile engine posterior dictionaries with the current strategy store.
37
+
38
+ - If new strategies appear: add priors (zero mean, low precision identity).
39
+ - If strategies are hard-deleted: remove their posterior state.
40
+ - If strategies are disabled: keep state but they won't be selected/visualized
41
+ because selection/summary loops use `config.STRATEGY_NAMES`.
42
+ """
43
+ desired_ids = self._all_strategy_ids()
44
+
45
+ # Add missing strategy keys.
46
+ for sid in desired_ids:
47
+ if sid not in self.global_mu:
48
+ self.global_mu[sid] = np.zeros(config.D)
49
+ self.global_sinv[sid] = np.eye(config.D) * 0.1
50
+
51
+ # Remove hard-deleted strategies.
52
+ current_ids = set(self.global_mu.keys())
53
+ to_remove = current_ids - desired_ids
54
+ for sid in to_remove:
55
+ self.global_mu.pop(sid, None)
56
+ self.global_sinv.pop(sid, None)
57
+ for user in self.users.values():
58
+ user.get("mu", {}).pop(sid, None)
59
+ user.get("sigma_inv", {}).pop(sid, None)
60
+ prefs = user.get("prefs")
61
+ if isinstance(prefs, set) and sid in prefs:
62
+ prefs.discard(sid)
63
+ if user.get("locked_strategy") == sid:
64
+ user["locked_strategy"] = None
65
+ if user.get("pending_strategy") == sid:
66
+ user["pending_strategy"] = None
67
+
68
+ # Ensure all existing users have keys for every known strategy.
69
+ for user in self.users.values():
70
+ mu = user.get("mu") or {}
71
+ sinv = user.get("sigma_inv") or {}
72
+ for sid in desired_ids:
73
+ if sid not in mu:
74
+ mu[sid] = self.global_mu[sid].copy()
75
+ if sid not in sinv:
76
+ sinv[sid] = self.global_sinv[sid].copy()
77
+ user["mu"] = mu
78
+ user["sigma_inv"] = sinv
79
+
80
  def _new_user(self):
81
+ all_ids = self._all_strategy_ids()
82
  return {
83
+ "mu": {k: self.global_mu[k].copy() for k in all_ids},
84
+ "sigma_inv": {k: self.global_sinv[k].copy() for k in all_ids},
85
  "history": [],
86
  "reward_log": [],
87
  "last_message": "",
 
110
  rl = user["reward_log"]
111
  avg_r = sum(r for _, r in rl[-5:]) / max(len(rl[-5:]), 1) if rl else 0.5
112
  msg_num = min(user["msg_count"] / 20.0, 1.0)
113
+ # Map last strategy into [0,1] by its rank among enabled strategies.
114
+ # If the strategy is currently disabled, fall back to 0.5.
115
+ if user.get("last_strategy") and user["last_strategy"] in config.STRATEGY_NAMES and config.K > 1:
116
+ si = config.STRATEGY_NAMES.index(user["last_strategy"]) / (config.K - 1)
117
+ else:
118
+ si = 0.5
119
  trend = 0.0
120
  if len(rl) >= 3:
121
  ys = [r for _, r in rl[-5:]]
 
130
 
131
  def _damp_posterior(self, user: dict, strategy: str, strength: float):
132
  """Option B: reduce confidence in the last chosen strategy."""
133
+ if strategy not in user.get("mu", {}):
134
  return
135
  s = float(np.clip(strength, 0.0, 1.0))
136
  if s <= 0:
 
155
  # One-time override: honor the upfront user-selected format once,
156
  # then return to adaptive Thompson Sampling on later turns.
157
  pending = user.get("pending_strategy")
158
+ if pending in self.global_mu:
159
  user["pending_strategy"] = None
160
  return pending, {k: 0.0 for k in config.STRATEGY_NAMES}, x, prev
161
 
 
163
  # Hard-lock disables exploration, but should NOT block an explicit request like
164
  # "compare X vs Y" or "put it in a table".
165
  locked = user.get("locked_strategy")
166
+ if locked in self.global_mu:
167
  force_explore = False
168
  if explicit_strategy is None:
169
  explicit_strategy = locked
 
195
  # previously hard-locked a default style. The lock only disables exploration.
196
  if explicit_strategy in config.STRATEGY_NAMES:
197
  chosen = explicit_strategy
198
+ elif locked in self.global_mu and locked in config.STRATEGY_NAMES:
199
  chosen = locked
200
  else:
201
  chosen = max(scores, key=scores.get)
 
204
 
205
  def update(self, uid: str, strategy: str, x: np.ndarray, reward: float):
206
  user = self.get_user(uid)
207
+ if strategy not in user.get("mu", {}) or strategy not in user.get("sigma_inv", {}):
208
+ # Strategy might have been hard-deleted after selection; ignore update safely.
209
+ return
210
  mu_old = user["mu"][strategy]
211
  si_old = user["sigma_inv"][strategy]
212
  r_hat = sigmoid(float(x @ mu_old))
 
267
  def reset_user(self, uid: str):
268
  self.users.pop(uid, None)
269
 
270
+ def clear_conversation_thread(self, uid: str) -> None:
271
+ """Drop chat history and last-turn pointers without resetting posteriors or reward log."""
272
+ u = self.get_user(uid)
273
+ u["history"] = []
274
+ u["last_message"] = ""
275
+ u["last_response"] = ""
276
+ u["last_strategy"] = None
277
+ u["last_x"] = None
278
+
279
 
280
  engine = BayesianEngine()
281
  USERB_ID = "__user_b__"
{anupa → vivek}/backend/llm.py RENAMED
File without changes
vivek/backend/server.py ADDED
@@ -0,0 +1,1399 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FastAPI server for the Adaptive Presentation Engine backend.
2
+
3
+ Endpoints
4
+ ---------
5
+ - GET / : Vue SPA entry
6
+ - GET /api/healthz : process liveness (no external calls)
7
+ - GET /api/health : LLM connectivity check (use sparingly)
8
+ - GET /api/state : bandit posteriors
9
+ - POST /api/chat_plain : baseline chat (no strategy selection)
10
+ - POST /api/chat : adaptive chat (non-streaming)
11
+ - POST /api/chat_stream : adaptive chat (SSE streaming — Claude-style)
12
+ - POST /api/rate : explicit feedback (reward update)
13
+ - POST /api/preference : user preference warm-start
14
+ - POST /api/reset : clear user session
15
+
16
+ SSE stream events (`evt.type`)
17
+ ------------------------------
18
+ - strategy : bandit selected strategy + posteriors (emitted first)
19
+ - response_delta : token chunk inside <RESPONSE>
20
+ - widget_start : model opened <WIDGET>
21
+ - widget_delta : raw token chunk inside <WIDGET> (live, Claude-style)
22
+ - done : final assembled payload (canonical response + polished widget)
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import json
28
+ import os
29
+ import time
30
+ import threading
31
+ import uuid
32
+ from typing import Iterable
33
+
34
+ import numpy as np
35
+ from fastapi import BackgroundTasks, Depends, FastAPI, HTTPException, Request, status
36
+ from fastapi.middleware.cors import CORSMiddleware
37
+ from fastapi.middleware.gzip import GZipMiddleware
38
+ from fastapi.security import HTTPBearer
39
+ from fastapi.staticfiles import StaticFiles
40
+ from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
41
+ from pydantic import BaseModel
42
+
43
+ from . import config, llm
44
+ from .combined_prompt import (
45
+ build_combined_system_prompt,
46
+ build_combined_user_prompt,
47
+ extract_embeddable_html_document,
48
+ finalize_widget_schema_json,
49
+ is_social_or_greeting_turn,
50
+ parse_combined_output,
51
+ widget_schema_json_is_valid,
52
+ )
53
+ from .engine import engine, USERB_ID
54
+ from .auth import (
55
+ authenticate_login,
56
+ create_access_token,
57
+ decode_access_token,
58
+ get_user_by_id,
59
+ get_user_by_email,
60
+ register_user,
61
+ seed_users_from_db,
62
+ update_password,
63
+ )
64
+ from . import db as persistence
65
+ from .utils import (
66
+ detect_explore_trigger,
67
+ enforce_response,
68
+ fast_valence,
69
+ negative_strength,
70
+ )
71
+ from .widget_prompt import estimate_widget_height, inject_design_system
72
+ from .widget_stream import parse_combined_stream
73
+
74
+
75
+ # ---------------------------------------------------------------------------
76
+ # Enforcement helpers
77
+ # ---------------------------------------------------------------------------
78
+
79
+ def _maybe_enforce_primitive(user_message: str, strategy: str, response: str) -> str:
80
+ """Apply strict format except for short greeting/thanks-only turns."""
81
+ if not config.STRICT_PRIMITIVES or not response:
82
+ return response
83
+ if is_social_or_greeting_turn(user_message):
84
+ return response
85
+ return enforce_response(strategy, response)
86
+
87
+
88
+ # ---------------------------------------------------------------------------
89
+ # Auth plumbing
90
+ # ---------------------------------------------------------------------------
91
+
92
+ bearer_scheme = HTTPBearer(auto_error=False)
93
+
94
+ _password_reset_lock = threading.Lock()
95
+ _password_reset_tokens: dict[str, dict] = {}
96
+ _PASSWORD_RESET_TTL_SECONDS = int(os.getenv("PASSWORD_RESET_TTL_SECONDS", "900"))
97
+
98
+
99
+ def require_user_id(credentials=Depends(bearer_scheme)) -> str:
100
+ if credentials is None or not getattr(credentials, "credentials", None):
101
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated")
102
+ token = credentials.credentials
103
+ try:
104
+ return decode_access_token(token)
105
+ except Exception as e:
106
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=f"Invalid token: {str(e)}")
107
+
108
+
109
+ def _is_admin_user(user_id: str) -> bool:
110
+ """
111
+ Admin check. Previous behavior silently promoted everyone to admin when
112
+ ADMIN_* config was missing; that's a production footgun. We now require
113
+ explicit admin configuration — no config means no admin.
114
+ """
115
+ if not getattr(config, "ADMIN_CONFIGURED", False):
116
+ return False
117
+
118
+ if user_id in set(map(str, getattr(config, "ADMIN_USER_IDS", []) or [])):
119
+ return True
120
+
121
+ rec = get_user_by_id(user_id)
122
+ if not rec:
123
+ return False
124
+
125
+ username_n = (rec.username or "").strip().lower()
126
+ email_n = (rec.email or "").strip().lower()
127
+ return username_n in {u.lower() for u in (config.ADMIN_USERNAMES or [])} or email_n in {
128
+ e.lower() for e in (config.ADMIN_EMAILS or [])
129
+ }
130
+
131
+
132
+ def require_admin_user_id(user_id: str = Depends(require_user_id)) -> str:
133
+ if not _is_admin_user(user_id):
134
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin only")
135
+ return user_id
136
+
137
+
138
+ # ---------------------------------------------------------------------------
139
+ # SSE helpers
140
+ # ---------------------------------------------------------------------------
141
+
142
+ def sse_pack(evt: dict) -> str:
143
+ """Pack a JSON event into an SSE data frame."""
144
+ return f"data: {json.dumps(evt, ensure_ascii=False)}\n\n"
145
+
146
+
147
+ def _looks_truncated_widget_html(html: str) -> bool:
148
+ if not html:
149
+ return True
150
+ lower = html.lower()
151
+ if lower.count("<style") > lower.count("</style>"):
152
+ return True
153
+ if lower.count("<script") > lower.count("</script>"):
154
+ return True
155
+ if lower.count("<body") > lower.count("</body>"):
156
+ return True
157
+ if lower.count("<html") > lower.count("</html>"):
158
+ return True
159
+ import re as _re
160
+ if _re.search(r"[<{(]$", html.strip()):
161
+ return True
162
+ return False
163
+
164
+
165
+ def _json_layout_is_only_numeric_index_arrays(schema_str: str) -> bool:
166
+ """
167
+ Detect bogus JSON widgets where every block is text like '[0,1,2]' (e.g. tic-tac-toe
168
+ win lines) instead of real UI. Those pass structural validation but are not widgets.
169
+ """
170
+ import json as _json
171
+ import re as _re
172
+
173
+ try:
174
+ o = _json.loads(schema_str)
175
+ except _json.JSONDecodeError:
176
+ return False
177
+ layout = o.get("layout")
178
+ if not isinstance(layout, list) or len(layout) < 2:
179
+ return False
180
+ pat = _re.compile(r"^\s*\[\s*\d+(\s*,\s*\d+)*\s*\]\s*$")
181
+ for item in layout:
182
+ if not isinstance(item, dict):
183
+ return False
184
+ if str(item.get("type", "")).lower() != "text":
185
+ return False
186
+ if not pat.match(str(item.get("content", ""))):
187
+ return False
188
+ return True
189
+
190
+
191
+ def _dispatch_json_mode_widget(widget_payload_raw: str) -> tuple[str, str, int, str]:
192
+ """
193
+ Prefer a valid JSON schema for the Vue renderer. If the model returned
194
+ HTML instead, fall back to iframe HTML. Returns
195
+ (widget_schema, widget_html, widget_height, widget_debug_tag).
196
+ """
197
+ raw = (widget_payload_raw or "").strip()
198
+ if not raw:
199
+ return "", "", 0, ""
200
+ finalized = finalize_widget_schema_json(raw)
201
+ if widget_schema_json_is_valid(finalized) and not _json_layout_is_only_numeric_index_arrays(finalized):
202
+ return finalized, "", 0, "json_schema_ok"
203
+ doc = extract_embeddable_html_document(raw)
204
+ if doc:
205
+ full = inject_design_system(doc)
206
+ if _looks_truncated_widget_html(full):
207
+ if _json_layout_is_only_numeric_index_arrays(finalized):
208
+ return "", "", 0, "json_html_fallback_truncated"
209
+ return finalized, "", 0, "json_html_fallback_truncated"
210
+ return "", full, estimate_widget_height(full), "json_html_fallback"
211
+ if _json_layout_is_only_numeric_index_arrays(finalized):
212
+ return "", "", 0, "json_degenerate_layout"
213
+ return finalized, "", 0, "json_schema_invalid"
214
+
215
+
216
+ def _should_generate_widget(message: str) -> bool:
217
+ """Lightweight intent gate so small-talk / explainers do not force widgets."""
218
+ import re as _re
219
+
220
+ text = (message or "").strip().lower()
221
+ if not text:
222
+ return False
223
+ low_signal = {"hi", "hello", "hey", "thanks", "thank you", "ok", "okay", "got it", "cool"}
224
+ if text in low_signal:
225
+ return False
226
+
227
+ widget_triggers = (
228
+ "chart", "graph", "plot", "dashboard", "table", "compare", "comparison",
229
+ "trend", "timeseries", "time series", "distribution", "heatmap", "scatter",
230
+ "pie", "bar", "line", "kpi", "analytics", "analyze", "analysis", "forecast",
231
+ "breakdown", "report", "visualize", "visualise", "show me", "insight", "metrics",
232
+ "tic tac", "tic-tac", "tictac", "game", "playable", "simulator", "write code",
233
+ "html page", "mini app", "calculator app",
234
+ )
235
+ if any(t in text for t in widget_triggers):
236
+ return True
237
+
238
+ text_only_intents = (
239
+ "explain", "what is", "why", "how does", "difference between", "define",
240
+ "summarize", "summarise", "plan", "roadmap", "steps", "implementation plan",
241
+ )
242
+ if any(t in text for t in text_only_intents):
243
+ return False
244
+
245
+ has_numeric_cue = bool(_re.search(r"\b\d+(\.\d+)?%?\b", text))
246
+ has_time_cue = any(t in text for t in ("daily", "weekly", "monthly", "quarterly", "yearly", "over time", "timeline"))
247
+ has_compare_cue = any(t in text for t in ("vs", "versus", "compare", "top", "rank", "distribution"))
248
+ if has_numeric_cue and (has_time_cue or has_compare_cue):
249
+ return True
250
+
251
+ return False
252
+
253
+
254
+ # ---------------------------------------------------------------------------
255
+ # Request/response schemas
256
+ # ---------------------------------------------------------------------------
257
+
258
+ class ChatPlainReq(BaseModel):
259
+ uid: str | None = None
260
+ message: str
261
+
262
+
263
+ class ChatReq(BaseModel):
264
+ uid: str | None = None
265
+ message: str
266
+
267
+
268
+ class RateReq(BaseModel):
269
+ uid: str | None = None
270
+ strategy: str
271
+ x_vec: list[float]
272
+ reward: float
273
+
274
+
275
+ class PreferenceReq(BaseModel):
276
+ uid: str | None = None
277
+ strategies: list[str] = []
278
+ lock: bool = False
279
+
280
+
281
+ class ResetReq(BaseModel):
282
+ uid: str | None = None
283
+
284
+
285
+ class PrimitiveCreateReq(BaseModel):
286
+ name: str
287
+ instruction: str
288
+
289
+
290
+ class PrimitiveUpdateReq(BaseModel):
291
+ name: str
292
+ instruction: str
293
+
294
+
295
+ class StrategyCreateReq(BaseModel):
296
+ id: str
297
+ label: str
298
+ instruction: str
299
+ enabled: bool = True
300
+ event_name: str = ""
301
+
302
+
303
+ class StrategyUpdateReq(BaseModel):
304
+ label: str
305
+ instruction: str
306
+ enabled: bool = True
307
+ event_name: str = ""
308
+
309
+
310
+ class AuthRegisterReq(BaseModel):
311
+ username: str
312
+ email: str
313
+ password: str
314
+
315
+
316
+ class AuthLoginReq(BaseModel):
317
+ username_or_email: str
318
+ password: str
319
+
320
+
321
+ class ForgotPasswordReq(BaseModel):
322
+ email: str
323
+
324
+
325
+ class ResetPasswordReq(BaseModel):
326
+ token: str
327
+ new_password: str
328
+
329
+
330
+ # ---------------------------------------------------------------------------
331
+ # App
332
+ # ---------------------------------------------------------------------------
333
+
334
+ app = FastAPI(title="Adaptive Presentation Engine")
335
+
336
+ app.add_middleware(
337
+ CORSMiddleware,
338
+ # allow_credentials=True is incompatible with "*" per CORS spec;
339
+ # keep it False since we use bearer tokens, not cookies.
340
+ allow_origins=["*"],
341
+ allow_credentials=False,
342
+ allow_methods=["*"],
343
+ allow_headers=["*"],
344
+ )
345
+ app.add_middleware(GZipMiddleware, minimum_size=1000)
346
+
347
+
348
+ @app.on_event("startup")
349
+ def _on_startup():
350
+ persistence.init_db()
351
+
352
+ if getattr(config, "ADMIN_CONFIGURED", False) and config.ADMIN_USERNAME and config.ADMIN_PASSWORD and config.ADMIN_EMAIL:
353
+ try:
354
+ register_user(username=config.ADMIN_USERNAME, email=config.ADMIN_EMAIL, password=config.ADMIN_PASSWORD)
355
+ except Exception:
356
+ pass
357
+
358
+ try:
359
+ users = persistence.load_users_from_db()
360
+ seed_users_from_db(users)
361
+ except Exception:
362
+ pass
363
+
364
+ try:
365
+ persistence.load_global_state(engine)
366
+ persistence.load_user_states(engine)
367
+ except Exception:
368
+ pass
369
+
370
+
371
+ # ---------------------------------------------------------------------------
372
+ # Static / SPA
373
+ # ---------------------------------------------------------------------------
374
+
375
+ @app.get("/", response_class=HTMLResponse)
376
+ def index() -> HTMLResponse:
377
+ html = config.INDEX_HTML.read_bytes()
378
+ return HTMLResponse(content=html, media_type="text/html; charset=utf-8")
379
+
380
+
381
+ @app.get("/api/healthz")
382
+ def healthz():
383
+ """Process liveness — cheap, no external dependencies."""
384
+ return {"ok": True}
385
+
386
+
387
+ @app.get("/api/health")
388
+ def health():
389
+ if config.LLM_MODE == "openai_compat":
390
+ h = llm.openai_health()
391
+ return {
392
+ "server": "ok",
393
+ "mode": config.LLM_MODE,
394
+ "openai_base_url": config.OPENAI_BASE_URL,
395
+ "model": config.OPENAI_MODEL,
396
+ **h,
397
+ }
398
+ if config.LLM_MODE == "anthropic":
399
+ h = llm.anthropic_health()
400
+ return {"server": "ok", "mode": config.LLM_MODE, **h}
401
+ return {
402
+ "server": "ok",
403
+ "mode": config.LLM_MODE,
404
+ "ok": False,
405
+ "reachable": False,
406
+ "error": "Unsupported LLM_MODE (expected openai_compat or anthropic)",
407
+ }
408
+
409
+
410
+ # ---------------------------------------------------------------------------
411
+ # Auth
412
+ # ---------------------------------------------------------------------------
413
+
414
+ @app.post("/api/auth/register")
415
+ def auth_register(req: AuthRegisterReq):
416
+ try:
417
+ rec = register_user(username=req.username, email=req.email, password=req.password)
418
+ except ValueError as e:
419
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
420
+
421
+ token = create_access_token(user_id=rec.user_id)
422
+ return {
423
+ "access_token": token,
424
+ "token_type": "bearer",
425
+ "user": {"user_id": rec.user_id, "username": rec.username, "email": rec.email},
426
+ }
427
+
428
+
429
+ @app.post("/api/auth/login")
430
+ def auth_login(req: AuthLoginReq):
431
+ rec = authenticate_login(username_or_email=req.username_or_email, password=req.password)
432
+ if not rec:
433
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
434
+
435
+ token = create_access_token(user_id=rec.user_id)
436
+ return {"access_token": token, "token_type": "bearer"}
437
+
438
+
439
+ @app.post("/api/auth/forgot-password")
440
+ def auth_forgot_password(req: ForgotPasswordReq):
441
+ """Dev/demo only. Do NOT return the reset token to the caller in production."""
442
+ email = (req.email or "").strip()
443
+ user = get_user_by_email(email)
444
+
445
+ if not user:
446
+ return {"ok": True}
447
+
448
+ token = uuid.uuid4().hex
449
+ now = int(time.time())
450
+ expires = now + _PASSWORD_RESET_TTL_SECONDS
451
+ with _password_reset_lock:
452
+ _password_reset_tokens[token] = {"user_id": user.user_id, "expires": expires}
453
+
454
+ # Expose reset token only in development to ease local testing.
455
+ expose_token = (os.getenv("ENV", "development") or "development").lower() != "production"
456
+ if expose_token:
457
+ return {"ok": True, "reset_token": token}
458
+ return {"ok": True}
459
+
460
+
461
+ @app.post("/api/auth/reset-password")
462
+ def auth_reset_password(req: ResetPasswordReq):
463
+ token = (req.token or "").strip()
464
+ new_password = (req.new_password or "").strip()
465
+
466
+ if not token or not new_password:
467
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="token and new_password required")
468
+
469
+ now = int(time.time())
470
+ with _password_reset_lock:
471
+ rec = _password_reset_tokens.get(token)
472
+ if not rec:
473
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid or expired token")
474
+ if int(rec.get("expires") or 0) < now:
475
+ _password_reset_tokens.pop(token, None)
476
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid or expired token")
477
+ user_id = str(rec.get("user_id") or "")
478
+ _password_reset_tokens.pop(token, None)
479
+
480
+ try:
481
+ ok = update_password(user_id=user_id, new_password=new_password)
482
+ except ValueError as e:
483
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
484
+
485
+ if not ok:
486
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
487
+
488
+ return {"ok": True}
489
+
490
+
491
+ @app.get("/api/me")
492
+ def me(user_id: str = Depends(require_user_id)):
493
+ rec = get_user_by_id(user_id)
494
+ if not rec:
495
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
496
+ return {
497
+ "user_id": rec.user_id,
498
+ "username": rec.username,
499
+ "email": rec.email,
500
+ "is_admin": _is_admin_user(rec.user_id),
501
+ }
502
+
503
+
504
+ # ---------------------------------------------------------------------------
505
+ # Bandit state + strategies
506
+ # ---------------------------------------------------------------------------
507
+
508
+ @app.get("/api/state")
509
+ def state(user_id: str = Depends(require_user_id)):
510
+ uid = user_id
511
+ x = np.ones(config.D) * 0.5
512
+ ub = engine.get_user(USERB_ID)
513
+ return {
514
+ "posterior": engine.user_posterior(uid, x),
515
+ "global": engine.global_posterior(x),
516
+ "userb": engine.posterior_summary(ub["mu"], ub["sigma_inv"], x),
517
+ "global_n": engine.global_n,
518
+ "n_users": len(engine.users),
519
+ "msg_count": engine.get_user(uid)["msg_count"],
520
+ }
521
+
522
+
523
+ @app.get("/api/strategies")
524
+ def list_strategies(user_id: str = Depends(require_user_id)):
525
+ admin = _is_admin_user(user_id)
526
+ items = []
527
+ for sid, it in getattr(config, "STRATEGY_ITEMS", {}).items():
528
+ enabled = bool(it.get("enabled", True))
529
+ if admin or enabled:
530
+ items.append(
531
+ {
532
+ "id": sid,
533
+ "label": it.get("label") or sid,
534
+ "instruction": it.get("instruction") or "",
535
+ "enabled": enabled,
536
+ "event_name": it.get("event_name") or "",
537
+ }
538
+ )
539
+ items.sort(key=lambda x: (not bool(x.get("enabled", True)), str(x.get("id") or "")))
540
+ return {"items": items}
541
+
542
+
543
+ @app.options("/api/strategies")
544
+ def strategies_preflight():
545
+ return JSONResponse({"ok": True})
546
+
547
+
548
+ @app.options("/api/strategies/")
549
+ def strategies_preflight_slash():
550
+ return JSONResponse({"ok": True})
551
+
552
+
553
+ @app.get("/api/strategies/usage")
554
+ def strategies_usage(user_id: str = Depends(require_admin_user_id)):
555
+ return {"by_id": persistence.aggregate_strategy_usage()}
556
+
557
+
558
+ @app.get("/api/strategies/usage/")
559
+ def strategies_usage_slash(user_id: str = Depends(require_admin_user_id)):
560
+ return strategies_usage(user_id=user_id)
561
+
562
+
563
+ @app.get("/api/strategies/{sid}/analytics")
564
+ def strategy_analytics(
565
+ sid: str,
566
+ days: int = 30,
567
+ user_id: str = Depends(require_admin_user_id),
568
+ ):
569
+ sid = str(sid or "").strip()
570
+ if not sid:
571
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="sid is required")
572
+ if sid not in getattr(config, "STRATEGY_ITEMS", {}):
573
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Strategy not found")
574
+ return persistence.get_strategy_analytics(sid, days=days)
575
+
576
+
577
+ @app.post("/api/strategies")
578
+ def create_strategy(req: StrategyCreateReq, user_id: str = Depends(require_admin_user_id)):
579
+ sid = str(req.id or "").strip()
580
+ if not sid:
581
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="id is required")
582
+
583
+ if sid in getattr(config, "STRATEGY_ITEMS", {}):
584
+ raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="strategy id already exists")
585
+
586
+ event_name = str(req.event_name or "").strip()
587
+ items = list(getattr(config, "STRATEGY_ITEMS", {}).values())
588
+ items.append({
589
+ "id": sid,
590
+ "label": req.label,
591
+ "instruction": req.instruction,
592
+ "enabled": bool(req.enabled),
593
+ "event_name": event_name,
594
+ })
595
+
596
+ config.persist_strategies(items)
597
+ engine.reconcile_strategies()
598
+ return {
599
+ "item": {
600
+ "id": sid,
601
+ "label": req.label,
602
+ "instruction": req.instruction,
603
+ "enabled": bool(req.enabled),
604
+ "event_name": event_name,
605
+ }
606
+ }
607
+
608
+
609
+ @app.put("/api/strategies/{sid}")
610
+ def update_strategy(sid: str, req: StrategyUpdateReq, user_id: str = Depends(require_admin_user_id)):
611
+ sid = str(sid or "").strip()
612
+ if not sid:
613
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="sid is required")
614
+
615
+ cur = getattr(config, "STRATEGY_ITEMS", {}).get(sid)
616
+ if not cur:
617
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Strategy not found")
618
+
619
+ event_name = str(req.event_name or "").strip() or str(cur.get("event_name") or "").strip()
620
+ items = []
621
+ for it in getattr(config, "STRATEGY_ITEMS", {}).values():
622
+ if it.get("id") == sid:
623
+ items.append({
624
+ "id": sid,
625
+ "label": req.label,
626
+ "instruction": req.instruction,
627
+ "enabled": bool(req.enabled),
628
+ "event_name": event_name,
629
+ })
630
+ else:
631
+ items.append(it)
632
+
633
+ config.persist_strategies(items)
634
+ engine.reconcile_strategies()
635
+ return {
636
+ "item": {
637
+ "id": sid,
638
+ "label": req.label,
639
+ "instruction": req.instruction,
640
+ "enabled": bool(req.enabled),
641
+ "event_name": event_name,
642
+ }
643
+ }
644
+
645
+
646
+ @app.post("/api/strategies/{sid}/enable")
647
+ def enable_strategy(sid: str, user_id: str = Depends(require_admin_user_id)):
648
+ sid = str(sid or "").strip()
649
+ cur = getattr(config, "STRATEGY_ITEMS", {}).get(sid)
650
+ if not cur:
651
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Strategy not found")
652
+ items = []
653
+ for it in getattr(config, "STRATEGY_ITEMS", {}).values():
654
+ if it.get("id") == sid:
655
+ items.append({"id": sid, "label": it.get("label") or sid, "instruction": it.get("instruction") or "", "enabled": True})
656
+ else:
657
+ items.append(it)
658
+ config.persist_strategies(items)
659
+ engine.reconcile_strategies()
660
+ return {"ok": True}
661
+
662
+
663
+ @app.post("/api/strategies/{sid}/disable")
664
+ def disable_strategy(sid: str, user_id: str = Depends(require_admin_user_id)):
665
+ sid = str(sid or "").strip()
666
+ cur = getattr(config, "STRATEGY_ITEMS", {}).get(sid)
667
+ if not cur:
668
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Strategy not found")
669
+ items = []
670
+ for it in getattr(config, "STRATEGY_ITEMS", {}).values():
671
+ if it.get("id") == sid:
672
+ items.append({"id": sid, "label": it.get("label") or sid, "instruction": it.get("instruction") or "", "enabled": False})
673
+ else:
674
+ items.append(it)
675
+ config.persist_strategies(items)
676
+ engine.reconcile_strategies()
677
+ return {"ok": True}
678
+
679
+
680
+ @app.delete("/api/strategies/{sid}")
681
+ def delete_strategy(sid: str, user_id: str = Depends(require_admin_user_id)):
682
+ sid = str(sid or "").strip()
683
+ cur = getattr(config, "STRATEGY_ITEMS", {}).get(sid)
684
+ if not cur:
685
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Strategy not found")
686
+
687
+ items = [it for it in getattr(config, "STRATEGY_ITEMS", {}).values() if it.get("id") != sid]
688
+ if not items:
689
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot delete all strategies")
690
+
691
+ config.persist_strategies(items)
692
+ engine.reconcile_strategies()
693
+ return {"ok": True}
694
+
695
+
696
+ # ---------------------------------------------------------------------------
697
+ # Primitives
698
+ # ---------------------------------------------------------------------------
699
+
700
+ @app.get("/api/primitives")
701
+ def list_primitives(user_id: str = Depends(require_admin_user_id)):
702
+ return {"items": persistence.list_user_primitives(user_id)}
703
+
704
+
705
+ @app.post("/api/primitives")
706
+ def create_primitive(req: PrimitiveCreateReq, user_id: str = Depends(require_admin_user_id)):
707
+ name = (req.name or "").strip()
708
+ inst = (req.instruction or "").strip()
709
+ if not name or not inst:
710
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="name and instruction are required")
711
+ row = persistence.create_user_primitive(user_id=user_id, name=name, instruction=inst)
712
+ return {"item": row}
713
+
714
+
715
+ @app.put("/api/primitives/{prim_id}")
716
+ def update_primitive(prim_id: int, req: PrimitiveUpdateReq, user_id: str = Depends(require_admin_user_id)):
717
+ name = (req.name or "").strip()
718
+ inst = (req.instruction or "").strip()
719
+ if not name or not inst:
720
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="name and instruction are required")
721
+ row = persistence.update_user_primitive(user_id=user_id, prim_id=int(prim_id), name=name, instruction=inst)
722
+ if row is None:
723
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Primitive not found")
724
+ return {"item": row}
725
+
726
+
727
+ @app.delete("/api/primitives/{prim_id}")
728
+ def delete_primitive(prim_id: int, user_id: str = Depends(require_admin_user_id)):
729
+ ok = persistence.delete_user_primitive(user_id=user_id, prim_id=int(prim_id))
730
+ if not ok:
731
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Primitive not found")
732
+ return {"ok": True}
733
+
734
+
735
+ # ---------------------------------------------------------------------------
736
+ # Conversation history
737
+ # ---------------------------------------------------------------------------
738
+
739
+ @app.get("/api/conversation")
740
+ def conversation(limit: int = 20, user_id: str = Depends(require_user_id)):
741
+ uid = user_id
742
+ lim = int(limit)
743
+ lim = max(1, min(lim, 50))
744
+
745
+ def _pairs_from_msgs(msgs: list[dict]) -> list[dict]:
746
+ out: list[dict] = []
747
+ last_user: str | None = None
748
+ for m in msgs:
749
+ if m.get("role") == "user":
750
+ last_user = str(m.get("content") or "")
751
+ elif m.get("role") == "assistant":
752
+ if last_user is None:
753
+ continue
754
+ out.append(
755
+ {
756
+ "user": last_user,
757
+ "assistant": str(m.get("content") or ""),
758
+ "widget_html": str(m.get("widget_html") or ""),
759
+ "widget_schema": str(m.get("widget_schema") or ""),
760
+ "widget_height": int(m.get("widget_height") or 0),
761
+ }
762
+ )
763
+ last_user = None
764
+ return out[-lim:]
765
+
766
+ adaptive_msgs = persistence.get_recent_conversation_messages(user_id=uid, pane="adaptive", limit=lim * 4)
767
+ baseline_msgs = persistence.get_recent_conversation_messages(user_id=uid, pane="baseline", limit=lim * 4)
768
+
769
+ adaptive_pairs = _pairs_from_msgs(adaptive_msgs)
770
+ baseline_pairs = _pairs_from_msgs(baseline_msgs)
771
+
772
+ if not adaptive_pairs:
773
+ adaptive_user = engine.get_user(uid)
774
+ adaptive_pairs = (adaptive_user.get("history") or [])[-lim:]
775
+ if not baseline_pairs:
776
+ baseline_user = engine.get_user(uid + "_plain")
777
+ baseline_pairs = (baseline_user.get("history") or [])[-lim:]
778
+
779
+ return {"adaptive": {"history": adaptive_pairs}, "baseline": {"history": baseline_pairs}}
780
+
781
+
782
+ # ---------------------------------------------------------------------------
783
+ # Background persistence (off the hot path)
784
+ # ---------------------------------------------------------------------------
785
+
786
+ def _persist_after_response(
787
+ *,
788
+ user_id: str,
789
+ uid: str,
790
+ msg: str,
791
+ response: str,
792
+ strategy: str | None,
793
+ elapsed: float | None,
794
+ widget_present: bool,
795
+ pane: str,
796
+ widget_html: str = "",
797
+ widget_schema: str = "",
798
+ widget_height: int = 0,
799
+ ) -> None:
800
+ """Persist bandit state + conversation log after the response has been sent.
801
+
802
+ Runs in FastAPI BackgroundTasks so HTTP latency is not gated by SQLite commits.
803
+ """
804
+ try:
805
+ persistence.persist_user_state(engine, uid)
806
+ if pane == "adaptive":
807
+ persistence.persist_global_state(engine)
808
+ persistence.log_conversation_message(
809
+ user_id=user_id, pane=pane, role="user", content=msg,
810
+ strategy=strategy if pane == "adaptive" else None,
811
+ )
812
+ persistence.log_conversation_message(
813
+ user_id=user_id, pane=pane, role="assistant", content=response,
814
+ strategy=strategy if pane == "adaptive" else None,
815
+ elapsed=elapsed, widget=widget_present,
816
+ widget_html=widget_html or "",
817
+ widget_schema=widget_schema or "",
818
+ widget_height=int(widget_height or 0),
819
+ )
820
+ except Exception:
821
+ # Persistence failures must never surface to the user.
822
+ pass
823
+
824
+
825
+ # ---------------------------------------------------------------------------
826
+ # Baseline chat
827
+ # ---------------------------------------------------------------------------
828
+
829
+ @app.post("/api/chat_plain")
830
+ def chat_plain(req: ChatPlainReq, bg: BackgroundTasks, user_id: str = Depends(require_user_id)):
831
+ uid = user_id + "_plain"
832
+ msg = (req.message or "").strip()
833
+ if not msg:
834
+ return JSONResponse({"error": "empty message"}, status_code=400)
835
+
836
+ user = engine.get_user(uid)
837
+ ctx: list[str] = []
838
+ for t in user["history"][-6:]:
839
+ ctx += [f"User: {t['user']}", f"Assistant: {t['assistant']}"]
840
+ ctx.append(f"User: {msg}")
841
+ prompt = "\n".join(ctx)
842
+ system = "You are a helpful AI assistant."
843
+
844
+ try:
845
+ base_mode = (config.BASELINE_LLM_MODE or config.LLM_MODE).lower()
846
+ if base_mode == "openai_compat":
847
+ response, elapsed, mode = llm.call_openai_compat(prompt, system, timeout=60)
848
+ elif base_mode == "anthropic":
849
+ response, elapsed, mode = llm.call_anthropic(prompt, system, timeout=60)
850
+ else:
851
+ raise RuntimeError("Unsupported BASELINE_LLM_MODE (expected openai_compat or anthropic)")
852
+ except Exception as e:
853
+ return JSONResponse({"error": f"LLM error: {str(e)}"}, status_code=500)
854
+
855
+ if not response:
856
+ return JSONResponse({"error": "LLM returned empty response. Check model/service."}, status_code=500)
857
+
858
+ user["history"].append({"user": msg, "assistant": response})
859
+ user["history"] = user["history"][-20:]
860
+
861
+ bg.add_task(
862
+ _persist_after_response,
863
+ user_id=user_id, uid=uid, msg=msg, response=response,
864
+ strategy=None, elapsed=elapsed, widget_present=False, pane="baseline",
865
+ )
866
+
867
+ return {"response": response, "elapsed": elapsed, "llm_mode": mode}
868
+
869
+
870
+ # ---------------------------------------------------------------------------
871
+ # Adaptive chat — non-streaming
872
+ # ---------------------------------------------------------------------------
873
+
874
+ def _build_adaptive_prompt(uid: str, msg: str):
875
+ """Shared prompt-assembly + bandit-select step for adaptive endpoints."""
876
+ user = engine.get_user(uid)
877
+
878
+ ev = fast_valence(msg, user["last_response"])
879
+ auto_detected = False
880
+ auto_r = None
881
+
882
+ if user["last_response"] and user["last_x"] is not None and user["last_strategy"]:
883
+ reward = float(np.clip(0.5 + 0.45 * ev["pos"] - 0.45 * ev["neg"], 0.05, 0.95))
884
+ engine.update(uid, user["last_strategy"], np.array(user["last_x"]), reward)
885
+ auto_detected = True
886
+ auto_r = reward
887
+
888
+ explicit = False
889
+ force_explore = bool(detect_explore_trigger(msg) or (ev.get("neg", 0.0) >= config.NEG_EXPLORE_THRESHOLD))
890
+ neg_s = negative_strength(ev)
891
+
892
+ strat, scores, x, prev = engine.select(
893
+ uid, msg, force_explore=force_explore, neg_strength=neg_s, explicit_strategy=None
894
+ )
895
+
896
+ format_rule = config.STRATEGIES.get(strat, "Be helpful and clear.")
897
+ widget_required = _should_generate_widget(msg)
898
+
899
+ prim_block = ""
900
+ if _is_admin_user(uid):
901
+ user_prims = persistence.list_user_primitives(uid)
902
+ if user_prims:
903
+ prim_lines = []
904
+ for p in user_prims:
905
+ nm = str(p.get("name") or "").strip()
906
+ inst = str(p.get("instruction") or "").strip()
907
+ if nm and inst:
908
+ prim_lines.append(f"- {nm}: {inst}")
909
+ if prim_lines:
910
+ prim_block = "\n\n## User primitives (follow these as constraints)\n" + "\n".join(prim_lines) + "\n"
911
+
912
+ combined_system = build_combined_system_prompt(
913
+ strategy_id=strat,
914
+ format_rule=format_rule,
915
+ primitive_extra_context=(getattr(config, "SKILLS_CONTENT", "") or "") + prim_block,
916
+ user_message=msg,
917
+ widget_required=widget_required,
918
+ forbidden_components=None,
919
+ required_components=None,
920
+ )
921
+ combined_prompt = build_combined_user_prompt(user_message=msg, history=user["history"])
922
+
923
+ return {
924
+ "user": user, "ev": ev, "auto_detected": auto_detected, "auto_r": auto_r,
925
+ "explicit": explicit, "force_explore": force_explore,
926
+ "strat": strat, "scores": scores, "x": x, "prev": prev,
927
+ "format_rule": format_rule, "widget_required": widget_required,
928
+ "combined_system": combined_system, "combined_prompt": combined_prompt,
929
+ }
930
+
931
+
932
+ def _post_done_payload(
933
+ *,
934
+ uid: str,
935
+ ctx: dict,
936
+ elapsed: float | None,
937
+ mode: str,
938
+ response: str,
939
+ widget_html: str,
940
+ widget_schema: str,
941
+ widget_height: int,
942
+ widget_debug: str,
943
+ raw_preview: str,
944
+ ):
945
+ ub = engine.get_user(USERB_ID)
946
+ ev = ctx["ev"]
947
+ x = ctx["x"]
948
+ strat = ctx["strat"]
949
+ prev = ctx["prev"]
950
+ scores = ctx["scores"]
951
+ return {
952
+ "response": response,
953
+ "strategy": strat,
954
+ "prev_strategy": prev,
955
+ "explicit": ctx["explicit"],
956
+ "force_explore": ctx["force_explore"],
957
+ "instruction": config.STRATEGIES.get(strat, ""),
958
+ "format_rule": ctx["format_rule"],
959
+ "elapsed": elapsed,
960
+ "llm_mode": mode,
961
+ "scores": {k: round(v, 4) for k, v in scores.items()},
962
+ "x_vec": x.tolist(),
963
+ "posterior": engine.user_posterior(uid, x),
964
+ "global": engine.global_posterior(x),
965
+ "userb": engine.posterior_summary(ub["mu"], ub["sigma_inv"], x),
966
+ "global_n": engine.global_n,
967
+ "auto_detected": ctx["auto_detected"],
968
+ "auto_r": ctx["auto_r"],
969
+ "auto_reason": ev["reason"],
970
+ "widget_html": widget_html or "",
971
+ "widget_schema": widget_schema or "",
972
+ "widget_height": widget_height,
973
+ "widget_debug": widget_debug,
974
+ "widget_raw_preview": raw_preview if not (widget_html or widget_schema) else "",
975
+ }
976
+
977
+
978
+ @app.post("/api/chat")
979
+ def chat(req: ChatReq, bg: BackgroundTasks, user_id: str = Depends(require_user_id)):
980
+ uid = user_id
981
+ msg = (req.message or "").strip()
982
+ if not msg:
983
+ return JSONResponse({"error": "empty message"}, status_code=400)
984
+
985
+ ctx = _build_adaptive_prompt(uid, msg)
986
+
987
+ combined_max_tokens = getattr(config, "COMBINED_MAX_TOKENS", 4000)
988
+ combined_timeout = getattr(config, "COMBINED_TIMEOUT_SECONDS", 45)
989
+
990
+ try:
991
+ adapt_mode = (config.ADAPTIVE_LLM_MODE or config.LLM_MODE).lower()
992
+ if adapt_mode == "openai_compat":
993
+ raw_combined, elapsed, mode = llm.call_openai_compat(
994
+ ctx["combined_prompt"], ctx["combined_system"],
995
+ timeout=combined_timeout, max_tokens=combined_max_tokens, temperature=0.2,
996
+ )
997
+ elif adapt_mode == "anthropic":
998
+ raw_combined, elapsed, mode = llm.call_anthropic(
999
+ ctx["combined_prompt"], ctx["combined_system"],
1000
+ timeout=combined_timeout, max_tokens=combined_max_tokens, temperature=0.2,
1001
+ )
1002
+ else:
1003
+ raise RuntimeError("Unsupported ADAPTIVE_LLM_MODE (expected openai_compat or anthropic)")
1004
+ except Exception as e:
1005
+ return JSONResponse({"error": f"LLM error: {str(e)}"}, status_code=500)
1006
+
1007
+ if not raw_combined:
1008
+ return JSONResponse({"error": "LLM returned empty response. Check model/service."}, status_code=500)
1009
+
1010
+ response, widget_payload_raw = parse_combined_output(raw_combined)
1011
+ if not response:
1012
+ response = raw_combined.strip()
1013
+ response = _maybe_enforce_primitive(msg, ctx["strat"], response)
1014
+
1015
+ widget_html = ""
1016
+ widget_schema = ""
1017
+ widget_height = 0
1018
+ widget_debug = ""
1019
+ widget_mode = getattr(config, "WIDGET_MODE", "json").strip().lower()
1020
+ raw_preview = ""
1021
+
1022
+ if widget_payload_raw:
1023
+ if widget_mode == "json":
1024
+ widget_schema, widget_html, widget_height, tag = _dispatch_json_mode_widget(widget_payload_raw)
1025
+ widget_debug = tag or ""
1026
+ else:
1027
+ if _looks_truncated_widget_html(widget_payload_raw):
1028
+ widget_debug = "combined_widget_truncated"
1029
+ else:
1030
+ widget_html = widget_payload_raw
1031
+ widget_height = estimate_widget_height(widget_payload_raw)
1032
+ widget_debug = "combined_widget_ok"
1033
+ else:
1034
+ widget_debug = "combined_no_schema" if widget_mode == "json" else "combined_no_widget_tag"
1035
+ raw_preview = (raw_combined or "")[:800]
1036
+ if widget_mode != "json" and ctx["widget_required"]:
1037
+ placeholder = (
1038
+ "<html><head></head><body>"
1039
+ "<div class='widget-root card'><div class='card-title'>Interactive widget</div>"
1040
+ "<div class='empty'>No widget returned.</div></div>"
1041
+ "</body></html>"
1042
+ )
1043
+ widget_html = inject_design_system(placeholder)
1044
+ widget_height = estimate_widget_height(widget_html)
1045
+ widget_debug = "fallback_widget_generated"
1046
+ elif not ctx["widget_required"]:
1047
+ widget_debug = "widget_skipped_by_intent"
1048
+
1049
+ user = ctx["user"]
1050
+ user["history"].append({"user": msg, "assistant": response})
1051
+ user["history"] = user["history"][-20:]
1052
+ user["last_message"] = msg
1053
+ user["last_response"] = response
1054
+ user["last_strategy"] = ctx["strat"]
1055
+ user["last_x"] = ctx["x"].tolist()
1056
+ user["msg_count"] += 1
1057
+
1058
+ payload = _post_done_payload(
1059
+ uid=uid, ctx=ctx, elapsed=elapsed, mode=mode, response=response,
1060
+ widget_html=widget_html, widget_schema=widget_schema,
1061
+ widget_height=widget_height, widget_debug=widget_debug, raw_preview=raw_preview,
1062
+ )
1063
+
1064
+ bg.add_task(
1065
+ _persist_after_response,
1066
+ user_id=user_id, uid=uid, msg=msg, response=response,
1067
+ strategy=ctx["strat"], elapsed=elapsed,
1068
+ widget_present=bool(widget_html or widget_schema), pane="adaptive",
1069
+ widget_html=widget_html or "",
1070
+ widget_schema=widget_schema or "",
1071
+ widget_height=int(widget_height or 0),
1072
+ )
1073
+
1074
+ return payload
1075
+
1076
+
1077
+ # ---------------------------------------------------------------------------
1078
+ # Adaptive chat — SSE streaming (Claude-style live widget streaming)
1079
+ # ---------------------------------------------------------------------------
1080
+
1081
+ @app.post("/api/chat_stream")
1082
+ def chat_stream(req: ChatReq, bg: BackgroundTasks, user_id: str = Depends(require_user_id)):
1083
+ uid = user_id
1084
+ msg = (req.message or "").strip()
1085
+ if not msg:
1086
+ return JSONResponse({"error": "empty message"}, status_code=400)
1087
+
1088
+ ctx = _build_adaptive_prompt(uid, msg)
1089
+ combined_max_tokens = getattr(config, "COMBINED_MAX_TOKENS", 4000)
1090
+ combined_timeout = getattr(config, "COMBINED_TIMEOUT_SECONDS", 45)
1091
+ adapt_mode = (config.ADAPTIVE_LLM_MODE or config.LLM_MODE).lower()
1092
+
1093
+ ub = engine.get_user(USERB_ID)
1094
+ strat = ctx["strat"]
1095
+ x = ctx["x"]
1096
+ scores = ctx["scores"]
1097
+ prev = ctx["prev"]
1098
+ ev = ctx["ev"]
1099
+
1100
+ def gen() -> Iterable[str]:
1101
+ t0 = time.time()
1102
+
1103
+ # Initial strategy event — UI paints immediately.
1104
+ yield sse_pack({
1105
+ "type": "strategy",
1106
+ "strategy": strat,
1107
+ "instruction": config.STRATEGIES.get(strat, ""),
1108
+ "format_rule": ctx["format_rule"],
1109
+ "elapsed": None,
1110
+ "force_explore": ctx["force_explore"],
1111
+ "scores": {k: round(v, 4) for k, v in scores.items()},
1112
+ "x_vec": x.tolist(),
1113
+ "posterior": engine.user_posterior(uid, x),
1114
+ "global": engine.global_posterior(x),
1115
+ "userb": engine.posterior_summary(ub["mu"], ub["sigma_inv"], x),
1116
+ "global_n": engine.global_n,
1117
+ "prev_strategy": prev,
1118
+ "explicit": ctx["explicit"],
1119
+ "auto_detected": ctx["auto_detected"],
1120
+ "auto_r": ctx["auto_r"],
1121
+ "auto_reason": ev["reason"],
1122
+ })
1123
+
1124
+ widget_html = ""
1125
+ widget_schema = ""
1126
+ widget_height = 0
1127
+ widget_debug = ""
1128
+ widget_mode = getattr(config, "WIDGET_MODE", "json").strip().lower()
1129
+ raw_preview = ""
1130
+ response = ""
1131
+ mode = adapt_mode
1132
+ elapsed_out: float | None = None
1133
+ widget_payload_raw = ""
1134
+
1135
+ try:
1136
+ if adapt_mode == "anthropic":
1137
+ stream = llm.stream_anthropic(
1138
+ ctx["combined_prompt"], ctx["combined_system"],
1139
+ timeout=combined_timeout, max_tokens=combined_max_tokens, temperature=0.2,
1140
+ )
1141
+
1142
+ # Strict primitives: buffer response until </RESPONSE>, then emit
1143
+ # the enforced version. Otherwise stream raw tokens directly.
1144
+ emit_raw_response = not (
1145
+ config.STRICT_PRIMITIVES and not is_social_or_greeting_turn(msg)
1146
+ )
1147
+
1148
+ response_closed = False
1149
+ for event in parse_combined_stream(
1150
+ stream,
1151
+ emit_response_deltas=emit_raw_response,
1152
+ emit_widget_deltas=True,
1153
+ ):
1154
+ etype = event[0]
1155
+ if etype == "response_delta":
1156
+ yield sse_pack({"type": "response_delta", "delta": event[1]})
1157
+ elif etype == "response_closed":
1158
+ response_closed = True
1159
+ response = _maybe_enforce_primitive(msg, strat, event[1])
1160
+ if not emit_raw_response and response:
1161
+ for i in range(0, len(response), 180):
1162
+ yield sse_pack({"type": "response_delta", "delta": response[i:i + 180]})
1163
+ elif etype == "widget_start":
1164
+ yield sse_pack({"type": "widget_start"})
1165
+ elif etype == "widget_delta":
1166
+ # LIVE widget streaming — Claude-style.
1167
+ yield sse_pack({"type": "widget_delta", "delta": event[1]})
1168
+ elif etype == "complete":
1169
+ if not response_closed:
1170
+ response = _maybe_enforce_primitive(msg, strat, event[1])
1171
+ if not emit_raw_response and response:
1172
+ for i in range(0, len(response), 180):
1173
+ yield sse_pack({"type": "response_delta", "delta": response[i:i + 180]})
1174
+ widget_payload_raw = event[2]
1175
+ break
1176
+
1177
+ elapsed_out = round(time.time() - t0, 1)
1178
+ mode = "anthropic"
1179
+
1180
+ elif adapt_mode == "openai_compat":
1181
+ # No token streaming on this path; simulate progressive deltas.
1182
+ raw_combined, elapsed, mode = llm.call_openai_compat(
1183
+ ctx["combined_prompt"], ctx["combined_system"],
1184
+ timeout=combined_timeout, max_tokens=combined_max_tokens, temperature=0.2,
1185
+ )
1186
+ response, widget_payload_raw = parse_combined_output(raw_combined)
1187
+ if not response:
1188
+ response = raw_combined.strip()
1189
+ response = _maybe_enforce_primitive(msg, strat, response)
1190
+ for i in range(0, len(response), 180):
1191
+ yield sse_pack({"type": "response_delta", "delta": response[i:i + 180]})
1192
+ if widget_payload_raw:
1193
+ yield sse_pack({"type": "widget_start"})
1194
+ for i in range(0, len(widget_payload_raw), 400):
1195
+ yield sse_pack({"type": "widget_delta", "delta": widget_payload_raw[i:i + 400]})
1196
+ elapsed_out = elapsed
1197
+ else:
1198
+ raise RuntimeError("Unsupported ADAPTIVE_LLM_MODE (expected openai_compat or anthropic)")
1199
+
1200
+ # Finalize widget payload for the canonical 'done' event.
1201
+ if widget_payload_raw:
1202
+ if widget_mode == "json":
1203
+ widget_schema, widget_html, widget_height, tag = _dispatch_json_mode_widget(widget_payload_raw)
1204
+ widget_debug = tag or ""
1205
+ else:
1206
+ import re as _re
1207
+ raw_widget = widget_payload_raw
1208
+ if "```" in raw_widget:
1209
+ fence = _re.search(r"```(?:json|html)?\s*(.*?)```", raw_widget, _re.DOTALL | _re.IGNORECASE)
1210
+ raw_widget = fence.group(1).strip() if fence else _re.sub(r"```\w*", "", raw_widget).strip()
1211
+ if "<" in raw_widget and ">" in raw_widget:
1212
+ if "<html" not in raw_widget.lower():
1213
+ raw_widget = f"<html><head></head><body>{raw_widget}</body></html>"
1214
+ raw_widget = _re.sub(r"<!DOCTYPE[^>]*>", "", raw_widget, flags=_re.IGNORECASE).strip()
1215
+ widget_payload_raw = inject_design_system(raw_widget)
1216
+ if _looks_truncated_widget_html(widget_payload_raw):
1217
+ widget_debug = "stream_widget_truncated"
1218
+ else:
1219
+ widget_html = widget_payload_raw
1220
+ widget_height = estimate_widget_height(widget_html)
1221
+ if widget_mode != "json" and not widget_html:
1222
+ widget_debug = widget_debug or "no_widget"
1223
+ if not response and not widget_payload_raw:
1224
+ response = "(No content)"
1225
+
1226
+ except Exception as e:
1227
+ msg_err = str(e).strip() or repr(e)
1228
+ yield sse_pack({
1229
+ "type": "done",
1230
+ "strategy": strat,
1231
+ "format_rule": ctx["format_rule"],
1232
+ "elapsed": None,
1233
+ "llm_mode": mode,
1234
+ "response": "",
1235
+ "widget_html": "",
1236
+ "widget_schema": "",
1237
+ "widget_height": 0,
1238
+ "widget_debug": f"stream_error:{msg_err}",
1239
+ "error": msg_err,
1240
+ "force_explore": ctx["force_explore"],
1241
+ "scores": {k: round(v, 4) for k, v in scores.items()},
1242
+ "x_vec": x.tolist(),
1243
+ "posterior": engine.user_posterior(uid, x),
1244
+ "global": engine.global_posterior(x),
1245
+ "userb": engine.posterior_summary(ub["mu"], ub["sigma_inv"], x),
1246
+ "global_n": engine.global_n,
1247
+ "prev_strategy": prev,
1248
+ "explicit": ctx["explicit"],
1249
+ "auto_detected": ctx["auto_detected"],
1250
+ "auto_r": ctx["auto_r"],
1251
+ "auto_reason": ev["reason"],
1252
+ })
1253
+ return
1254
+
1255
+ # Update in-memory history
1256
+ user = ctx["user"]
1257
+ user["history"].append({"user": msg, "assistant": response})
1258
+ user["history"] = user["history"][-20:]
1259
+ user["last_message"] = msg
1260
+ user["last_response"] = response
1261
+ user["last_strategy"] = strat
1262
+ user["last_x"] = x.tolist()
1263
+ user["msg_count"] += 1
1264
+
1265
+ yield sse_pack({
1266
+ "type": "done",
1267
+ "strategy": strat,
1268
+ "format_rule": ctx["format_rule"],
1269
+ "elapsed": elapsed_out,
1270
+ "llm_mode": mode,
1271
+ "response": response,
1272
+ "widget_html": widget_html or "",
1273
+ "widget_schema": widget_schema or "",
1274
+ "widget_height": widget_height,
1275
+ "widget_debug": widget_debug,
1276
+ "force_explore": ctx["force_explore"] and (ctx["explicit"] is None),
1277
+ "scores": {k: round(v, 4) for k, v in scores.items()},
1278
+ "x_vec": x.tolist(),
1279
+ "posterior": engine.user_posterior(uid, x),
1280
+ "global": engine.global_posterior(x),
1281
+ "userb": engine.posterior_summary(ub["mu"], ub["sigma_inv"], x),
1282
+ "global_n": engine.global_n,
1283
+ "prev_strategy": prev,
1284
+ "explicit": ctx["explicit"],
1285
+ "auto_detected": ctx["auto_detected"],
1286
+ "auto_r": ctx["auto_r"],
1287
+ "auto_reason": ev["reason"],
1288
+ })
1289
+
1290
+ # Persistence off the hot path
1291
+ bg.add_task(
1292
+ _persist_after_response,
1293
+ user_id=user_id, uid=uid, msg=msg, response=response,
1294
+ strategy=strat, elapsed=elapsed_out,
1295
+ widget_present=bool(widget_html or widget_schema), pane="adaptive",
1296
+ widget_html=widget_html or "",
1297
+ widget_schema=widget_schema or "",
1298
+ widget_height=int(widget_height or 0),
1299
+ )
1300
+
1301
+ return StreamingResponse(
1302
+ gen(),
1303
+ media_type="text/event-stream",
1304
+ headers={
1305
+ "Cache-Control": "no-cache, no-transform",
1306
+ "X-Accel-Buffering": "no",
1307
+ "Connection": "keep-alive",
1308
+ },
1309
+ )
1310
+
1311
+
1312
+ # ---------------------------------------------------------------------------
1313
+ # Feedback / preferences / reset
1314
+ # ---------------------------------------------------------------------------
1315
+
1316
+ @app.post("/api/rate")
1317
+ def rate(req: RateReq, bg: BackgroundTasks, user_id: str = Depends(require_user_id)):
1318
+ uid = user_id
1319
+ strategy = req.strategy
1320
+ x = np.array(req.x_vec, dtype=float)
1321
+ reward = float(req.reward)
1322
+ if strategy not in config.STRATEGY_NAMES:
1323
+ return JSONResponse({"error": "bad request"}, status_code=400)
1324
+ engine.update(uid, strategy, x, reward)
1325
+ bg.add_task(persistence.persist_user_state, engine, uid)
1326
+ bg.add_task(persistence.persist_global_state, engine)
1327
+ ub = engine.get_user(USERB_ID)
1328
+ return {
1329
+ "posterior": engine.user_posterior(uid, x),
1330
+ "global": engine.global_posterior(x),
1331
+ "userb": engine.posterior_summary(ub["mu"], ub["sigma_inv"], x),
1332
+ "global_n": engine.global_n,
1333
+ }
1334
+
1335
+
1336
+ @app.post("/api/preference")
1337
+ def preference(req: PreferenceReq, bg: BackgroundTasks, user_id: str = Depends(require_user_id)):
1338
+ uid = user_id
1339
+ engine.apply_preferences(uid, req.strategies, lock=bool(req.lock))
1340
+ bg.add_task(persistence.persist_user_state, engine, uid)
1341
+ return {"posterior": engine.user_posterior(uid)}
1342
+
1343
+
1344
+ @app.post("/api/conversation/clear")
1345
+ def conversation_clear(bg: BackgroundTasks, user_id: str = Depends(require_user_id)):
1346
+ """Remove stored chat + widgets for this account; keep bandit posteriors and reward history."""
1347
+ plain_uid = user_id + "_plain"
1348
+ persistence.delete_conversation_logs(user_id=user_id, pane="adaptive")
1349
+ persistence.delete_conversation_logs(user_id=user_id, pane="baseline")
1350
+ engine.clear_conversation_thread(user_id)
1351
+ engine.clear_conversation_thread(plain_uid)
1352
+ bg.add_task(persistence.persist_user_state, engine, user_id)
1353
+ bg.add_task(persistence.persist_user_state, engine, plain_uid)
1354
+ return {"ok": True}
1355
+
1356
+
1357
+ @app.post("/api/reset")
1358
+ def reset(req: ResetReq, user_id: str = Depends(require_user_id)):
1359
+ plain_uid = user_id + "_plain"
1360
+ engine.reset_user(user_id)
1361
+ engine.reset_user(plain_uid)
1362
+ persistence.delete_user_state(user_id)
1363
+ persistence.delete_user_state(plain_uid)
1364
+ persistence.delete_conversation_logs(user_id=user_id, pane="adaptive")
1365
+ persistence.delete_conversation_logs(user_id=user_id, pane="baseline")
1366
+ return {"ok": True}
1367
+
1368
+
1369
+ # ---------------------------------------------------------------------------
1370
+ # Static assets + SPA fallback
1371
+ # ---------------------------------------------------------------------------
1372
+
1373
+ _frontend_dist_assets_dir = config.INDEX_HTML.parent / "assets"
1374
+ if _frontend_dist_assets_dir.exists():
1375
+ app.mount(
1376
+ "/assets",
1377
+ StaticFiles(directory=str(_frontend_dist_assets_dir), html=False),
1378
+ name="frontend_assets",
1379
+ )
1380
+
1381
+
1382
+ @app.get("/{full_path:path}", response_class=HTMLResponse)
1383
+ def spa_fallback(full_path: str):
1384
+ # Never serve HTML for API URLs — return JSON 404 so client parsers don't break.
1385
+ if full_path.startswith("api"):
1386
+ return JSONResponse({"detail": "Not found"}, status_code=status.HTTP_404_NOT_FOUND)
1387
+ html = config.INDEX_HTML.read_bytes()
1388
+ return HTMLResponse(content=html, media_type="text/html; charset=utf-8")
1389
+
1390
+
1391
+ def run_server():
1392
+ """Run with uvicorn (production-ready)."""
1393
+ import uvicorn
1394
+
1395
+ port = int(os.getenv("PORT", "5051"))
1396
+ print("=" * 60)
1397
+ print(f" http://localhost:{port} mode: {config.LLM_MODE}")
1398
+ print("=" * 60)
1399
+ uvicorn.run(app, host="0.0.0.0", port=port)
{anupa → vivek}/backend/utils.py RENAMED
@@ -142,7 +142,20 @@ def enforce_response(strategy: str, text: str) -> str:
142
  try:
143
  obj = json.loads(t)
144
  if isinstance(obj, dict) and isinstance(obj.get("columns"), list) and isinstance(obj.get("rows"), list):
145
- return json.dumps(obj)
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  except Exception:
147
  pass
148
 
@@ -163,7 +176,10 @@ def enforce_response(strategy: str, text: str) -> str:
163
  ["B", "", "", ""],
164
  ],
165
  }
166
- return json.dumps(fallback)
 
 
 
167
 
168
  if strategy == "visualization":
169
  try:
@@ -174,7 +190,23 @@ def enforce_response(strategy: str, text: str) -> str:
174
  and isinstance(obj.get("labels"), list)
175
  and isinstance(obj.get("values"), list)
176
  ):
177
- return json.dumps(obj)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  except Exception:
179
  pass
180
 
@@ -190,6 +222,6 @@ def enforce_response(strategy: str, text: str) -> str:
190
  "x_label": "Category",
191
  "y_label": "Value",
192
  }
193
- return json.dumps(fallback)
194
 
195
  return t
 
142
  try:
143
  obj = json.loads(t)
144
  if isinstance(obj, dict) and isinstance(obj.get("columns"), list) and isinstance(obj.get("rows"), list):
145
+ cols = [str(c).strip() for c in obj.get("columns", [])]
146
+ rows = obj.get("rows", [])
147
+ if cols and isinstance(rows, list):
148
+ header = "| " + " | ".join(cols) + " |"
149
+ sep = "| " + " | ".join(["---"] * len(cols)) + " |"
150
+ data_lines = []
151
+ for r in rows[:8]:
152
+ if not isinstance(r, list):
153
+ continue
154
+ vals = [str(v).strip() for v in r[: len(cols)]]
155
+ if len(vals) < len(cols):
156
+ vals += [""] * (len(cols) - len(vals))
157
+ data_lines.append("| " + " | ".join(vals) + " |")
158
+ return "\n".join([header, sep] + data_lines) if data_lines else "\n".join([header, sep])
159
  except Exception:
160
  pass
161
 
 
176
  ["B", "", "", ""],
177
  ],
178
  }
179
+ header = "| Option | Pros | Cons | Best for |"
180
+ sep = "| --- | --- | --- | --- |"
181
+ rows = ["| A | | | |", "| B | | | |"]
182
+ return "\n".join([header, sep] + rows)
183
 
184
  if strategy == "visualization":
185
  try:
 
190
  and isinstance(obj.get("labels"), list)
191
  and isinstance(obj.get("values"), list)
192
  ):
193
+ labels = [str(x) for x in obj.get("labels", [])]
194
+ values = obj.get("values", [])
195
+ pairs: list[tuple[str, float]] = []
196
+ for i, lab in enumerate(labels):
197
+ try:
198
+ val = float(values[i]) if i < len(values) else 0.0
199
+ except Exception:
200
+ val = 0.0
201
+ pairs.append((lab, val))
202
+ max_val = max([abs(v) for _, v in pairs], default=1.0) or 1.0
203
+ lines = []
204
+ for lab, val in pairs[:10]:
205
+ width = int(round((abs(val) / max_val) * 24))
206
+ bar = "#" * max(1, width)
207
+ lines.append(f"{lab:<12} | {bar} {val:g}")
208
+ body = "\n".join(lines) if lines else "A | #### 1\nB | #### 1"
209
+ return f"```text\n{body}\n```"
210
  except Exception:
211
  pass
212
 
 
222
  "x_label": "Category",
223
  "y_label": "Value",
224
  }
225
+ return "```text\nA | #### 1\nB | #### 1\n```"
226
 
227
  return t
{anupa → vivek}/backend/widget_prompt.py RENAMED
@@ -58,12 +58,12 @@ _DESIGN_SYSTEM_CSS = """<style id="__ds__">
58
  --border:rgba(255,255,255,0.07);--border2:rgba(255,255,255,0.14);
59
  --accent:#5ba4f5;--accent-bg:rgba(91,164,245,0.10);--accent-b:rgba(91,164,245,0.35);
60
  }}
61
- html,body{margin:0!important;padding:12px 14px!important;
62
  background:transparent!important;color:var(--text);
63
  font-family:"Segoe UI",system-ui,sans-serif;font-size:14px;line-height:1.6}
64
  @keyframes __fu{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:translateY(0)}}
65
- .widget-root{animation:__fu .22s ease-out}
66
- .card{background:var(--bg2);border:0.5px solid var(--border);border-radius:var(--radius);padding:14px 16px;margin-bottom:12px;transition:border-color .15s}
67
  .card:hover{border-color:var(--border2)}
68
  .card-title{font-size:11px;font-weight:500;color:var(--text2);text-transform:uppercase;letter-spacing:.05em;margin-bottom:12px}
69
  .raised{background:var(--bg);border:0.5px solid var(--border);border-radius:var(--radius);padding:14px;transition:border-color .15s,transform .1s;cursor:pointer}
@@ -76,9 +76,9 @@ html,body{margin:0!important;padding:12px 14px!important;
76
  .tab:active{transform:scale(.985)}
77
  .tab.active{background:var(--bg3);color:var(--text);border-color:var(--accent)}
78
  .panel{display:none}.panel.active{display:block}
79
- table{width:100%;border-collapse:collapse;font-size:13px}
80
- th{background:var(--bg3);color:var(--text2);font-weight:500;font-size:11px;text-transform:uppercase;letter-spacing:.04em;padding:8px 11px;text-align:left;border-bottom:0.5px solid var(--border2)}
81
- td{padding:8px 11px;border-bottom:0.5px solid var(--border);color:var(--text);vertical-align:middle}
82
  tr:last-child td{border-bottom:none}
83
  tr.clickable:hover td{background:var(--accent-bg);cursor:pointer}
84
  .search{width:100%;padding:8px 12px;border-radius:var(--radius-sm);border:0.5px solid var(--border2);background:var(--bg);color:var(--text);font-size:13px;outline:none;box-sizing:border-box;margin-bottom:10px;transition:border-color .15s}
@@ -114,6 +114,10 @@ tr.clickable:hover td{background:var(--accent-bg);cursor:pointer}
114
  .step-title{font-size:14px;font-weight:500}.step-desc{font-size:12px;color:var(--text2);margin-top:2px;line-height:1.5}
115
  .count-lbl{font-size:12px;color:var(--text2);margin-bottom:8px}
116
  .empty{font-size:13px;color:var(--text2);padding:12px 0;text-align:center}
 
 
 
 
117
  </style>"""
118
 
119
  _SEND_PROMPT_BRIDGE = """<script>
@@ -224,12 +228,14 @@ const gc=dark?'rgba(255,255,255,0.06)':'rgba(0,0,0,0.06)';
224
  - sendPrompt text must be specific — never generic "tell me more"
225
 
226
  ## What not to do
 
227
  - No hardcoded colors
228
  - No sendPrompt on slider drag
229
  - No empty output on first render — always call calc() on load
230
  - No placeholder or lorem ipsum data — use real content only
231
  - No position:fixed
232
- - No external fonts or icon libraries"""
 
233
 
234
 
235
  # ══════════════════════════════════════════════════════════════════════════════
@@ -595,8 +601,8 @@ def estimate_widget_height(html: str) -> int:
595
  h = html.lower()
596
  height = 120
597
  height += h.count('<canvas') * 240
598
- height += h.count('<table') * 160
599
- height += min(h.count('<tr'), 15) * 36
600
  height += (h.count('class="card') + h.count("class='card")) * 80
601
  height += (h.count('class="raised') + h.count("class='raised")) * 100
602
  height += min(h.count('<li'), 12) * 30
@@ -607,7 +613,7 @@ def estimate_widget_height(html: str) -> int:
607
  height += h.count('result-box') * 90
608
  height += (h.count('class="tab') + h.count("class='tab")) * 10
609
 
610
- return max(280, min(height, 920))
611
 
612
 
613
  def build_local_calc_fallback(user_message: str, assistant_response: str) -> str:
 
58
  --border:rgba(255,255,255,0.07);--border2:rgba(255,255,255,0.14);
59
  --accent:#5ba4f5;--accent-bg:rgba(91,164,245,0.10);--accent-b:rgba(91,164,245,0.35);
60
  }}
61
+ html,body{margin:0!important;padding:12px 14px!important;width:100%;box-sizing:border-box;
62
  background:transparent!important;color:var(--text);
63
  font-family:"Segoe UI",system-ui,sans-serif;font-size:14px;line-height:1.6}
64
  @keyframes __fu{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:translateY(0)}}
65
+ .widget-root{animation:__fu .22s ease-out;width:100%;min-width:100%;box-sizing:border-box}
66
+ .card{background:var(--bg2);border:0.5px solid var(--border);border-radius:var(--radius);padding:14px 16px;margin-bottom:12px;transition:border-color .15s;width:100%;max-width:100%;box-sizing:border-box}
67
  .card:hover{border-color:var(--border2)}
68
  .card-title{font-size:11px;font-weight:500;color:var(--text2);text-transform:uppercase;letter-spacing:.05em;margin-bottom:12px}
69
  .raised{background:var(--bg);border:0.5px solid var(--border);border-radius:var(--radius);padding:14px;transition:border-color .15s,transform .1s;cursor:pointer}
 
76
  .tab:active{transform:scale(.985)}
77
  .tab.active{background:var(--bg3);color:var(--text);border-color:var(--accent)}
78
  .panel{display:none}.panel.active{display:block}
79
+ table{width:100%;min-width:100%;border-collapse:collapse;font-size:14px}
80
+ th{background:var(--bg3);color:var(--text2);font-weight:500;font-size:12px;text-transform:uppercase;letter-spacing:.04em;padding:10px 12px;text-align:left;border-bottom:0.5px solid var(--border2)}
81
+ td{padding:10px 12px;border-bottom:0.5px solid var(--border);color:var(--text);vertical-align:middle;font-size:14px}
82
  tr:last-child td{border-bottom:none}
83
  tr.clickable:hover td{background:var(--accent-bg);cursor:pointer}
84
  .search{width:100%;padding:8px 12px;border-radius:var(--radius-sm);border:0.5px solid var(--border2);background:var(--bg);color:var(--text);font-size:13px;outline:none;box-sizing:border-box;margin-bottom:10px;transition:border-color .15s}
 
114
  .step-title{font-size:14px;font-weight:500}.step-desc{font-size:12px;color:var(--text2);margin-top:2px;line-height:1.5}
115
  .count-lbl{font-size:12px;color:var(--text2);margin-bottom:8px}
116
  .empty{font-size:13px;color:var(--text2);padding:12px 0;text-align:center}
117
+ .card,.metric,.raised{transition:transform .22s cubic-bezier(0.22,1,0.36,1),box-shadow .25s ease,border-color .2s ease}
118
+ @media(hover:hover){.card:hover,.metric:hover{transform:translateY(-2px);box-shadow:0 8px 28px rgba(0,0,0,0.1)}}
119
+ @media(prefers-color-scheme:dark){@media(hover:hover){.card:hover,.metric:hover{box-shadow:0 8px 28px rgba(0,0,0,0.45)}}}
120
+ .widget-root{transition:opacity .28s ease}
121
  </style>"""
122
 
123
  _SEND_PROMPT_BRIDGE = """<script>
 
228
  - sendPrompt text must be specific — never generic "tell me more"
229
 
230
  ## What not to do
231
+ - No emojis or decorative symbols in labels or UI
232
  - No hardcoded colors
233
  - No sendPrompt on slider drag
234
  - No empty output on first render — always call calc() on load
235
  - No placeholder or lorem ipsum data — use real content only
236
  - No position:fixed
237
+ - No external fonts or icon libraries
238
+ - No decorative clutter — neat, minimal, professional design only"""
239
 
240
 
241
  # ══════════════════════════════════════════════════════════════════════════════
 
601
  h = html.lower()
602
  height = 120
603
  height += h.count('<canvas') * 240
604
+ height += h.count('<table') * 180
605
+ height += min(h.count('<tr'), 20) * 38
606
  height += (h.count('class="card') + h.count("class='card")) * 80
607
  height += (h.count('class="raised') + h.count("class='raised")) * 100
608
  height += min(h.count('<li'), 12) * 30
 
613
  height += h.count('result-box') * 90
614
  height += (h.count('class="tab') + h.count("class='tab")) * 10
615
 
616
+ return max(280, min(height, 1200))
617
 
618
 
619
  def build_local_calc_fallback(user_message: str, assistant_response: str) -> str:
vivek/backend/widget_stream.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Incremental parser for combined <RESPONSE>/<WIDGET> output.
2
+
3
+ Unlike the previous parser which buffered the entire widget body before
4
+ emitting anything, this yields widget deltas as they arrive — giving the
5
+ frontend Claude-style, token-by-token widget streaming.
6
+
7
+ Event types yielded
8
+ -------------------
9
+ - ("response_delta", str) : raw response tokens (inside <RESPONSE>)
10
+ - ("response_closed", str) : full response text when </RESPONSE> is seen
11
+ - ("widget_start", "") : first token of <WIDGET> opens
12
+ - ("widget_delta", str) : raw widget tokens (inside <WIDGET>)
13
+ - ("complete", response_text, widget_raw) : after </WIDGET> or EOF
14
+
15
+ Callers decide whether to forward response_delta (strict-primitive mode may
16
+ prefer to buffer until the full response is known before re-streaming it).
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from typing import Generator, Iterable, Tuple
22
+
23
+
24
+ _RESPONSE_OPEN = "<RESPONSE>"
25
+ _RESPONSE_CLOSE = "</RESPONSE>"
26
+ _WIDGET_OPEN = "<WIDGET>"
27
+ _WIDGET_CLOSE = "</WIDGET>"
28
+
29
+ # Keep enough buffer around tag-ends to avoid splitting a tag across emits.
30
+ _TAG_SAFETY = max(len(_RESPONSE_CLOSE), len(_WIDGET_CLOSE))
31
+
32
+
33
+ def parse_combined_stream(
34
+ chunks: Iterable[str],
35
+ *,
36
+ emit_response_deltas: bool = True,
37
+ emit_widget_deltas: bool = True,
38
+ ) -> Generator[Tuple[str, ...], None, None]:
39
+ """Yield structured events from an iterable of raw LLM chunks.
40
+
41
+ The function is a pure state machine: preamble -> response -> widget_pending ->
42
+ widget -> complete. Each chunk is appended to a rolling buffer and emitted
43
+ up to a safe boundary so we never emit half of a closing tag.
44
+ """
45
+ buf = ""
46
+ state = "preamble"
47
+ widget_started_emitted = False
48
+ response_text = ""
49
+ response_emitted_len = 0
50
+ widget_text = ""
51
+ widget_emitted_len = 0
52
+
53
+ def _find(needle: str, haystack_upper: str) -> int:
54
+ return haystack_upper.find(needle)
55
+
56
+ for chunk in chunks:
57
+ if not chunk:
58
+ continue
59
+ buf += chunk
60
+ buf_upper = buf.upper()
61
+
62
+ # --- Enter <RESPONSE> ---
63
+ if state == "preamble":
64
+ idx = _find(_RESPONSE_OPEN, buf_upper)
65
+ if idx != -1:
66
+ buf = buf[idx + len(_RESPONSE_OPEN):]
67
+ buf_upper = buf.upper()
68
+ state = "response"
69
+ response_emitted_len = 0
70
+
71
+ # --- Stream <RESPONSE> content until </RESPONSE> ---
72
+ if state == "response":
73
+ close_idx = _find(_RESPONSE_CLOSE, buf_upper)
74
+ if close_idx != -1:
75
+ to_send = buf[:close_idx][response_emitted_len:]
76
+ if to_send and emit_response_deltas:
77
+ yield ("response_delta", to_send)
78
+ response_text = buf[:close_idx].strip()
79
+ buf = buf[close_idx + len(_RESPONSE_CLOSE):]
80
+ buf_upper = buf.upper()
81
+ state = "widget_pending"
82
+ yield ("response_closed", response_text)
83
+ else:
84
+ safe_len = max(0, len(buf) - _TAG_SAFETY)
85
+ if safe_len > response_emitted_len:
86
+ delta = buf[response_emitted_len:safe_len]
87
+ if emit_response_deltas:
88
+ yield ("response_delta", delta)
89
+ response_emitted_len = safe_len
90
+
91
+ # --- Wait for <WIDGET> tag open ---
92
+ if state == "widget_pending":
93
+ idx = _find(_WIDGET_OPEN, buf_upper)
94
+ if idx != -1:
95
+ buf = buf[idx + len(_WIDGET_OPEN):]
96
+ buf_upper = buf.upper()
97
+ state = "widget"
98
+ widget_emitted_len = 0
99
+ if not widget_started_emitted:
100
+ widget_started_emitted = True
101
+ yield ("widget_start", "")
102
+
103
+ # --- Stream <WIDGET> content until </WIDGET> ---
104
+ if state == "widget":
105
+ close_idx = _find(_WIDGET_CLOSE, buf_upper)
106
+ if close_idx != -1:
107
+ to_send = buf[:close_idx][widget_emitted_len:]
108
+ if to_send and emit_widget_deltas:
109
+ yield ("widget_delta", to_send)
110
+ widget_text = buf[:close_idx].strip()
111
+ yield ("complete", response_text, widget_text)
112
+ return
113
+ safe_len = max(0, len(buf) - _TAG_SAFETY)
114
+ if safe_len > widget_emitted_len:
115
+ delta = buf[widget_emitted_len:safe_len]
116
+ if emit_widget_deltas:
117
+ yield ("widget_delta", delta)
118
+ widget_emitted_len = safe_len
119
+
120
+ # Stream ended without clean closure — salvage what we can.
121
+ if state == "response":
122
+ remaining = buf[response_emitted_len:]
123
+ if remaining and emit_response_deltas:
124
+ yield ("response_delta", remaining)
125
+ response_text = (response_text or buf).strip()
126
+ elif state == "widget":
127
+ remaining = buf[widget_emitted_len:]
128
+ if remaining and emit_widget_deltas:
129
+ yield ("widget_delta", remaining)
130
+ widget_text = (widget_text or buf).strip()
131
+ elif state == "preamble" and buf.strip():
132
+ # Model didn't use XML tags — treat the whole thing as the response.
133
+ response_text = buf.strip()
134
+ if emit_response_deltas:
135
+ yield ("response_delta", response_text)
136
+
137
+ yield ("complete", response_text, widget_text)
vivek/frontend-vue/.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
vivek/frontend-vue/README.md ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # Vue 3 + TypeScript + Vite
2
+
3
+ This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
4
+
5
+ Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
vivek/frontend-vue/components.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "style": "new-york",
3
+ "typescript": true,
4
+ "tailwind": {
5
+ "config": "tailwind.config.cjs",
6
+ "css": "src/styles/globals.css",
7
+ "baseColor": "neutral",
8
+ "cssVariables": true,
9
+ "prefix": ""
10
+ },
11
+ "aliases": {
12
+ "components": "@/components",
13
+ "composables": "@/composables",
14
+ "utils": "@/lib/utils",
15
+ "ui": "@/components/ui",
16
+ "lib": "@/lib"
17
+ },
18
+ "iconLibrary": "lucide"
19
+ }
20
+
vivek/frontend-vue/dist/assets/index-B2HHLelt.css ADDED
The diff for this file is too large to render. See raw diff
 
vivek/frontend-vue/dist/assets/index-Bq2Moqhb.js ADDED
The diff for this file is too large to render. See raw diff
 
vivek/frontend-vue/dist/favicon.svg ADDED
vivek/frontend-vue/dist/icons.svg ADDED
vivek/frontend-vue/dist/index.html ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>frontend-vue</title>
8
+ <script type="module" crossorigin src="/assets/index-Bq2Moqhb.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-B2HHLelt.css">
10
+ </head>
11
+ <body>
12
+ <div id="app"></div>
13
+ </body>
14
+ </html>
vivek/frontend-vue/index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>frontend-vue</title>
8
+ </head>
9
+ <body>
10
+ <div id="app"></div>
11
+ <script type="module" src="/src/main.ts"></script>
12
+ </body>
13
+ </html>
vivek/frontend-vue/package-lock.json ADDED
@@ -0,0 +1,2537 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend-vue",
3
+ "version": "0.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "frontend-vue",
9
+ "version": "0.0.0",
10
+ "dependencies": {
11
+ "@heroicons/vue": "^2.2.0",
12
+ "@motionone/vue": "^10.16.4",
13
+ "@tailwindcss/vite": "^4.2.2",
14
+ "class-variance-authority": "^0.7.1",
15
+ "clsx": "^2.1.1",
16
+ "dompurify": "^3.3.3",
17
+ "echarts": "^6.0.0",
18
+ "gsap": "^3.14.2",
19
+ "lucide-vue-next": "^1.0.0",
20
+ "markdown-it": "^14.1.1",
21
+ "markdown-it-multimd-table": "^4.2.3",
22
+ "plotly.js-dist-min": "^3.4.0",
23
+ "radix-vue": "^1.9.17",
24
+ "tailwind-merge": "^3.5.0",
25
+ "tailwindcss": "^4.2.2",
26
+ "tw-animate-css": "^1.4.0",
27
+ "vue": "^3.5.30",
28
+ "vue-router": "^5.0.4",
29
+ "zod": "^4.3.6"
30
+ },
31
+ "devDependencies": {
32
+ "@types/markdown-it": "^14.1.2",
33
+ "@types/node": "^24.12.0",
34
+ "@vitejs/plugin-vue": "^6.0.5",
35
+ "@vue/tsconfig": "^0.9.0",
36
+ "typescript": "~5.9.3",
37
+ "vite": "^8.0.1",
38
+ "vue-tsc": "^3.2.5"
39
+ }
40
+ },
41
+ "node_modules/@babel/generator": {
42
+ "version": "7.29.1",
43
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
44
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
45
+ "license": "MIT",
46
+ "dependencies": {
47
+ "@babel/parser": "^7.29.0",
48
+ "@babel/types": "^7.29.0",
49
+ "@jridgewell/gen-mapping": "^0.3.12",
50
+ "@jridgewell/trace-mapping": "^0.3.28",
51
+ "jsesc": "^3.0.2"
52
+ },
53
+ "engines": {
54
+ "node": ">=6.9.0"
55
+ }
56
+ },
57
+ "node_modules/@babel/helper-string-parser": {
58
+ "version": "7.27.1",
59
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
60
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
61
+ "license": "MIT",
62
+ "engines": {
63
+ "node": ">=6.9.0"
64
+ }
65
+ },
66
+ "node_modules/@babel/helper-validator-identifier": {
67
+ "version": "7.28.5",
68
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
69
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
70
+ "license": "MIT",
71
+ "engines": {
72
+ "node": ">=6.9.0"
73
+ }
74
+ },
75
+ "node_modules/@babel/parser": {
76
+ "version": "7.29.2",
77
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
78
+ "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
79
+ "license": "MIT",
80
+ "dependencies": {
81
+ "@babel/types": "^7.29.0"
82
+ },
83
+ "bin": {
84
+ "parser": "bin/babel-parser.js"
85
+ },
86
+ "engines": {
87
+ "node": ">=6.0.0"
88
+ }
89
+ },
90
+ "node_modules/@babel/types": {
91
+ "version": "7.29.0",
92
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
93
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
94
+ "license": "MIT",
95
+ "dependencies": {
96
+ "@babel/helper-string-parser": "^7.27.1",
97
+ "@babel/helper-validator-identifier": "^7.28.5"
98
+ },
99
+ "engines": {
100
+ "node": ">=6.9.0"
101
+ }
102
+ },
103
+ "node_modules/@emnapi/core": {
104
+ "version": "1.9.1",
105
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz",
106
+ "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==",
107
+ "license": "MIT",
108
+ "optional": true,
109
+ "dependencies": {
110
+ "@emnapi/wasi-threads": "1.2.0",
111
+ "tslib": "^2.4.0"
112
+ }
113
+ },
114
+ "node_modules/@emnapi/runtime": {
115
+ "version": "1.9.1",
116
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz",
117
+ "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==",
118
+ "license": "MIT",
119
+ "optional": true,
120
+ "dependencies": {
121
+ "tslib": "^2.4.0"
122
+ }
123
+ },
124
+ "node_modules/@emnapi/wasi-threads": {
125
+ "version": "1.2.0",
126
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
127
+ "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
128
+ "license": "MIT",
129
+ "optional": true,
130
+ "dependencies": {
131
+ "tslib": "^2.4.0"
132
+ }
133
+ },
134
+ "node_modules/@floating-ui/core": {
135
+ "version": "1.7.5",
136
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
137
+ "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
138
+ "license": "MIT",
139
+ "dependencies": {
140
+ "@floating-ui/utils": "^0.2.11"
141
+ }
142
+ },
143
+ "node_modules/@floating-ui/dom": {
144
+ "version": "1.7.6",
145
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
146
+ "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
147
+ "license": "MIT",
148
+ "dependencies": {
149
+ "@floating-ui/core": "^1.7.5",
150
+ "@floating-ui/utils": "^0.2.11"
151
+ }
152
+ },
153
+ "node_modules/@floating-ui/utils": {
154
+ "version": "0.2.11",
155
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
156
+ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
157
+ "license": "MIT"
158
+ },
159
+ "node_modules/@floating-ui/vue": {
160
+ "version": "1.1.11",
161
+ "resolved": "https://registry.npmjs.org/@floating-ui/vue/-/vue-1.1.11.tgz",
162
+ "integrity": "sha512-HzHKCNVxnGS35r9fCHBc3+uCnjw9IWIlCPL683cGgM9Kgj2BiAl8x1mS7vtvP6F9S/e/q4O6MApwSHj8hNLGfw==",
163
+ "license": "MIT",
164
+ "dependencies": {
165
+ "@floating-ui/dom": "^1.7.6",
166
+ "@floating-ui/utils": "^0.2.11",
167
+ "vue-demi": ">=0.13.0"
168
+ }
169
+ },
170
+ "node_modules/@floating-ui/vue/node_modules/vue-demi": {
171
+ "version": "0.14.10",
172
+ "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
173
+ "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
174
+ "hasInstallScript": true,
175
+ "license": "MIT",
176
+ "bin": {
177
+ "vue-demi-fix": "bin/vue-demi-fix.js",
178
+ "vue-demi-switch": "bin/vue-demi-switch.js"
179
+ },
180
+ "engines": {
181
+ "node": ">=12"
182
+ },
183
+ "funding": {
184
+ "url": "https://github.com/sponsors/antfu"
185
+ },
186
+ "peerDependencies": {
187
+ "@vue/composition-api": "^1.0.0-rc.1",
188
+ "vue": "^3.0.0-0 || ^2.6.0"
189
+ },
190
+ "peerDependenciesMeta": {
191
+ "@vue/composition-api": {
192
+ "optional": true
193
+ }
194
+ }
195
+ },
196
+ "node_modules/@heroicons/vue": {
197
+ "version": "2.2.0",
198
+ "resolved": "https://registry.npmjs.org/@heroicons/vue/-/vue-2.2.0.tgz",
199
+ "integrity": "sha512-G3dbSxoeEKqbi/DFalhRxJU4mTXJn7GwZ7ae8NuEQzd1bqdd0jAbdaBZlHPcvPD2xI1iGzNVB4k20Un2AguYPw==",
200
+ "license": "MIT",
201
+ "peerDependencies": {
202
+ "vue": ">= 3"
203
+ }
204
+ },
205
+ "node_modules/@internationalized/date": {
206
+ "version": "3.12.0",
207
+ "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz",
208
+ "integrity": "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==",
209
+ "license": "Apache-2.0",
210
+ "dependencies": {
211
+ "@swc/helpers": "^0.5.0"
212
+ }
213
+ },
214
+ "node_modules/@internationalized/number": {
215
+ "version": "3.6.5",
216
+ "resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.5.tgz",
217
+ "integrity": "sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==",
218
+ "license": "Apache-2.0",
219
+ "dependencies": {
220
+ "@swc/helpers": "^0.5.0"
221
+ }
222
+ },
223
+ "node_modules/@jridgewell/gen-mapping": {
224
+ "version": "0.3.13",
225
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
226
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
227
+ "license": "MIT",
228
+ "dependencies": {
229
+ "@jridgewell/sourcemap-codec": "^1.5.0",
230
+ "@jridgewell/trace-mapping": "^0.3.24"
231
+ }
232
+ },
233
+ "node_modules/@jridgewell/remapping": {
234
+ "version": "2.3.5",
235
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
236
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
237
+ "license": "MIT",
238
+ "dependencies": {
239
+ "@jridgewell/gen-mapping": "^0.3.5",
240
+ "@jridgewell/trace-mapping": "^0.3.24"
241
+ }
242
+ },
243
+ "node_modules/@jridgewell/resolve-uri": {
244
+ "version": "3.1.2",
245
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
246
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
247
+ "license": "MIT",
248
+ "engines": {
249
+ "node": ">=6.0.0"
250
+ }
251
+ },
252
+ "node_modules/@jridgewell/sourcemap-codec": {
253
+ "version": "1.5.5",
254
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
255
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
256
+ "license": "MIT"
257
+ },
258
+ "node_modules/@jridgewell/trace-mapping": {
259
+ "version": "0.3.31",
260
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
261
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
262
+ "license": "MIT",
263
+ "dependencies": {
264
+ "@jridgewell/resolve-uri": "^3.1.0",
265
+ "@jridgewell/sourcemap-codec": "^1.4.14"
266
+ }
267
+ },
268
+ "node_modules/@motionone/animation": {
269
+ "version": "10.18.0",
270
+ "resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.18.0.tgz",
271
+ "integrity": "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw==",
272
+ "license": "MIT",
273
+ "dependencies": {
274
+ "@motionone/easing": "^10.18.0",
275
+ "@motionone/types": "^10.17.1",
276
+ "@motionone/utils": "^10.18.0",
277
+ "tslib": "^2.3.1"
278
+ }
279
+ },
280
+ "node_modules/@motionone/dom": {
281
+ "version": "10.18.0",
282
+ "resolved": "https://registry.npmjs.org/@motionone/dom/-/dom-10.18.0.tgz",
283
+ "integrity": "sha512-bKLP7E0eyO4B2UaHBBN55tnppwRnaE3KFfh3Ps9HhnAkar3Cb69kUCJY9as8LrccVYKgHA+JY5dOQqJLOPhF5A==",
284
+ "license": "MIT",
285
+ "dependencies": {
286
+ "@motionone/animation": "^10.18.0",
287
+ "@motionone/generators": "^10.18.0",
288
+ "@motionone/types": "^10.17.1",
289
+ "@motionone/utils": "^10.18.0",
290
+ "hey-listen": "^1.0.8",
291
+ "tslib": "^2.3.1"
292
+ }
293
+ },
294
+ "node_modules/@motionone/easing": {
295
+ "version": "10.18.0",
296
+ "resolved": "https://registry.npmjs.org/@motionone/easing/-/easing-10.18.0.tgz",
297
+ "integrity": "sha512-VcjByo7XpdLS4o9T8t99JtgxkdMcNWD3yHU/n6CLEz3bkmKDRZyYQ/wmSf6daum8ZXqfUAgFeCZSpJZIMxaCzg==",
298
+ "license": "MIT",
299
+ "dependencies": {
300
+ "@motionone/utils": "^10.18.0",
301
+ "tslib": "^2.3.1"
302
+ }
303
+ },
304
+ "node_modules/@motionone/generators": {
305
+ "version": "10.18.0",
306
+ "resolved": "https://registry.npmjs.org/@motionone/generators/-/generators-10.18.0.tgz",
307
+ "integrity": "sha512-+qfkC2DtkDj4tHPu+AFKVfR/C30O1vYdvsGYaR13W/1cczPrrcjdvYCj0VLFuRMN+lP1xvpNZHCRNM4fBzn1jg==",
308
+ "license": "MIT",
309
+ "dependencies": {
310
+ "@motionone/types": "^10.17.1",
311
+ "@motionone/utils": "^10.18.0",
312
+ "tslib": "^2.3.1"
313
+ }
314
+ },
315
+ "node_modules/@motionone/types": {
316
+ "version": "10.17.1",
317
+ "resolved": "https://registry.npmjs.org/@motionone/types/-/types-10.17.1.tgz",
318
+ "integrity": "sha512-KaC4kgiODDz8hswCrS0btrVrzyU2CSQKO7Ps90ibBVSQmjkrt2teqta6/sOG59v7+dPnKMAg13jyqtMKV2yJ7A==",
319
+ "license": "MIT"
320
+ },
321
+ "node_modules/@motionone/utils": {
322
+ "version": "10.18.0",
323
+ "resolved": "https://registry.npmjs.org/@motionone/utils/-/utils-10.18.0.tgz",
324
+ "integrity": "sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw==",
325
+ "license": "MIT",
326
+ "dependencies": {
327
+ "@motionone/types": "^10.17.1",
328
+ "hey-listen": "^1.0.8",
329
+ "tslib": "^2.3.1"
330
+ }
331
+ },
332
+ "node_modules/@motionone/vue": {
333
+ "version": "10.16.4",
334
+ "resolved": "https://registry.npmjs.org/@motionone/vue/-/vue-10.16.4.tgz",
335
+ "integrity": "sha512-z10PF9JV6SbjFq+/rYabM+8CVlMokgl8RFGvieSGNTmrkQanfHn+15XBrhG3BgUfvmTeSeyShfOHpG0i9zEdcg==",
336
+ "deprecated": "Motion One for Vue is deprecated. Use Oku Motion instead https://oku-ui.com/motion",
337
+ "license": "MIT",
338
+ "dependencies": {
339
+ "@motionone/dom": "^10.16.4",
340
+ "tslib": "^2.3.1"
341
+ }
342
+ },
343
+ "node_modules/@napi-rs/wasm-runtime": {
344
+ "version": "1.1.1",
345
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
346
+ "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
347
+ "license": "MIT",
348
+ "optional": true,
349
+ "dependencies": {
350
+ "@emnapi/core": "^1.7.1",
351
+ "@emnapi/runtime": "^1.7.1",
352
+ "@tybys/wasm-util": "^0.10.1"
353
+ },
354
+ "funding": {
355
+ "type": "github",
356
+ "url": "https://github.com/sponsors/Brooooooklyn"
357
+ }
358
+ },
359
+ "node_modules/@oxc-project/types": {
360
+ "version": "0.122.0",
361
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz",
362
+ "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==",
363
+ "license": "MIT",
364
+ "funding": {
365
+ "url": "https://github.com/sponsors/Boshen"
366
+ }
367
+ },
368
+ "node_modules/@rolldown/binding-android-arm64": {
369
+ "version": "1.0.0-rc.11",
370
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.11.tgz",
371
+ "integrity": "sha512-SJ+/g+xNnOh6NqYxD0V3uVN4W3VfnrGsC9/hoglicgTNfABFG9JjISvkkU0dNY84MNHLWyOgxP9v9Y9pX4S7+A==",
372
+ "cpu": [
373
+ "arm64"
374
+ ],
375
+ "license": "MIT",
376
+ "optional": true,
377
+ "os": [
378
+ "android"
379
+ ],
380
+ "engines": {
381
+ "node": "^20.19.0 || >=22.12.0"
382
+ }
383
+ },
384
+ "node_modules/@rolldown/binding-darwin-arm64": {
385
+ "version": "1.0.0-rc.11",
386
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.11.tgz",
387
+ "integrity": "sha512-7WQgR8SfOPwmDZGFkThUvsmd/nwAWv91oCO4I5LS7RKrssPZmOt7jONN0cW17ydGC1n/+puol1IpoieKqQidmg==",
388
+ "cpu": [
389
+ "arm64"
390
+ ],
391
+ "license": "MIT",
392
+ "optional": true,
393
+ "os": [
394
+ "darwin"
395
+ ],
396
+ "engines": {
397
+ "node": "^20.19.0 || >=22.12.0"
398
+ }
399
+ },
400
+ "node_modules/@rolldown/binding-darwin-x64": {
401
+ "version": "1.0.0-rc.11",
402
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.11.tgz",
403
+ "integrity": "sha512-39Ks6UvIHq4rEogIfQBoBRusj0Q0nPVWIvqmwBLaT6aqQGIakHdESBVOPRRLacy4WwUPIx4ZKzfZ9PMW+IeyUQ==",
404
+ "cpu": [
405
+ "x64"
406
+ ],
407
+ "license": "MIT",
408
+ "optional": true,
409
+ "os": [
410
+ "darwin"
411
+ ],
412
+ "engines": {
413
+ "node": "^20.19.0 || >=22.12.0"
414
+ }
415
+ },
416
+ "node_modules/@rolldown/binding-freebsd-x64": {
417
+ "version": "1.0.0-rc.11",
418
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.11.tgz",
419
+ "integrity": "sha512-jfsm0ZHfhiqrvWjJAmzsqiIFPz5e7mAoCOPBNTcNgkiid/LaFKiq92+0ojH+nmJmKYkre4t71BWXUZDNp7vsag==",
420
+ "cpu": [
421
+ "x64"
422
+ ],
423
+ "license": "MIT",
424
+ "optional": true,
425
+ "os": [
426
+ "freebsd"
427
+ ],
428
+ "engines": {
429
+ "node": "^20.19.0 || >=22.12.0"
430
+ }
431
+ },
432
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
433
+ "version": "1.0.0-rc.11",
434
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.11.tgz",
435
+ "integrity": "sha512-zjQaUtSyq1nVe3nxmlSCuR96T1LPlpvmJ0SZy0WJFEsV4kFbXcq2u68L4E6O0XeFj4aex9bEauqjW8UQBeAvfQ==",
436
+ "cpu": [
437
+ "arm"
438
+ ],
439
+ "license": "MIT",
440
+ "optional": true,
441
+ "os": [
442
+ "linux"
443
+ ],
444
+ "engines": {
445
+ "node": "^20.19.0 || >=22.12.0"
446
+ }
447
+ },
448
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
449
+ "version": "1.0.0-rc.11",
450
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.11.tgz",
451
+ "integrity": "sha512-WMW1yE6IOnehTcFE9eipFkm3XN63zypWlrJQ2iF7NrQ9b2LDRjumFoOGJE8RJJTJCTBAdmLMnJ8uVitACUUo1Q==",
452
+ "cpu": [
453
+ "arm64"
454
+ ],
455
+ "license": "MIT",
456
+ "optional": true,
457
+ "os": [
458
+ "linux"
459
+ ],
460
+ "engines": {
461
+ "node": "^20.19.0 || >=22.12.0"
462
+ }
463
+ },
464
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
465
+ "version": "1.0.0-rc.11",
466
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.11.tgz",
467
+ "integrity": "sha512-jfndI9tsfm4APzjNt6QdBkYwre5lRPUgHeDHoI7ydKUuJvz3lZeCfMsI56BZj+7BYqiKsJm7cfd/6KYV7ubrBg==",
468
+ "cpu": [
469
+ "arm64"
470
+ ],
471
+ "license": "MIT",
472
+ "optional": true,
473
+ "os": [
474
+ "linux"
475
+ ],
476
+ "engines": {
477
+ "node": "^20.19.0 || >=22.12.0"
478
+ }
479
+ },
480
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
481
+ "version": "1.0.0-rc.11",
482
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.11.tgz",
483
+ "integrity": "sha512-ZlFgw46NOAGMgcdvdYwAGu2Q+SLFA9LzbJLW+iyMOJyhj5wk6P3KEE9Gct4xWwSzFoPI7JCdYmYMzVtlgQ+zfw==",
484
+ "cpu": [
485
+ "ppc64"
486
+ ],
487
+ "license": "MIT",
488
+ "optional": true,
489
+ "os": [
490
+ "linux"
491
+ ],
492
+ "engines": {
493
+ "node": "^20.19.0 || >=22.12.0"
494
+ }
495
+ },
496
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
497
+ "version": "1.0.0-rc.11",
498
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.11.tgz",
499
+ "integrity": "sha512-hIOYmuT6ofM4K04XAZd3OzMySEO4K0/nc9+jmNcxNAxRi6c5UWpqfw3KMFV4MVFWL+jQsSh+bGw2VqmaPMTLyw==",
500
+ "cpu": [
501
+ "s390x"
502
+ ],
503
+ "license": "MIT",
504
+ "optional": true,
505
+ "os": [
506
+ "linux"
507
+ ],
508
+ "engines": {
509
+ "node": "^20.19.0 || >=22.12.0"
510
+ }
511
+ },
512
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
513
+ "version": "1.0.0-rc.11",
514
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.11.tgz",
515
+ "integrity": "sha512-qXBQQO9OvkjjQPLdUVr7Nr2t3QTZI7s4KZtfw7HzBgjbmAPSFwSv4rmET9lLSgq3rH/ndA3ngv3Qb8l2njoPNA==",
516
+ "cpu": [
517
+ "x64"
518
+ ],
519
+ "license": "MIT",
520
+ "optional": true,
521
+ "os": [
522
+ "linux"
523
+ ],
524
+ "engines": {
525
+ "node": "^20.19.0 || >=22.12.0"
526
+ }
527
+ },
528
+ "node_modules/@rolldown/binding-linux-x64-musl": {
529
+ "version": "1.0.0-rc.11",
530
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.11.tgz",
531
+ "integrity": "sha512-/tpFfoSTzUkH9LPY+cYbqZBDyyX62w5fICq9qzsHLL8uTI6BHip3Q9Uzft0wylk/i8OOwKik8OxW+QAhDmzwmg==",
532
+ "cpu": [
533
+ "x64"
534
+ ],
535
+ "license": "MIT",
536
+ "optional": true,
537
+ "os": [
538
+ "linux"
539
+ ],
540
+ "engines": {
541
+ "node": "^20.19.0 || >=22.12.0"
542
+ }
543
+ },
544
+ "node_modules/@rolldown/binding-openharmony-arm64": {
545
+ "version": "1.0.0-rc.11",
546
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.11.tgz",
547
+ "integrity": "sha512-mcp3Rio2w72IvdZG0oQ4bM2c2oumtwHfUfKncUM6zGgz0KgPz4YmDPQfnXEiY5t3+KD/i8HG2rOB/LxdmieK2g==",
548
+ "cpu": [
549
+ "arm64"
550
+ ],
551
+ "license": "MIT",
552
+ "optional": true,
553
+ "os": [
554
+ "openharmony"
555
+ ],
556
+ "engines": {
557
+ "node": "^20.19.0 || >=22.12.0"
558
+ }
559
+ },
560
+ "node_modules/@rolldown/binding-wasm32-wasi": {
561
+ "version": "1.0.0-rc.11",
562
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.11.tgz",
563
+ "integrity": "sha512-LXk5Hii1Ph9asuGRjBuz8TUxdc1lWzB7nyfdoRgI0WGPZKmCxvlKk8KfYysqtr4MfGElu/f/pEQRh8fcEgkrWw==",
564
+ "cpu": [
565
+ "wasm32"
566
+ ],
567
+ "license": "MIT",
568
+ "optional": true,
569
+ "dependencies": {
570
+ "@napi-rs/wasm-runtime": "^1.1.1"
571
+ },
572
+ "engines": {
573
+ "node": ">=14.0.0"
574
+ }
575
+ },
576
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
577
+ "version": "1.0.0-rc.11",
578
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.11.tgz",
579
+ "integrity": "sha512-dDwf5otnx0XgRY1yqxOC4ITizcdzS/8cQ3goOWv3jFAo4F+xQYni+hnMuO6+LssHHdJW7+OCVL3CoU4ycnh35Q==",
580
+ "cpu": [
581
+ "arm64"
582
+ ],
583
+ "license": "MIT",
584
+ "optional": true,
585
+ "os": [
586
+ "win32"
587
+ ],
588
+ "engines": {
589
+ "node": "^20.19.0 || >=22.12.0"
590
+ }
591
+ },
592
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
593
+ "version": "1.0.0-rc.11",
594
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.11.tgz",
595
+ "integrity": "sha512-LN4/skhSggybX71ews7dAj6r2geaMJfm3kMbK2KhFMg9B10AZXnKoLCVVgzhMHL0S+aKtr4p8QbAW8k+w95bAA==",
596
+ "cpu": [
597
+ "x64"
598
+ ],
599
+ "license": "MIT",
600
+ "optional": true,
601
+ "os": [
602
+ "win32"
603
+ ],
604
+ "engines": {
605
+ "node": "^20.19.0 || >=22.12.0"
606
+ }
607
+ },
608
+ "node_modules/@rolldown/pluginutils": {
609
+ "version": "1.0.0-rc.2",
610
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz",
611
+ "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==",
612
+ "dev": true,
613
+ "license": "MIT"
614
+ },
615
+ "node_modules/@swc/helpers": {
616
+ "version": "0.5.20",
617
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.20.tgz",
618
+ "integrity": "sha512-2egEBHUMasdypIzrprsu8g+OEVd7Vp2MM3a2eVlM/cyFYto0nGz5BX5BTgh/ShZZI9ed+ozEq+Ngt+rgmUs8tw==",
619
+ "license": "Apache-2.0",
620
+ "dependencies": {
621
+ "tslib": "^2.8.0"
622
+ }
623
+ },
624
+ "node_modules/@tailwindcss/node": {
625
+ "version": "4.2.2",
626
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz",
627
+ "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==",
628
+ "license": "MIT",
629
+ "dependencies": {
630
+ "@jridgewell/remapping": "^2.3.5",
631
+ "enhanced-resolve": "^5.19.0",
632
+ "jiti": "^2.6.1",
633
+ "lightningcss": "1.32.0",
634
+ "magic-string": "^0.30.21",
635
+ "source-map-js": "^1.2.1",
636
+ "tailwindcss": "4.2.2"
637
+ }
638
+ },
639
+ "node_modules/@tailwindcss/oxide": {
640
+ "version": "4.2.2",
641
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz",
642
+ "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==",
643
+ "license": "MIT",
644
+ "engines": {
645
+ "node": ">= 20"
646
+ },
647
+ "optionalDependencies": {
648
+ "@tailwindcss/oxide-android-arm64": "4.2.2",
649
+ "@tailwindcss/oxide-darwin-arm64": "4.2.2",
650
+ "@tailwindcss/oxide-darwin-x64": "4.2.2",
651
+ "@tailwindcss/oxide-freebsd-x64": "4.2.2",
652
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2",
653
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2",
654
+ "@tailwindcss/oxide-linux-arm64-musl": "4.2.2",
655
+ "@tailwindcss/oxide-linux-x64-gnu": "4.2.2",
656
+ "@tailwindcss/oxide-linux-x64-musl": "4.2.2",
657
+ "@tailwindcss/oxide-wasm32-wasi": "4.2.2",
658
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2",
659
+ "@tailwindcss/oxide-win32-x64-msvc": "4.2.2"
660
+ }
661
+ },
662
+ "node_modules/@tailwindcss/oxide-android-arm64": {
663
+ "version": "4.2.2",
664
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz",
665
+ "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==",
666
+ "cpu": [
667
+ "arm64"
668
+ ],
669
+ "license": "MIT",
670
+ "optional": true,
671
+ "os": [
672
+ "android"
673
+ ],
674
+ "engines": {
675
+ "node": ">= 20"
676
+ }
677
+ },
678
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
679
+ "version": "4.2.2",
680
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz",
681
+ "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==",
682
+ "cpu": [
683
+ "arm64"
684
+ ],
685
+ "license": "MIT",
686
+ "optional": true,
687
+ "os": [
688
+ "darwin"
689
+ ],
690
+ "engines": {
691
+ "node": ">= 20"
692
+ }
693
+ },
694
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
695
+ "version": "4.2.2",
696
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz",
697
+ "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==",
698
+ "cpu": [
699
+ "x64"
700
+ ],
701
+ "license": "MIT",
702
+ "optional": true,
703
+ "os": [
704
+ "darwin"
705
+ ],
706
+ "engines": {
707
+ "node": ">= 20"
708
+ }
709
+ },
710
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
711
+ "version": "4.2.2",
712
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz",
713
+ "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==",
714
+ "cpu": [
715
+ "x64"
716
+ ],
717
+ "license": "MIT",
718
+ "optional": true,
719
+ "os": [
720
+ "freebsd"
721
+ ],
722
+ "engines": {
723
+ "node": ">= 20"
724
+ }
725
+ },
726
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
727
+ "version": "4.2.2",
728
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz",
729
+ "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==",
730
+ "cpu": [
731
+ "arm"
732
+ ],
733
+ "license": "MIT",
734
+ "optional": true,
735
+ "os": [
736
+ "linux"
737
+ ],
738
+ "engines": {
739
+ "node": ">= 20"
740
+ }
741
+ },
742
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
743
+ "version": "4.2.2",
744
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz",
745
+ "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==",
746
+ "cpu": [
747
+ "arm64"
748
+ ],
749
+ "license": "MIT",
750
+ "optional": true,
751
+ "os": [
752
+ "linux"
753
+ ],
754
+ "engines": {
755
+ "node": ">= 20"
756
+ }
757
+ },
758
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
759
+ "version": "4.2.2",
760
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz",
761
+ "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==",
762
+ "cpu": [
763
+ "arm64"
764
+ ],
765
+ "license": "MIT",
766
+ "optional": true,
767
+ "os": [
768
+ "linux"
769
+ ],
770
+ "engines": {
771
+ "node": ">= 20"
772
+ }
773
+ },
774
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
775
+ "version": "4.2.2",
776
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz",
777
+ "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==",
778
+ "cpu": [
779
+ "x64"
780
+ ],
781
+ "license": "MIT",
782
+ "optional": true,
783
+ "os": [
784
+ "linux"
785
+ ],
786
+ "engines": {
787
+ "node": ">= 20"
788
+ }
789
+ },
790
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
791
+ "version": "4.2.2",
792
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz",
793
+ "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==",
794
+ "cpu": [
795
+ "x64"
796
+ ],
797
+ "license": "MIT",
798
+ "optional": true,
799
+ "os": [
800
+ "linux"
801
+ ],
802
+ "engines": {
803
+ "node": ">= 20"
804
+ }
805
+ },
806
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
807
+ "version": "4.2.2",
808
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz",
809
+ "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==",
810
+ "bundleDependencies": [
811
+ "@napi-rs/wasm-runtime",
812
+ "@emnapi/core",
813
+ "@emnapi/runtime",
814
+ "@tybys/wasm-util",
815
+ "@emnapi/wasi-threads",
816
+ "tslib"
817
+ ],
818
+ "cpu": [
819
+ "wasm32"
820
+ ],
821
+ "license": "MIT",
822
+ "optional": true,
823
+ "dependencies": {
824
+ "@emnapi/core": "^1.8.1",
825
+ "@emnapi/runtime": "^1.8.1",
826
+ "@emnapi/wasi-threads": "^1.1.0",
827
+ "@napi-rs/wasm-runtime": "^1.1.1",
828
+ "@tybys/wasm-util": "^0.10.1",
829
+ "tslib": "^2.8.1"
830
+ },
831
+ "engines": {
832
+ "node": ">=14.0.0"
833
+ }
834
+ },
835
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
836
+ "version": "4.2.2",
837
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz",
838
+ "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==",
839
+ "cpu": [
840
+ "arm64"
841
+ ],
842
+ "license": "MIT",
843
+ "optional": true,
844
+ "os": [
845
+ "win32"
846
+ ],
847
+ "engines": {
848
+ "node": ">= 20"
849
+ }
850
+ },
851
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
852
+ "version": "4.2.2",
853
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz",
854
+ "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==",
855
+ "cpu": [
856
+ "x64"
857
+ ],
858
+ "license": "MIT",
859
+ "optional": true,
860
+ "os": [
861
+ "win32"
862
+ ],
863
+ "engines": {
864
+ "node": ">= 20"
865
+ }
866
+ },
867
+ "node_modules/@tailwindcss/vite": {
868
+ "version": "4.2.2",
869
+ "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz",
870
+ "integrity": "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==",
871
+ "license": "MIT",
872
+ "dependencies": {
873
+ "@tailwindcss/node": "4.2.2",
874
+ "@tailwindcss/oxide": "4.2.2",
875
+ "tailwindcss": "4.2.2"
876
+ },
877
+ "peerDependencies": {
878
+ "vite": "^5.2.0 || ^6 || ^7 || ^8"
879
+ }
880
+ },
881
+ "node_modules/@tanstack/virtual-core": {
882
+ "version": "3.13.23",
883
+ "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.23.tgz",
884
+ "integrity": "sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg==",
885
+ "license": "MIT",
886
+ "funding": {
887
+ "type": "github",
888
+ "url": "https://github.com/sponsors/tannerlinsley"
889
+ }
890
+ },
891
+ "node_modules/@tanstack/vue-virtual": {
892
+ "version": "3.13.23",
893
+ "resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.13.23.tgz",
894
+ "integrity": "sha512-b5jPluAR6U3eOq6GWAYSpj3ugnAIZgGR0e6aGAgyRse0Yu6MVQQ0ZWm9SArSXWtageogn6bkVD8D//c4IjW3xQ==",
895
+ "license": "MIT",
896
+ "dependencies": {
897
+ "@tanstack/virtual-core": "3.13.23"
898
+ },
899
+ "funding": {
900
+ "type": "github",
901
+ "url": "https://github.com/sponsors/tannerlinsley"
902
+ },
903
+ "peerDependencies": {
904
+ "vue": "^2.7.0 || ^3.0.0"
905
+ }
906
+ },
907
+ "node_modules/@tybys/wasm-util": {
908
+ "version": "0.10.1",
909
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
910
+ "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
911
+ "license": "MIT",
912
+ "optional": true,
913
+ "dependencies": {
914
+ "tslib": "^2.4.0"
915
+ }
916
+ },
917
+ "node_modules/@types/linkify-it": {
918
+ "version": "5.0.0",
919
+ "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
920
+ "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
921
+ "dev": true,
922
+ "license": "MIT"
923
+ },
924
+ "node_modules/@types/markdown-it": {
925
+ "version": "14.1.2",
926
+ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
927
+ "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
928
+ "dev": true,
929
+ "license": "MIT",
930
+ "dependencies": {
931
+ "@types/linkify-it": "^5",
932
+ "@types/mdurl": "^2"
933
+ }
934
+ },
935
+ "node_modules/@types/mdurl": {
936
+ "version": "2.0.0",
937
+ "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
938
+ "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
939
+ "dev": true,
940
+ "license": "MIT"
941
+ },
942
+ "node_modules/@types/node": {
943
+ "version": "24.12.0",
944
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz",
945
+ "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==",
946
+ "devOptional": true,
947
+ "license": "MIT",
948
+ "dependencies": {
949
+ "undici-types": "~7.16.0"
950
+ }
951
+ },
952
+ "node_modules/@types/trusted-types": {
953
+ "version": "2.0.7",
954
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
955
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
956
+ "license": "MIT",
957
+ "optional": true
958
+ },
959
+ "node_modules/@types/web-bluetooth": {
960
+ "version": "0.0.20",
961
+ "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
962
+ "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
963
+ "license": "MIT"
964
+ },
965
+ "node_modules/@vitejs/plugin-vue": {
966
+ "version": "6.0.5",
967
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.5.tgz",
968
+ "integrity": "sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg==",
969
+ "dev": true,
970
+ "license": "MIT",
971
+ "dependencies": {
972
+ "@rolldown/pluginutils": "1.0.0-rc.2"
973
+ },
974
+ "engines": {
975
+ "node": "^20.19.0 || >=22.12.0"
976
+ },
977
+ "peerDependencies": {
978
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
979
+ "vue": "^3.2.25"
980
+ }
981
+ },
982
+ "node_modules/@volar/language-core": {
983
+ "version": "2.4.28",
984
+ "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz",
985
+ "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==",
986
+ "dev": true,
987
+ "license": "MIT",
988
+ "dependencies": {
989
+ "@volar/source-map": "2.4.28"
990
+ }
991
+ },
992
+ "node_modules/@volar/source-map": {
993
+ "version": "2.4.28",
994
+ "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz",
995
+ "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==",
996
+ "dev": true,
997
+ "license": "MIT"
998
+ },
999
+ "node_modules/@volar/typescript": {
1000
+ "version": "2.4.28",
1001
+ "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz",
1002
+ "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==",
1003
+ "dev": true,
1004
+ "license": "MIT",
1005
+ "dependencies": {
1006
+ "@volar/language-core": "2.4.28",
1007
+ "path-browserify": "^1.0.1",
1008
+ "vscode-uri": "^3.0.8"
1009
+ }
1010
+ },
1011
+ "node_modules/@vue-macros/common": {
1012
+ "version": "3.1.2",
1013
+ "resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-3.1.2.tgz",
1014
+ "integrity": "sha512-h9t4ArDdniO9ekYHAD95t9AZcAbb19lEGK+26iAjUODOIJKmObDNBSe4+6ELQAA3vtYiFPPBtHh7+cQCKi3Dng==",
1015
+ "license": "MIT",
1016
+ "dependencies": {
1017
+ "@vue/compiler-sfc": "^3.5.22",
1018
+ "ast-kit": "^2.1.2",
1019
+ "local-pkg": "^1.1.2",
1020
+ "magic-string-ast": "^1.0.2",
1021
+ "unplugin-utils": "^0.3.0"
1022
+ },
1023
+ "engines": {
1024
+ "node": ">=20.19.0"
1025
+ },
1026
+ "funding": {
1027
+ "url": "https://github.com/sponsors/vue-macros"
1028
+ },
1029
+ "peerDependencies": {
1030
+ "vue": "^2.7.0 || ^3.2.25"
1031
+ },
1032
+ "peerDependenciesMeta": {
1033
+ "vue": {
1034
+ "optional": true
1035
+ }
1036
+ }
1037
+ },
1038
+ "node_modules/@vue/compiler-core": {
1039
+ "version": "3.5.31",
1040
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.31.tgz",
1041
+ "integrity": "sha512-k/ueL14aNIEy5Onf0OVzR8kiqF/WThgLdFhxwa4e/KF/0qe38IwIdofoSWBTvvxQOesaz6riAFAUaYjoF9fLLQ==",
1042
+ "license": "MIT",
1043
+ "dependencies": {
1044
+ "@babel/parser": "^7.29.2",
1045
+ "@vue/shared": "3.5.31",
1046
+ "entities": "^7.0.1",
1047
+ "estree-walker": "^2.0.2",
1048
+ "source-map-js": "^1.2.1"
1049
+ }
1050
+ },
1051
+ "node_modules/@vue/compiler-dom": {
1052
+ "version": "3.5.31",
1053
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.31.tgz",
1054
+ "integrity": "sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw==",
1055
+ "license": "MIT",
1056
+ "dependencies": {
1057
+ "@vue/compiler-core": "3.5.31",
1058
+ "@vue/shared": "3.5.31"
1059
+ }
1060
+ },
1061
+ "node_modules/@vue/compiler-sfc": {
1062
+ "version": "3.5.31",
1063
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.31.tgz",
1064
+ "integrity": "sha512-M8wpPgR9UJ8MiRGjppvx9uWJfLV7A/T+/rL8s/y3QG3u0c2/YZgff3d6SuimKRIhcYnWg5fTfDMlz2E6seUW8Q==",
1065
+ "license": "MIT",
1066
+ "dependencies": {
1067
+ "@babel/parser": "^7.29.2",
1068
+ "@vue/compiler-core": "3.5.31",
1069
+ "@vue/compiler-dom": "3.5.31",
1070
+ "@vue/compiler-ssr": "3.5.31",
1071
+ "@vue/shared": "3.5.31",
1072
+ "estree-walker": "^2.0.2",
1073
+ "magic-string": "^0.30.21",
1074
+ "postcss": "^8.5.8",
1075
+ "source-map-js": "^1.2.1"
1076
+ }
1077
+ },
1078
+ "node_modules/@vue/compiler-ssr": {
1079
+ "version": "3.5.31",
1080
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.31.tgz",
1081
+ "integrity": "sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog==",
1082
+ "license": "MIT",
1083
+ "dependencies": {
1084
+ "@vue/compiler-dom": "3.5.31",
1085
+ "@vue/shared": "3.5.31"
1086
+ }
1087
+ },
1088
+ "node_modules/@vue/devtools-api": {
1089
+ "version": "8.1.1",
1090
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.1.1.tgz",
1091
+ "integrity": "sha512-bsDMJ07b3GN1puVwJb/fyFnj/U2imyswK5UQVLZwVl7O05jDrt6BHxeG5XffmOOdasOj/bOmIjxJvGPxU7pcqw==",
1092
+ "license": "MIT",
1093
+ "dependencies": {
1094
+ "@vue/devtools-kit": "^8.1.1"
1095
+ }
1096
+ },
1097
+ "node_modules/@vue/devtools-kit": {
1098
+ "version": "8.1.1",
1099
+ "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.1.1.tgz",
1100
+ "integrity": "sha512-gVBaBv++i+adg4JpH71k9ppl4soyR7Y2McEqO5YNgv0BI1kMZ7BDX5gnwkZ5COYgiCyhejZG+yGNrBAjj6Coqg==",
1101
+ "license": "MIT",
1102
+ "dependencies": {
1103
+ "@vue/devtools-shared": "^8.1.1",
1104
+ "birpc": "^2.6.1",
1105
+ "hookable": "^5.5.3",
1106
+ "perfect-debounce": "^2.0.0"
1107
+ }
1108
+ },
1109
+ "node_modules/@vue/devtools-shared": {
1110
+ "version": "8.1.1",
1111
+ "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.1.1.tgz",
1112
+ "integrity": "sha512-+h4ttmJYl/txpxHKaoZcaKpC+pvckgLzIDiSQlaQ7kKthKh8KuwoLW2D8hPJEnqKzXOvu15UHEoGyngAXCz0EQ==",
1113
+ "license": "MIT"
1114
+ },
1115
+ "node_modules/@vue/language-core": {
1116
+ "version": "3.2.6",
1117
+ "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.6.tgz",
1118
+ "integrity": "sha512-xYYYX3/aVup576tP/23sEUpgiEnujrENaoNRbaozC1/MA9I6EGFQRJb4xrt/MmUCAGlxTKL2RmT8JLTPqagCkg==",
1119
+ "dev": true,
1120
+ "license": "MIT",
1121
+ "dependencies": {
1122
+ "@volar/language-core": "2.4.28",
1123
+ "@vue/compiler-dom": "^3.5.0",
1124
+ "@vue/shared": "^3.5.0",
1125
+ "alien-signals": "^3.0.0",
1126
+ "muggle-string": "^0.4.1",
1127
+ "path-browserify": "^1.0.1",
1128
+ "picomatch": "^4.0.2"
1129
+ }
1130
+ },
1131
+ "node_modules/@vue/reactivity": {
1132
+ "version": "3.5.31",
1133
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.31.tgz",
1134
+ "integrity": "sha512-DtKXxk9E/KuVvt8VxWu+6Luc9I9ETNcqR1T1oW1gf02nXaZ1kuAx58oVu7uX9XxJR0iJCro6fqBLw9oSBELo5g==",
1135
+ "license": "MIT",
1136
+ "dependencies": {
1137
+ "@vue/shared": "3.5.31"
1138
+ }
1139
+ },
1140
+ "node_modules/@vue/runtime-core": {
1141
+ "version": "3.5.31",
1142
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.31.tgz",
1143
+ "integrity": "sha512-AZPmIHXEAyhpkmN7aWlqjSfYynmkWlluDNPHMCZKFHH+lLtxP/30UJmoVhXmbDoP1Ng0jG0fyY2zCj1PnSSA6Q==",
1144
+ "license": "MIT",
1145
+ "dependencies": {
1146
+ "@vue/reactivity": "3.5.31",
1147
+ "@vue/shared": "3.5.31"
1148
+ }
1149
+ },
1150
+ "node_modules/@vue/runtime-dom": {
1151
+ "version": "3.5.31",
1152
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.31.tgz",
1153
+ "integrity": "sha512-xQJsNRmGPeDCJq/u813tyonNgWBFjzfVkBwDREdEWndBnGdHLHgkwNBQxLtg4zDrzKTEcnikUy1UUNecb3lJ6g==",
1154
+ "license": "MIT",
1155
+ "dependencies": {
1156
+ "@vue/reactivity": "3.5.31",
1157
+ "@vue/runtime-core": "3.5.31",
1158
+ "@vue/shared": "3.5.31",
1159
+ "csstype": "^3.2.3"
1160
+ }
1161
+ },
1162
+ "node_modules/@vue/server-renderer": {
1163
+ "version": "3.5.31",
1164
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.31.tgz",
1165
+ "integrity": "sha512-GJuwRvMcdZX/CriUnyIIOGkx3rMV3H6sOu0JhdKbduaeCji6zb60iOGMY7tFoN24NfsUYoFBhshZtGxGpxO4iA==",
1166
+ "license": "MIT",
1167
+ "dependencies": {
1168
+ "@vue/compiler-ssr": "3.5.31",
1169
+ "@vue/shared": "3.5.31"
1170
+ },
1171
+ "peerDependencies": {
1172
+ "vue": "3.5.31"
1173
+ }
1174
+ },
1175
+ "node_modules/@vue/shared": {
1176
+ "version": "3.5.31",
1177
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.31.tgz",
1178
+ "integrity": "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw==",
1179
+ "license": "MIT"
1180
+ },
1181
+ "node_modules/@vue/tsconfig": {
1182
+ "version": "0.9.1",
1183
+ "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.9.1.tgz",
1184
+ "integrity": "sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w==",
1185
+ "dev": true,
1186
+ "license": "MIT",
1187
+ "peerDependencies": {
1188
+ "typescript": ">= 5.8",
1189
+ "vue": "^3.4.0"
1190
+ },
1191
+ "peerDependenciesMeta": {
1192
+ "typescript": {
1193
+ "optional": true
1194
+ },
1195
+ "vue": {
1196
+ "optional": true
1197
+ }
1198
+ }
1199
+ },
1200
+ "node_modules/@vueuse/core": {
1201
+ "version": "10.11.1",
1202
+ "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz",
1203
+ "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==",
1204
+ "license": "MIT",
1205
+ "dependencies": {
1206
+ "@types/web-bluetooth": "^0.0.20",
1207
+ "@vueuse/metadata": "10.11.1",
1208
+ "@vueuse/shared": "10.11.1",
1209
+ "vue-demi": ">=0.14.8"
1210
+ },
1211
+ "funding": {
1212
+ "url": "https://github.com/sponsors/antfu"
1213
+ }
1214
+ },
1215
+ "node_modules/@vueuse/core/node_modules/vue-demi": {
1216
+ "version": "0.14.10",
1217
+ "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
1218
+ "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
1219
+ "hasInstallScript": true,
1220
+ "license": "MIT",
1221
+ "bin": {
1222
+ "vue-demi-fix": "bin/vue-demi-fix.js",
1223
+ "vue-demi-switch": "bin/vue-demi-switch.js"
1224
+ },
1225
+ "engines": {
1226
+ "node": ">=12"
1227
+ },
1228
+ "funding": {
1229
+ "url": "https://github.com/sponsors/antfu"
1230
+ },
1231
+ "peerDependencies": {
1232
+ "@vue/composition-api": "^1.0.0-rc.1",
1233
+ "vue": "^3.0.0-0 || ^2.6.0"
1234
+ },
1235
+ "peerDependenciesMeta": {
1236
+ "@vue/composition-api": {
1237
+ "optional": true
1238
+ }
1239
+ }
1240
+ },
1241
+ "node_modules/@vueuse/metadata": {
1242
+ "version": "10.11.1",
1243
+ "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz",
1244
+ "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==",
1245
+ "license": "MIT",
1246
+ "funding": {
1247
+ "url": "https://github.com/sponsors/antfu"
1248
+ }
1249
+ },
1250
+ "node_modules/@vueuse/shared": {
1251
+ "version": "10.11.1",
1252
+ "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz",
1253
+ "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==",
1254
+ "license": "MIT",
1255
+ "dependencies": {
1256
+ "vue-demi": ">=0.14.8"
1257
+ },
1258
+ "funding": {
1259
+ "url": "https://github.com/sponsors/antfu"
1260
+ }
1261
+ },
1262
+ "node_modules/@vueuse/shared/node_modules/vue-demi": {
1263
+ "version": "0.14.10",
1264
+ "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
1265
+ "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
1266
+ "hasInstallScript": true,
1267
+ "license": "MIT",
1268
+ "bin": {
1269
+ "vue-demi-fix": "bin/vue-demi-fix.js",
1270
+ "vue-demi-switch": "bin/vue-demi-switch.js"
1271
+ },
1272
+ "engines": {
1273
+ "node": ">=12"
1274
+ },
1275
+ "funding": {
1276
+ "url": "https://github.com/sponsors/antfu"
1277
+ },
1278
+ "peerDependencies": {
1279
+ "@vue/composition-api": "^1.0.0-rc.1",
1280
+ "vue": "^3.0.0-0 || ^2.6.0"
1281
+ },
1282
+ "peerDependenciesMeta": {
1283
+ "@vue/composition-api": {
1284
+ "optional": true
1285
+ }
1286
+ }
1287
+ },
1288
+ "node_modules/acorn": {
1289
+ "version": "8.16.0",
1290
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
1291
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
1292
+ "license": "MIT",
1293
+ "bin": {
1294
+ "acorn": "bin/acorn"
1295
+ },
1296
+ "engines": {
1297
+ "node": ">=0.4.0"
1298
+ }
1299
+ },
1300
+ "node_modules/alien-signals": {
1301
+ "version": "3.1.2",
1302
+ "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.2.tgz",
1303
+ "integrity": "sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==",
1304
+ "dev": true,
1305
+ "license": "MIT"
1306
+ },
1307
+ "node_modules/argparse": {
1308
+ "version": "2.0.1",
1309
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
1310
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
1311
+ "license": "Python-2.0"
1312
+ },
1313
+ "node_modules/aria-hidden": {
1314
+ "version": "1.2.6",
1315
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
1316
+ "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
1317
+ "license": "MIT",
1318
+ "dependencies": {
1319
+ "tslib": "^2.0.0"
1320
+ },
1321
+ "engines": {
1322
+ "node": ">=10"
1323
+ }
1324
+ },
1325
+ "node_modules/ast-kit": {
1326
+ "version": "2.2.0",
1327
+ "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz",
1328
+ "integrity": "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==",
1329
+ "license": "MIT",
1330
+ "dependencies": {
1331
+ "@babel/parser": "^7.28.5",
1332
+ "pathe": "^2.0.3"
1333
+ },
1334
+ "engines": {
1335
+ "node": ">=20.19.0"
1336
+ },
1337
+ "funding": {
1338
+ "url": "https://github.com/sponsors/sxzz"
1339
+ }
1340
+ },
1341
+ "node_modules/ast-walker-scope": {
1342
+ "version": "0.8.3",
1343
+ "resolved": "https://registry.npmjs.org/ast-walker-scope/-/ast-walker-scope-0.8.3.tgz",
1344
+ "integrity": "sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg==",
1345
+ "license": "MIT",
1346
+ "dependencies": {
1347
+ "@babel/parser": "^7.28.4",
1348
+ "ast-kit": "^2.1.3"
1349
+ },
1350
+ "engines": {
1351
+ "node": ">=20.19.0"
1352
+ },
1353
+ "funding": {
1354
+ "url": "https://github.com/sponsors/sxzz"
1355
+ }
1356
+ },
1357
+ "node_modules/birpc": {
1358
+ "version": "2.9.0",
1359
+ "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz",
1360
+ "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==",
1361
+ "license": "MIT",
1362
+ "funding": {
1363
+ "url": "https://github.com/sponsors/antfu"
1364
+ }
1365
+ },
1366
+ "node_modules/chokidar": {
1367
+ "version": "5.0.0",
1368
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
1369
+ "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
1370
+ "license": "MIT",
1371
+ "dependencies": {
1372
+ "readdirp": "^5.0.0"
1373
+ },
1374
+ "engines": {
1375
+ "node": ">= 20.19.0"
1376
+ },
1377
+ "funding": {
1378
+ "url": "https://paulmillr.com/funding/"
1379
+ }
1380
+ },
1381
+ "node_modules/class-variance-authority": {
1382
+ "version": "0.7.1",
1383
+ "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
1384
+ "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
1385
+ "license": "Apache-2.0",
1386
+ "dependencies": {
1387
+ "clsx": "^2.1.1"
1388
+ },
1389
+ "funding": {
1390
+ "url": "https://polar.sh/cva"
1391
+ }
1392
+ },
1393
+ "node_modules/clsx": {
1394
+ "version": "2.1.1",
1395
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
1396
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
1397
+ "license": "MIT",
1398
+ "engines": {
1399
+ "node": ">=6"
1400
+ }
1401
+ },
1402
+ "node_modules/confbox": {
1403
+ "version": "0.2.4",
1404
+ "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
1405
+ "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
1406
+ "license": "MIT"
1407
+ },
1408
+ "node_modules/csstype": {
1409
+ "version": "3.2.3",
1410
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
1411
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
1412
+ "license": "MIT"
1413
+ },
1414
+ "node_modules/defu": {
1415
+ "version": "6.1.4",
1416
+ "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
1417
+ "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
1418
+ "license": "MIT"
1419
+ },
1420
+ "node_modules/detect-libc": {
1421
+ "version": "2.1.2",
1422
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
1423
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
1424
+ "license": "Apache-2.0",
1425
+ "engines": {
1426
+ "node": ">=8"
1427
+ }
1428
+ },
1429
+ "node_modules/dompurify": {
1430
+ "version": "3.3.3",
1431
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz",
1432
+ "integrity": "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==",
1433
+ "license": "(MPL-2.0 OR Apache-2.0)",
1434
+ "optionalDependencies": {
1435
+ "@types/trusted-types": "^2.0.7"
1436
+ }
1437
+ },
1438
+ "node_modules/echarts": {
1439
+ "version": "6.0.0",
1440
+ "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz",
1441
+ "integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==",
1442
+ "license": "Apache-2.0",
1443
+ "dependencies": {
1444
+ "tslib": "2.3.0",
1445
+ "zrender": "6.0.0"
1446
+ }
1447
+ },
1448
+ "node_modules/echarts/node_modules/tslib": {
1449
+ "version": "2.3.0",
1450
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
1451
+ "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
1452
+ "license": "0BSD"
1453
+ },
1454
+ "node_modules/enhanced-resolve": {
1455
+ "version": "5.20.1",
1456
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz",
1457
+ "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==",
1458
+ "license": "MIT",
1459
+ "dependencies": {
1460
+ "graceful-fs": "^4.2.4",
1461
+ "tapable": "^2.3.0"
1462
+ },
1463
+ "engines": {
1464
+ "node": ">=10.13.0"
1465
+ }
1466
+ },
1467
+ "node_modules/entities": {
1468
+ "version": "7.0.1",
1469
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
1470
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
1471
+ "license": "BSD-2-Clause",
1472
+ "engines": {
1473
+ "node": ">=0.12"
1474
+ },
1475
+ "funding": {
1476
+ "url": "https://github.com/fb55/entities?sponsor=1"
1477
+ }
1478
+ },
1479
+ "node_modules/estree-walker": {
1480
+ "version": "2.0.2",
1481
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
1482
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
1483
+ "license": "MIT"
1484
+ },
1485
+ "node_modules/exsolve": {
1486
+ "version": "1.0.8",
1487
+ "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
1488
+ "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
1489
+ "license": "MIT"
1490
+ },
1491
+ "node_modules/fast-deep-equal": {
1492
+ "version": "3.1.3",
1493
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
1494
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
1495
+ "license": "MIT"
1496
+ },
1497
+ "node_modules/fdir": {
1498
+ "version": "6.5.0",
1499
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
1500
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
1501
+ "license": "MIT",
1502
+ "engines": {
1503
+ "node": ">=12.0.0"
1504
+ },
1505
+ "peerDependencies": {
1506
+ "picomatch": "^3 || ^4"
1507
+ },
1508
+ "peerDependenciesMeta": {
1509
+ "picomatch": {
1510
+ "optional": true
1511
+ }
1512
+ }
1513
+ },
1514
+ "node_modules/fsevents": {
1515
+ "version": "2.3.3",
1516
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1517
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1518
+ "hasInstallScript": true,
1519
+ "license": "MIT",
1520
+ "optional": true,
1521
+ "os": [
1522
+ "darwin"
1523
+ ],
1524
+ "engines": {
1525
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1526
+ }
1527
+ },
1528
+ "node_modules/graceful-fs": {
1529
+ "version": "4.2.11",
1530
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
1531
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
1532
+ "license": "ISC"
1533
+ },
1534
+ "node_modules/gsap": {
1535
+ "version": "3.14.2",
1536
+ "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.14.2.tgz",
1537
+ "integrity": "sha512-P8/mMxVLU7o4+55+1TCnQrPmgjPKnwkzkXOK1asnR9Jg2lna4tEY5qBJjMmAaOBDDZWtlRjBXjLa0w53G/uBLA==",
1538
+ "license": "Standard 'no charge' license: https://gsap.com/standard-license."
1539
+ },
1540
+ "node_modules/hey-listen": {
1541
+ "version": "1.0.8",
1542
+ "resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz",
1543
+ "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==",
1544
+ "license": "MIT"
1545
+ },
1546
+ "node_modules/hookable": {
1547
+ "version": "5.5.3",
1548
+ "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
1549
+ "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
1550
+ "license": "MIT"
1551
+ },
1552
+ "node_modules/jiti": {
1553
+ "version": "2.6.1",
1554
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
1555
+ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
1556
+ "license": "MIT",
1557
+ "bin": {
1558
+ "jiti": "lib/jiti-cli.mjs"
1559
+ }
1560
+ },
1561
+ "node_modules/jsesc": {
1562
+ "version": "3.1.0",
1563
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
1564
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
1565
+ "license": "MIT",
1566
+ "bin": {
1567
+ "jsesc": "bin/jsesc"
1568
+ },
1569
+ "engines": {
1570
+ "node": ">=6"
1571
+ }
1572
+ },
1573
+ "node_modules/json5": {
1574
+ "version": "2.2.3",
1575
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
1576
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
1577
+ "license": "MIT",
1578
+ "bin": {
1579
+ "json5": "lib/cli.js"
1580
+ },
1581
+ "engines": {
1582
+ "node": ">=6"
1583
+ }
1584
+ },
1585
+ "node_modules/lightningcss": {
1586
+ "version": "1.32.0",
1587
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
1588
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
1589
+ "license": "MPL-2.0",
1590
+ "dependencies": {
1591
+ "detect-libc": "^2.0.3"
1592
+ },
1593
+ "engines": {
1594
+ "node": ">= 12.0.0"
1595
+ },
1596
+ "funding": {
1597
+ "type": "opencollective",
1598
+ "url": "https://opencollective.com/parcel"
1599
+ },
1600
+ "optionalDependencies": {
1601
+ "lightningcss-android-arm64": "1.32.0",
1602
+ "lightningcss-darwin-arm64": "1.32.0",
1603
+ "lightningcss-darwin-x64": "1.32.0",
1604
+ "lightningcss-freebsd-x64": "1.32.0",
1605
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
1606
+ "lightningcss-linux-arm64-gnu": "1.32.0",
1607
+ "lightningcss-linux-arm64-musl": "1.32.0",
1608
+ "lightningcss-linux-x64-gnu": "1.32.0",
1609
+ "lightningcss-linux-x64-musl": "1.32.0",
1610
+ "lightningcss-win32-arm64-msvc": "1.32.0",
1611
+ "lightningcss-win32-x64-msvc": "1.32.0"
1612
+ }
1613
+ },
1614
+ "node_modules/lightningcss-android-arm64": {
1615
+ "version": "1.32.0",
1616
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
1617
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
1618
+ "cpu": [
1619
+ "arm64"
1620
+ ],
1621
+ "license": "MPL-2.0",
1622
+ "optional": true,
1623
+ "os": [
1624
+ "android"
1625
+ ],
1626
+ "engines": {
1627
+ "node": ">= 12.0.0"
1628
+ },
1629
+ "funding": {
1630
+ "type": "opencollective",
1631
+ "url": "https://opencollective.com/parcel"
1632
+ }
1633
+ },
1634
+ "node_modules/lightningcss-darwin-arm64": {
1635
+ "version": "1.32.0",
1636
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
1637
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
1638
+ "cpu": [
1639
+ "arm64"
1640
+ ],
1641
+ "license": "MPL-2.0",
1642
+ "optional": true,
1643
+ "os": [
1644
+ "darwin"
1645
+ ],
1646
+ "engines": {
1647
+ "node": ">= 12.0.0"
1648
+ },
1649
+ "funding": {
1650
+ "type": "opencollective",
1651
+ "url": "https://opencollective.com/parcel"
1652
+ }
1653
+ },
1654
+ "node_modules/lightningcss-darwin-x64": {
1655
+ "version": "1.32.0",
1656
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
1657
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
1658
+ "cpu": [
1659
+ "x64"
1660
+ ],
1661
+ "license": "MPL-2.0",
1662
+ "optional": true,
1663
+ "os": [
1664
+ "darwin"
1665
+ ],
1666
+ "engines": {
1667
+ "node": ">= 12.0.0"
1668
+ },
1669
+ "funding": {
1670
+ "type": "opencollective",
1671
+ "url": "https://opencollective.com/parcel"
1672
+ }
1673
+ },
1674
+ "node_modules/lightningcss-freebsd-x64": {
1675
+ "version": "1.32.0",
1676
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
1677
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
1678
+ "cpu": [
1679
+ "x64"
1680
+ ],
1681
+ "license": "MPL-2.0",
1682
+ "optional": true,
1683
+ "os": [
1684
+ "freebsd"
1685
+ ],
1686
+ "engines": {
1687
+ "node": ">= 12.0.0"
1688
+ },
1689
+ "funding": {
1690
+ "type": "opencollective",
1691
+ "url": "https://opencollective.com/parcel"
1692
+ }
1693
+ },
1694
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
1695
+ "version": "1.32.0",
1696
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
1697
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
1698
+ "cpu": [
1699
+ "arm"
1700
+ ],
1701
+ "license": "MPL-2.0",
1702
+ "optional": true,
1703
+ "os": [
1704
+ "linux"
1705
+ ],
1706
+ "engines": {
1707
+ "node": ">= 12.0.0"
1708
+ },
1709
+ "funding": {
1710
+ "type": "opencollective",
1711
+ "url": "https://opencollective.com/parcel"
1712
+ }
1713
+ },
1714
+ "node_modules/lightningcss-linux-arm64-gnu": {
1715
+ "version": "1.32.0",
1716
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
1717
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
1718
+ "cpu": [
1719
+ "arm64"
1720
+ ],
1721
+ "license": "MPL-2.0",
1722
+ "optional": true,
1723
+ "os": [
1724
+ "linux"
1725
+ ],
1726
+ "engines": {
1727
+ "node": ">= 12.0.0"
1728
+ },
1729
+ "funding": {
1730
+ "type": "opencollective",
1731
+ "url": "https://opencollective.com/parcel"
1732
+ }
1733
+ },
1734
+ "node_modules/lightningcss-linux-arm64-musl": {
1735
+ "version": "1.32.0",
1736
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
1737
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
1738
+ "cpu": [
1739
+ "arm64"
1740
+ ],
1741
+ "license": "MPL-2.0",
1742
+ "optional": true,
1743
+ "os": [
1744
+ "linux"
1745
+ ],
1746
+ "engines": {
1747
+ "node": ">= 12.0.0"
1748
+ },
1749
+ "funding": {
1750
+ "type": "opencollective",
1751
+ "url": "https://opencollective.com/parcel"
1752
+ }
1753
+ },
1754
+ "node_modules/lightningcss-linux-x64-gnu": {
1755
+ "version": "1.32.0",
1756
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
1757
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
1758
+ "cpu": [
1759
+ "x64"
1760
+ ],
1761
+ "license": "MPL-2.0",
1762
+ "optional": true,
1763
+ "os": [
1764
+ "linux"
1765
+ ],
1766
+ "engines": {
1767
+ "node": ">= 12.0.0"
1768
+ },
1769
+ "funding": {
1770
+ "type": "opencollective",
1771
+ "url": "https://opencollective.com/parcel"
1772
+ }
1773
+ },
1774
+ "node_modules/lightningcss-linux-x64-musl": {
1775
+ "version": "1.32.0",
1776
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
1777
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
1778
+ "cpu": [
1779
+ "x64"
1780
+ ],
1781
+ "license": "MPL-2.0",
1782
+ "optional": true,
1783
+ "os": [
1784
+ "linux"
1785
+ ],
1786
+ "engines": {
1787
+ "node": ">= 12.0.0"
1788
+ },
1789
+ "funding": {
1790
+ "type": "opencollective",
1791
+ "url": "https://opencollective.com/parcel"
1792
+ }
1793
+ },
1794
+ "node_modules/lightningcss-win32-arm64-msvc": {
1795
+ "version": "1.32.0",
1796
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
1797
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
1798
+ "cpu": [
1799
+ "arm64"
1800
+ ],
1801
+ "license": "MPL-2.0",
1802
+ "optional": true,
1803
+ "os": [
1804
+ "win32"
1805
+ ],
1806
+ "engines": {
1807
+ "node": ">= 12.0.0"
1808
+ },
1809
+ "funding": {
1810
+ "type": "opencollective",
1811
+ "url": "https://opencollective.com/parcel"
1812
+ }
1813
+ },
1814
+ "node_modules/lightningcss-win32-x64-msvc": {
1815
+ "version": "1.32.0",
1816
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
1817
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
1818
+ "cpu": [
1819
+ "x64"
1820
+ ],
1821
+ "license": "MPL-2.0",
1822
+ "optional": true,
1823
+ "os": [
1824
+ "win32"
1825
+ ],
1826
+ "engines": {
1827
+ "node": ">= 12.0.0"
1828
+ },
1829
+ "funding": {
1830
+ "type": "opencollective",
1831
+ "url": "https://opencollective.com/parcel"
1832
+ }
1833
+ },
1834
+ "node_modules/linkify-it": {
1835
+ "version": "5.0.0",
1836
+ "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
1837
+ "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
1838
+ "license": "MIT",
1839
+ "dependencies": {
1840
+ "uc.micro": "^2.0.0"
1841
+ }
1842
+ },
1843
+ "node_modules/local-pkg": {
1844
+ "version": "1.1.2",
1845
+ "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz",
1846
+ "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==",
1847
+ "license": "MIT",
1848
+ "dependencies": {
1849
+ "mlly": "^1.7.4",
1850
+ "pkg-types": "^2.3.0",
1851
+ "quansync": "^0.2.11"
1852
+ },
1853
+ "engines": {
1854
+ "node": ">=14"
1855
+ },
1856
+ "funding": {
1857
+ "url": "https://github.com/sponsors/antfu"
1858
+ }
1859
+ },
1860
+ "node_modules/lucide-vue-next": {
1861
+ "version": "1.0.0",
1862
+ "resolved": "https://registry.npmjs.org/lucide-vue-next/-/lucide-vue-next-1.0.0.tgz",
1863
+ "integrity": "sha512-V6SPvx1IHTj/UY+FrIYWV5faISsPSb8BnWSFDxAtezWKvWc9ZZ40PDrdu1/Qb5vg4lHWr1hs1BAMGVGm6V1Xdg==",
1864
+ "license": "ISC",
1865
+ "peerDependencies": {
1866
+ "vue": ">=3.0.1"
1867
+ }
1868
+ },
1869
+ "node_modules/magic-string": {
1870
+ "version": "0.30.21",
1871
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
1872
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
1873
+ "license": "MIT",
1874
+ "dependencies": {
1875
+ "@jridgewell/sourcemap-codec": "^1.5.5"
1876
+ }
1877
+ },
1878
+ "node_modules/magic-string-ast": {
1879
+ "version": "1.0.3",
1880
+ "resolved": "https://registry.npmjs.org/magic-string-ast/-/magic-string-ast-1.0.3.tgz",
1881
+ "integrity": "sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==",
1882
+ "license": "MIT",
1883
+ "dependencies": {
1884
+ "magic-string": "^0.30.19"
1885
+ },
1886
+ "engines": {
1887
+ "node": ">=20.19.0"
1888
+ },
1889
+ "funding": {
1890
+ "url": "https://github.com/sponsors/sxzz"
1891
+ }
1892
+ },
1893
+ "node_modules/markdown-it": {
1894
+ "version": "14.1.1",
1895
+ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz",
1896
+ "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==",
1897
+ "license": "MIT",
1898
+ "dependencies": {
1899
+ "argparse": "^2.0.1",
1900
+ "entities": "^4.4.0",
1901
+ "linkify-it": "^5.0.0",
1902
+ "mdurl": "^2.0.0",
1903
+ "punycode.js": "^2.3.1",
1904
+ "uc.micro": "^2.1.0"
1905
+ },
1906
+ "bin": {
1907
+ "markdown-it": "bin/markdown-it.mjs"
1908
+ }
1909
+ },
1910
+ "node_modules/markdown-it-multimd-table": {
1911
+ "version": "4.2.3",
1912
+ "resolved": "https://registry.npmjs.org/markdown-it-multimd-table/-/markdown-it-multimd-table-4.2.3.tgz",
1913
+ "integrity": "sha512-KepCr2OMJqm7IT6sOIbuqHGe+NERhgy66XMrc5lo6dHW7oaPzMDtYwR1EGwK16/blb6mCSg4jqityOe0o/H7HA==",
1914
+ "license": "MIT"
1915
+ },
1916
+ "node_modules/markdown-it/node_modules/entities": {
1917
+ "version": "4.5.0",
1918
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
1919
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
1920
+ "license": "BSD-2-Clause",
1921
+ "engines": {
1922
+ "node": ">=0.12"
1923
+ },
1924
+ "funding": {
1925
+ "url": "https://github.com/fb55/entities?sponsor=1"
1926
+ }
1927
+ },
1928
+ "node_modules/mdurl": {
1929
+ "version": "2.0.0",
1930
+ "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
1931
+ "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
1932
+ "license": "MIT"
1933
+ },
1934
+ "node_modules/mlly": {
1935
+ "version": "1.8.2",
1936
+ "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz",
1937
+ "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==",
1938
+ "license": "MIT",
1939
+ "dependencies": {
1940
+ "acorn": "^8.16.0",
1941
+ "pathe": "^2.0.3",
1942
+ "pkg-types": "^1.3.1",
1943
+ "ufo": "^1.6.3"
1944
+ }
1945
+ },
1946
+ "node_modules/mlly/node_modules/confbox": {
1947
+ "version": "0.1.8",
1948
+ "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
1949
+ "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
1950
+ "license": "MIT"
1951
+ },
1952
+ "node_modules/mlly/node_modules/pkg-types": {
1953
+ "version": "1.3.1",
1954
+ "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
1955
+ "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
1956
+ "license": "MIT",
1957
+ "dependencies": {
1958
+ "confbox": "^0.1.8",
1959
+ "mlly": "^1.7.4",
1960
+ "pathe": "^2.0.1"
1961
+ }
1962
+ },
1963
+ "node_modules/muggle-string": {
1964
+ "version": "0.4.1",
1965
+ "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
1966
+ "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
1967
+ "license": "MIT"
1968
+ },
1969
+ "node_modules/nanoid": {
1970
+ "version": "3.3.11",
1971
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
1972
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
1973
+ "funding": [
1974
+ {
1975
+ "type": "github",
1976
+ "url": "https://github.com/sponsors/ai"
1977
+ }
1978
+ ],
1979
+ "license": "MIT",
1980
+ "bin": {
1981
+ "nanoid": "bin/nanoid.cjs"
1982
+ },
1983
+ "engines": {
1984
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1985
+ }
1986
+ },
1987
+ "node_modules/path-browserify": {
1988
+ "version": "1.0.1",
1989
+ "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
1990
+ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
1991
+ "dev": true,
1992
+ "license": "MIT"
1993
+ },
1994
+ "node_modules/pathe": {
1995
+ "version": "2.0.3",
1996
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
1997
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
1998
+ "license": "MIT"
1999
+ },
2000
+ "node_modules/perfect-debounce": {
2001
+ "version": "2.1.0",
2002
+ "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz",
2003
+ "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==",
2004
+ "license": "MIT"
2005
+ },
2006
+ "node_modules/picocolors": {
2007
+ "version": "1.1.1",
2008
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
2009
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
2010
+ "license": "ISC"
2011
+ },
2012
+ "node_modules/picomatch": {
2013
+ "version": "4.0.4",
2014
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
2015
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
2016
+ "license": "MIT",
2017
+ "engines": {
2018
+ "node": ">=12"
2019
+ },
2020
+ "funding": {
2021
+ "url": "https://github.com/sponsors/jonschlinkert"
2022
+ }
2023
+ },
2024
+ "node_modules/pkg-types": {
2025
+ "version": "2.3.0",
2026
+ "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
2027
+ "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
2028
+ "license": "MIT",
2029
+ "dependencies": {
2030
+ "confbox": "^0.2.2",
2031
+ "exsolve": "^1.0.7",
2032
+ "pathe": "^2.0.3"
2033
+ }
2034
+ },
2035
+ "node_modules/plotly.js-dist-min": {
2036
+ "version": "3.4.0",
2037
+ "resolved": "https://registry.npmjs.org/plotly.js-dist-min/-/plotly.js-dist-min-3.4.0.tgz",
2038
+ "integrity": "sha512-eo7xh7oyE9fFoE/wintgmvfOjvTKwCb3wRf9ShQv90du4n+EVkOY7w5qEkmUS9SSkHRnAw8sk/0QI7wEc5U+8Q==",
2039
+ "license": "MIT"
2040
+ },
2041
+ "node_modules/postcss": {
2042
+ "version": "8.5.8",
2043
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
2044
+ "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
2045
+ "funding": [
2046
+ {
2047
+ "type": "opencollective",
2048
+ "url": "https://opencollective.com/postcss/"
2049
+ },
2050
+ {
2051
+ "type": "tidelift",
2052
+ "url": "https://tidelift.com/funding/github/npm/postcss"
2053
+ },
2054
+ {
2055
+ "type": "github",
2056
+ "url": "https://github.com/sponsors/ai"
2057
+ }
2058
+ ],
2059
+ "license": "MIT",
2060
+ "dependencies": {
2061
+ "nanoid": "^3.3.11",
2062
+ "picocolors": "^1.1.1",
2063
+ "source-map-js": "^1.2.1"
2064
+ },
2065
+ "engines": {
2066
+ "node": "^10 || ^12 || >=14"
2067
+ }
2068
+ },
2069
+ "node_modules/punycode.js": {
2070
+ "version": "2.3.1",
2071
+ "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
2072
+ "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
2073
+ "license": "MIT",
2074
+ "engines": {
2075
+ "node": ">=6"
2076
+ }
2077
+ },
2078
+ "node_modules/quansync": {
2079
+ "version": "0.2.11",
2080
+ "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
2081
+ "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==",
2082
+ "funding": [
2083
+ {
2084
+ "type": "individual",
2085
+ "url": "https://github.com/sponsors/antfu"
2086
+ },
2087
+ {
2088
+ "type": "individual",
2089
+ "url": "https://github.com/sponsors/sxzz"
2090
+ }
2091
+ ],
2092
+ "license": "MIT"
2093
+ },
2094
+ "node_modules/radix-vue": {
2095
+ "version": "1.9.17",
2096
+ "resolved": "https://registry.npmjs.org/radix-vue/-/radix-vue-1.9.17.tgz",
2097
+ "integrity": "sha512-mVCu7I2vXt1L2IUYHTt0sZMz7s1K2ZtqKeTIxG3yC5mMFfLBG4FtE1FDeRMpDd+Hhg/ybi9+iXmAP1ISREndoQ==",
2098
+ "license": "MIT",
2099
+ "dependencies": {
2100
+ "@floating-ui/dom": "^1.6.7",
2101
+ "@floating-ui/vue": "^1.1.0",
2102
+ "@internationalized/date": "^3.5.4",
2103
+ "@internationalized/number": "^3.5.3",
2104
+ "@tanstack/vue-virtual": "^3.8.1",
2105
+ "@vueuse/core": "^10.11.0",
2106
+ "@vueuse/shared": "^10.11.0",
2107
+ "aria-hidden": "^1.2.4",
2108
+ "defu": "^6.1.4",
2109
+ "fast-deep-equal": "^3.1.3",
2110
+ "nanoid": "^5.0.7"
2111
+ },
2112
+ "peerDependencies": {
2113
+ "vue": ">= 3.2.0"
2114
+ }
2115
+ },
2116
+ "node_modules/radix-vue/node_modules/nanoid": {
2117
+ "version": "5.1.7",
2118
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.7.tgz",
2119
+ "integrity": "sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ==",
2120
+ "funding": [
2121
+ {
2122
+ "type": "github",
2123
+ "url": "https://github.com/sponsors/ai"
2124
+ }
2125
+ ],
2126
+ "license": "MIT",
2127
+ "bin": {
2128
+ "nanoid": "bin/nanoid.js"
2129
+ },
2130
+ "engines": {
2131
+ "node": "^18 || >=20"
2132
+ }
2133
+ },
2134
+ "node_modules/readdirp": {
2135
+ "version": "5.0.0",
2136
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
2137
+ "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
2138
+ "license": "MIT",
2139
+ "engines": {
2140
+ "node": ">= 20.19.0"
2141
+ },
2142
+ "funding": {
2143
+ "type": "individual",
2144
+ "url": "https://paulmillr.com/funding/"
2145
+ }
2146
+ },
2147
+ "node_modules/rolldown": {
2148
+ "version": "1.0.0-rc.11",
2149
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.11.tgz",
2150
+ "integrity": "sha512-NRjoKMusSjfRbSYiH3VSumlkgFe7kYAa3pzVOsVYVFY3zb5d7nS+a3KGQ7hJKXuYWbzJKPVQ9Wxq2UvyK+ENpw==",
2151
+ "license": "MIT",
2152
+ "dependencies": {
2153
+ "@oxc-project/types": "=0.122.0",
2154
+ "@rolldown/pluginutils": "1.0.0-rc.11"
2155
+ },
2156
+ "bin": {
2157
+ "rolldown": "bin/cli.mjs"
2158
+ },
2159
+ "engines": {
2160
+ "node": "^20.19.0 || >=22.12.0"
2161
+ },
2162
+ "optionalDependencies": {
2163
+ "@rolldown/binding-android-arm64": "1.0.0-rc.11",
2164
+ "@rolldown/binding-darwin-arm64": "1.0.0-rc.11",
2165
+ "@rolldown/binding-darwin-x64": "1.0.0-rc.11",
2166
+ "@rolldown/binding-freebsd-x64": "1.0.0-rc.11",
2167
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.11",
2168
+ "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.11",
2169
+ "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.11",
2170
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.11",
2171
+ "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.11",
2172
+ "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.11",
2173
+ "@rolldown/binding-linux-x64-musl": "1.0.0-rc.11",
2174
+ "@rolldown/binding-openharmony-arm64": "1.0.0-rc.11",
2175
+ "@rolldown/binding-wasm32-wasi": "1.0.0-rc.11",
2176
+ "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.11",
2177
+ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.11"
2178
+ }
2179
+ },
2180
+ "node_modules/rolldown/node_modules/@rolldown/pluginutils": {
2181
+ "version": "1.0.0-rc.11",
2182
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.11.tgz",
2183
+ "integrity": "sha512-xQO9vbwBecJRv9EUcQ/y0dzSTJgA7Q6UVN7xp6B81+tBGSLVAK03yJ9NkJaUA7JFD91kbjxRSC/mDnmvXzbHoQ==",
2184
+ "license": "MIT"
2185
+ },
2186
+ "node_modules/scule": {
2187
+ "version": "1.3.0",
2188
+ "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz",
2189
+ "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
2190
+ "license": "MIT"
2191
+ },
2192
+ "node_modules/source-map-js": {
2193
+ "version": "1.2.1",
2194
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
2195
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
2196
+ "license": "BSD-3-Clause",
2197
+ "engines": {
2198
+ "node": ">=0.10.0"
2199
+ }
2200
+ },
2201
+ "node_modules/tailwind-merge": {
2202
+ "version": "3.5.0",
2203
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz",
2204
+ "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==",
2205
+ "license": "MIT",
2206
+ "funding": {
2207
+ "type": "github",
2208
+ "url": "https://github.com/sponsors/dcastil"
2209
+ }
2210
+ },
2211
+ "node_modules/tailwindcss": {
2212
+ "version": "4.2.2",
2213
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz",
2214
+ "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==",
2215
+ "license": "MIT"
2216
+ },
2217
+ "node_modules/tapable": {
2218
+ "version": "2.3.2",
2219
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz",
2220
+ "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==",
2221
+ "license": "MIT",
2222
+ "engines": {
2223
+ "node": ">=6"
2224
+ },
2225
+ "funding": {
2226
+ "type": "opencollective",
2227
+ "url": "https://opencollective.com/webpack"
2228
+ }
2229
+ },
2230
+ "node_modules/tinyglobby": {
2231
+ "version": "0.2.15",
2232
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
2233
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
2234
+ "license": "MIT",
2235
+ "dependencies": {
2236
+ "fdir": "^6.5.0",
2237
+ "picomatch": "^4.0.3"
2238
+ },
2239
+ "engines": {
2240
+ "node": ">=12.0.0"
2241
+ },
2242
+ "funding": {
2243
+ "url": "https://github.com/sponsors/SuperchupuDev"
2244
+ }
2245
+ },
2246
+ "node_modules/tslib": {
2247
+ "version": "2.8.1",
2248
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
2249
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
2250
+ "license": "0BSD"
2251
+ },
2252
+ "node_modules/tw-animate-css": {
2253
+ "version": "1.4.0",
2254
+ "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz",
2255
+ "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==",
2256
+ "license": "MIT",
2257
+ "funding": {
2258
+ "url": "https://github.com/sponsors/Wombosvideo"
2259
+ }
2260
+ },
2261
+ "node_modules/typescript": {
2262
+ "version": "5.9.3",
2263
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
2264
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
2265
+ "devOptional": true,
2266
+ "license": "Apache-2.0",
2267
+ "bin": {
2268
+ "tsc": "bin/tsc",
2269
+ "tsserver": "bin/tsserver"
2270
+ },
2271
+ "engines": {
2272
+ "node": ">=14.17"
2273
+ }
2274
+ },
2275
+ "node_modules/uc.micro": {
2276
+ "version": "2.1.0",
2277
+ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
2278
+ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
2279
+ "license": "MIT"
2280
+ },
2281
+ "node_modules/ufo": {
2282
+ "version": "1.6.3",
2283
+ "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz",
2284
+ "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==",
2285
+ "license": "MIT"
2286
+ },
2287
+ "node_modules/undici-types": {
2288
+ "version": "7.16.0",
2289
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
2290
+ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
2291
+ "devOptional": true,
2292
+ "license": "MIT"
2293
+ },
2294
+ "node_modules/unplugin": {
2295
+ "version": "3.0.0",
2296
+ "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.0.0.tgz",
2297
+ "integrity": "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==",
2298
+ "license": "MIT",
2299
+ "dependencies": {
2300
+ "@jridgewell/remapping": "^2.3.5",
2301
+ "picomatch": "^4.0.3",
2302
+ "webpack-virtual-modules": "^0.6.2"
2303
+ },
2304
+ "engines": {
2305
+ "node": "^20.19.0 || >=22.12.0"
2306
+ }
2307
+ },
2308
+ "node_modules/unplugin-utils": {
2309
+ "version": "0.3.1",
2310
+ "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz",
2311
+ "integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==",
2312
+ "license": "MIT",
2313
+ "dependencies": {
2314
+ "pathe": "^2.0.3",
2315
+ "picomatch": "^4.0.3"
2316
+ },
2317
+ "engines": {
2318
+ "node": ">=20.19.0"
2319
+ },
2320
+ "funding": {
2321
+ "url": "https://github.com/sponsors/sxzz"
2322
+ }
2323
+ },
2324
+ "node_modules/vite": {
2325
+ "version": "8.0.2",
2326
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.2.tgz",
2327
+ "integrity": "sha512-1gFhNi+bHhRE/qKZOJXACm6tX4bA3Isy9KuKF15AgSRuRazNBOJfdDemPBU16/mpMxApDPrWvZ08DcLPEoRnuA==",
2328
+ "license": "MIT",
2329
+ "dependencies": {
2330
+ "lightningcss": "^1.32.0",
2331
+ "picomatch": "^4.0.3",
2332
+ "postcss": "^8.5.8",
2333
+ "rolldown": "1.0.0-rc.11",
2334
+ "tinyglobby": "^0.2.15"
2335
+ },
2336
+ "bin": {
2337
+ "vite": "bin/vite.js"
2338
+ },
2339
+ "engines": {
2340
+ "node": "^20.19.0 || >=22.12.0"
2341
+ },
2342
+ "funding": {
2343
+ "url": "https://github.com/vitejs/vite?sponsor=1"
2344
+ },
2345
+ "optionalDependencies": {
2346
+ "fsevents": "~2.3.3"
2347
+ },
2348
+ "peerDependencies": {
2349
+ "@types/node": "^20.19.0 || >=22.12.0",
2350
+ "@vitejs/devtools": "^0.1.0",
2351
+ "esbuild": "^0.27.0",
2352
+ "jiti": ">=1.21.0",
2353
+ "less": "^4.0.0",
2354
+ "sass": "^1.70.0",
2355
+ "sass-embedded": "^1.70.0",
2356
+ "stylus": ">=0.54.8",
2357
+ "sugarss": "^5.0.0",
2358
+ "terser": "^5.16.0",
2359
+ "tsx": "^4.8.1",
2360
+ "yaml": "^2.4.2"
2361
+ },
2362
+ "peerDependenciesMeta": {
2363
+ "@types/node": {
2364
+ "optional": true
2365
+ },
2366
+ "@vitejs/devtools": {
2367
+ "optional": true
2368
+ },
2369
+ "esbuild": {
2370
+ "optional": true
2371
+ },
2372
+ "jiti": {
2373
+ "optional": true
2374
+ },
2375
+ "less": {
2376
+ "optional": true
2377
+ },
2378
+ "sass": {
2379
+ "optional": true
2380
+ },
2381
+ "sass-embedded": {
2382
+ "optional": true
2383
+ },
2384
+ "stylus": {
2385
+ "optional": true
2386
+ },
2387
+ "sugarss": {
2388
+ "optional": true
2389
+ },
2390
+ "terser": {
2391
+ "optional": true
2392
+ },
2393
+ "tsx": {
2394
+ "optional": true
2395
+ },
2396
+ "yaml": {
2397
+ "optional": true
2398
+ }
2399
+ }
2400
+ },
2401
+ "node_modules/vscode-uri": {
2402
+ "version": "3.1.0",
2403
+ "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
2404
+ "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
2405
+ "dev": true,
2406
+ "license": "MIT"
2407
+ },
2408
+ "node_modules/vue": {
2409
+ "version": "3.5.31",
2410
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.31.tgz",
2411
+ "integrity": "sha512-iV/sU9SzOlmA/0tygSmjkEN6Jbs3nPoIPFhCMLD2STrjgOU8DX7ZtzMhg4ahVwf5Rp9KoFzcXeB1ZrVbLBp5/Q==",
2412
+ "license": "MIT",
2413
+ "dependencies": {
2414
+ "@vue/compiler-dom": "3.5.31",
2415
+ "@vue/compiler-sfc": "3.5.31",
2416
+ "@vue/runtime-dom": "3.5.31",
2417
+ "@vue/server-renderer": "3.5.31",
2418
+ "@vue/shared": "3.5.31"
2419
+ },
2420
+ "peerDependencies": {
2421
+ "typescript": "*"
2422
+ },
2423
+ "peerDependenciesMeta": {
2424
+ "typescript": {
2425
+ "optional": true
2426
+ }
2427
+ }
2428
+ },
2429
+ "node_modules/vue-router": {
2430
+ "version": "5.0.4",
2431
+ "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.4.tgz",
2432
+ "integrity": "sha512-lCqDLCI2+fKVRl2OzXuzdSWmxXFLQRxQbmHugnRpTMyYiT+hNaycV0faqG5FBHDXoYrZ6MQcX87BvbY8mQ20Bg==",
2433
+ "license": "MIT",
2434
+ "dependencies": {
2435
+ "@babel/generator": "^7.28.6",
2436
+ "@vue-macros/common": "^3.1.1",
2437
+ "@vue/devtools-api": "^8.0.6",
2438
+ "ast-walker-scope": "^0.8.3",
2439
+ "chokidar": "^5.0.0",
2440
+ "json5": "^2.2.3",
2441
+ "local-pkg": "^1.1.2",
2442
+ "magic-string": "^0.30.21",
2443
+ "mlly": "^1.8.0",
2444
+ "muggle-string": "^0.4.1",
2445
+ "pathe": "^2.0.3",
2446
+ "picomatch": "^4.0.3",
2447
+ "scule": "^1.3.0",
2448
+ "tinyglobby": "^0.2.15",
2449
+ "unplugin": "^3.0.0",
2450
+ "unplugin-utils": "^0.3.1",
2451
+ "yaml": "^2.8.2"
2452
+ },
2453
+ "funding": {
2454
+ "url": "https://github.com/sponsors/posva"
2455
+ },
2456
+ "peerDependencies": {
2457
+ "@pinia/colada": ">=0.21.2",
2458
+ "@vue/compiler-sfc": "^3.5.17",
2459
+ "pinia": "^3.0.4",
2460
+ "vue": "^3.5.0"
2461
+ },
2462
+ "peerDependenciesMeta": {
2463
+ "@pinia/colada": {
2464
+ "optional": true
2465
+ },
2466
+ "@vue/compiler-sfc": {
2467
+ "optional": true
2468
+ },
2469
+ "pinia": {
2470
+ "optional": true
2471
+ }
2472
+ }
2473
+ },
2474
+ "node_modules/vue-tsc": {
2475
+ "version": "3.2.6",
2476
+ "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.6.tgz",
2477
+ "integrity": "sha512-gYW/kWI0XrwGzd0PKc7tVB/qpdeAkIZLNZb10/InizkQjHjnT8weZ/vBarZoj4kHKbUTZT/bAVgoOr8x4NsQ/Q==",
2478
+ "dev": true,
2479
+ "license": "MIT",
2480
+ "dependencies": {
2481
+ "@volar/typescript": "2.4.28",
2482
+ "@vue/language-core": "3.2.6"
2483
+ },
2484
+ "bin": {
2485
+ "vue-tsc": "bin/vue-tsc.js"
2486
+ },
2487
+ "peerDependencies": {
2488
+ "typescript": ">=5.0.0"
2489
+ }
2490
+ },
2491
+ "node_modules/webpack-virtual-modules": {
2492
+ "version": "0.6.2",
2493
+ "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
2494
+ "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
2495
+ "license": "MIT"
2496
+ },
2497
+ "node_modules/yaml": {
2498
+ "version": "2.8.3",
2499
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
2500
+ "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
2501
+ "license": "ISC",
2502
+ "bin": {
2503
+ "yaml": "bin.mjs"
2504
+ },
2505
+ "engines": {
2506
+ "node": ">= 14.6"
2507
+ },
2508
+ "funding": {
2509
+ "url": "https://github.com/sponsors/eemeli"
2510
+ }
2511
+ },
2512
+ "node_modules/zod": {
2513
+ "version": "4.3.6",
2514
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
2515
+ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
2516
+ "license": "MIT",
2517
+ "funding": {
2518
+ "url": "https://github.com/sponsors/colinhacks"
2519
+ }
2520
+ },
2521
+ "node_modules/zrender": {
2522
+ "version": "6.0.0",
2523
+ "resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz",
2524
+ "integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==",
2525
+ "license": "BSD-3-Clause",
2526
+ "dependencies": {
2527
+ "tslib": "2.3.0"
2528
+ }
2529
+ },
2530
+ "node_modules/zrender/node_modules/tslib": {
2531
+ "version": "2.3.0",
2532
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
2533
+ "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
2534
+ "license": "0BSD"
2535
+ }
2536
+ }
2537
+ }
vivek/frontend-vue/package.json ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend-vue",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vue-tsc -b && vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@heroicons/vue": "^2.2.0",
13
+ "@motionone/vue": "^10.16.4",
14
+ "@tailwindcss/vite": "^4.2.2",
15
+ "class-variance-authority": "^0.7.1",
16
+ "clsx": "^2.1.1",
17
+ "dompurify": "^3.3.3",
18
+ "echarts": "^6.0.0",
19
+ "gsap": "^3.14.2",
20
+ "lucide-vue-next": "^1.0.0",
21
+ "markdown-it": "^14.1.1",
22
+ "markdown-it-multimd-table": "^4.2.3",
23
+ "plotly.js-dist-min": "^3.4.0",
24
+ "radix-vue": "^1.9.17",
25
+ "tailwind-merge": "^3.5.0",
26
+ "tailwindcss": "^4.2.2",
27
+ "tw-animate-css": "^1.4.0",
28
+ "vue": "^3.5.30",
29
+ "vue-router": "^5.0.4",
30
+ "zod": "^4.3.6"
31
+ },
32
+ "devDependencies": {
33
+ "@types/markdown-it": "^14.1.2",
34
+ "@types/node": "^24.12.0",
35
+ "@vitejs/plugin-vue": "^6.0.5",
36
+ "@vue/tsconfig": "^0.9.0",
37
+ "typescript": "~5.9.3",
38
+ "vite": "^8.0.1",
39
+ "vue-tsc": "^3.2.5"
40
+ }
41
+ }
vivek/frontend-vue/public/favicon.svg ADDED
vivek/frontend-vue/public/icons.svg ADDED
vivek/frontend-vue/src/App.vue ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <router-view />
3
+ <ToastHost />
4
+ </template>
5
+
6
+ <script setup lang="ts">
7
+ import ToastHost from '@/components/ToastHost.vue'
8
+ </script>
vivek/frontend-vue/src/assets/bg-about.svg ADDED
vivek/frontend-vue/src/assets/bg-analytics.svg ADDED
vivek/frontend-vue/src/assets/bg-future.svg ADDED
vivek/frontend-vue/src/assets/hero.png ADDED
vivek/frontend-vue/src/assets/vite.svg ADDED
vivek/frontend-vue/src/assets/vue.svg ADDED
vivek/frontend-vue/src/components/AnalyticsPanel.vue ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang="ts">
2
+ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
3
+ import * as echarts from 'echarts'
4
+ import {
5
+ analyticsState,
6
+ computeAvgResponseTimeSec,
7
+ computeLastResponseTimes,
8
+ computeRewardPieLast,
9
+ computeRewardRateLast,
10
+ computeStrategyCounts,
11
+ computeWidgetRenderRate,
12
+ } from '@/lib/analyticsStore'
13
+
14
+ const strategyChartEl = ref<HTMLDivElement | null>(null)
15
+ const rewardChartEl = ref<HTMLDivElement | null>(null)
16
+ const responseTimeChartEl = ref<HTMLDivElement | null>(null)
17
+ const widgetRateChartEl = ref<HTMLDivElement | null>(null)
18
+
19
+ let strategyChart: echarts.ECharts | null = null
20
+ let rewardChart: echarts.ECharts | null = null
21
+ let responseTimeChart: echarts.ECharts | null = null
22
+ let widgetRateChart: echarts.ECharts | null = null
23
+
24
+ const chartWindowSize = 30
25
+ const rewardWindowSize = 50
26
+
27
+ const metrics = computed(() => {
28
+ const done = analyticsState.doneEvents
29
+ const widgetRenderedCount = done.filter((d) => (d.widgetHtml ?? '').trim().length > 0).length
30
+ const widgetTotal = done.length
31
+
32
+ return {
33
+ widgetRenderRate: computeWidgetRenderRate(),
34
+ avgResponseTime: computeAvgResponseTimeSec(),
35
+ rewardRate: computeRewardRateLast(rewardWindowSize),
36
+ strategyCounts: computeStrategyCounts(chartWindowSize),
37
+ responseTimes: computeLastResponseTimes(chartWindowSize),
38
+ rewardPie: computeRewardPieLast(rewardWindowSize),
39
+ widgetCounts: { rendered: widgetRenderedCount, total: widgetTotal },
40
+ }
41
+ })
42
+
43
+ function updateCharts() {
44
+ if (!strategyChart || !rewardChart || !responseTimeChart || !widgetRateChart) return
45
+
46
+ const stratKeys = Object.keys(metrics.value.strategyCounts)
47
+ const stratVals = stratKeys.map((k) => metrics.value.strategyCounts[k] ?? 0)
48
+
49
+ strategyChart.setOption(
50
+ {
51
+ tooltip: { trigger: 'axis' },
52
+ xAxis: { type: 'category', data: stratKeys.length ? stratKeys : ['—'] },
53
+ yAxis: { type: 'value' },
54
+ series: [
55
+ {
56
+ data: stratVals.length ? stratVals : [0],
57
+ type: 'bar',
58
+ itemStyle: { color: '#aa3bff' },
59
+ },
60
+ ],
61
+ },
62
+ true,
63
+ )
64
+
65
+ const rp = metrics.value.rewardPie
66
+ const totalRewards = (rp.pos ?? 0) + (rp.neg ?? 0)
67
+ rewardChart.setOption(
68
+ {
69
+ tooltip: { trigger: 'item' },
70
+ series: [
71
+ {
72
+ type: 'pie',
73
+ radius: ['55%', '80%'],
74
+ avoidLabelOverlap: true,
75
+ label: { formatter: '{b}: {c}' },
76
+ data:
77
+ totalRewards > 0
78
+ ? [
79
+ { name: 'Positive', value: rp.pos },
80
+ { name: 'Negative', value: rp.neg },
81
+ ]
82
+ : [{ name: 'No rewards', value: 1 }],
83
+ },
84
+ ],
85
+ },
86
+ true,
87
+ )
88
+
89
+ const respTimes = metrics.value.responseTimes
90
+ responseTimeChart.setOption(
91
+ {
92
+ tooltip: { trigger: 'axis' },
93
+ xAxis: { type: 'category', data: respTimes.length ? respTimes.map((d) => String(d.idx)) : ['—'] },
94
+ yAxis: { type: 'value' },
95
+ series: [
96
+ {
97
+ data: respTimes.length ? respTimes.map((d) => d.elapsed) : [0],
98
+ type: 'line',
99
+ smooth: true,
100
+ symbol: 'circle',
101
+ },
102
+ ],
103
+ },
104
+ true,
105
+ )
106
+
107
+ const wc = metrics.value.widgetCounts
108
+ const noWidget = wc.total - wc.rendered
109
+ widgetRateChart.setOption(
110
+ {
111
+ tooltip: { trigger: 'item' },
112
+ series: [
113
+ {
114
+ type: 'pie',
115
+ radius: ['55%', '80%'],
116
+ label: { formatter: '{b}: {c}' },
117
+ data:
118
+ wc.total > 0
119
+ ? [
120
+ { name: 'Widgets', value: wc.rendered },
121
+ { name: 'No widget', value: Math.max(0, noWidget) },
122
+ ]
123
+ : [{ name: 'No data', value: 1 }],
124
+ },
125
+ ],
126
+ },
127
+ true,
128
+ )
129
+ }
130
+
131
+ function resizeCharts() {
132
+ strategyChart?.resize()
133
+ rewardChart?.resize()
134
+ responseTimeChart?.resize()
135
+ widgetRateChart?.resize()
136
+ }
137
+
138
+ onMounted(() => {
139
+ if (strategyChartEl.value) strategyChart = echarts.init(strategyChartEl.value)
140
+ if (rewardChartEl.value) rewardChart = echarts.init(rewardChartEl.value)
141
+ if (responseTimeChartEl.value) responseTimeChart = echarts.init(responseTimeChartEl.value)
142
+ if (widgetRateChartEl.value) widgetRateChart = echarts.init(widgetRateChartEl.value)
143
+
144
+ updateCharts()
145
+
146
+ watch(
147
+ () => [analyticsState.doneEvents.length, analyticsState.rewardEvents.length],
148
+ () => updateCharts(),
149
+ )
150
+
151
+ window.addEventListener('resize', resizeCharts)
152
+ })
153
+
154
+ onBeforeUnmount(() => {
155
+ window.removeEventListener('resize', resizeCharts)
156
+ strategyChart?.dispose()
157
+ rewardChart?.dispose()
158
+ responseTimeChart?.dispose()
159
+ widgetRateChart?.dispose()
160
+ })
161
+ </script>
162
+
163
+ <template>
164
+ <div>
165
+ <h2 class="text-sm font-semibold mb-3">Analytics (live)</h2>
166
+
167
+ <div class="grid grid-cols-1 gap-3">
168
+ <div class="rounded-lg border bg-card p-3">
169
+ <div class="text-xs text-muted-foreground mb-2">Strategy distribution (last {{ chartWindowSize }})</div>
170
+ <div ref="strategyChartEl" class="h-[160px] w-full" />
171
+ </div>
172
+
173
+ <div class="rounded-lg border bg-card p-3">
174
+ <div class="text-xs text-muted-foreground mb-2">
175
+ Reward rate (last {{ rewardWindowSize }})
176
+ <span v-if="metrics.rewardRate !== null" class="ml-2 font-medium text-foreground">
177
+ {{ Math.round((metrics.rewardRate as number) * 100) }}%
178
+ </span>
179
+ </div>
180
+ <div ref="rewardChartEl" class="h-[160px] w-full" />
181
+ </div>
182
+
183
+ <div class="rounded-lg border bg-card p-3">
184
+ <div class="text-xs text-muted-foreground mb-2">
185
+ Avg response time
186
+ <span v-if="metrics.avgResponseTime !== null" class="ml-2 font-medium text-foreground">
187
+ {{ (metrics.avgResponseTime as number).toFixed(2) }}s
188
+ </span>
189
+ </div>
190
+ <div ref="responseTimeChartEl" class="h-[160px] w-full" />
191
+ </div>
192
+
193
+ <div class="rounded-lg border bg-card p-3">
194
+ <div class="text-xs text-muted-foreground mb-2">
195
+ Widget render rate
196
+ <span class="ml-2 font-medium text-foreground" v-if="metrics.widgetCounts.total > 0">
197
+ {{ Math.round(metrics.widgetRenderRate * 100) }}%
198
+ </span>
199
+ </div>
200
+ <div ref="widgetRateChartEl" class="h-[160px] w-full" />
201
+ </div>
202
+ </div>
203
+ </div>
204
+ </template>
205
+
vivek/frontend-vue/src/components/BanditInsightsSection.vue ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { banditState } from '@/lib/banditState'
4
+ import { sessionUserState } from '@/lib/sessionUser'
5
+ import { FEATURE_NAMES, type PosteriorMap } from '@/lib/strategies'
6
+ import { enabledStrategyIds, getStrategyLabel } from '@/lib/strategiesStore'
7
+
8
+ function barPct(s: string, data: PosteriorMap) {
9
+ const d = data[s]
10
+ if (!d) return 0
11
+ return Math.round((d.r ?? 0) * 100)
12
+ }
13
+
14
+ function uncPct(s: string, data: PosteriorMap) {
15
+ const d = data[s]
16
+ if (!d) return 50
17
+ return Math.min((d.u ?? 0) / 8, 1) * 100
18
+ }
19
+
20
+ function dimRow(s: string) {
21
+ if (!banditState.selectedStrategy) return false
22
+ return s !== banditState.selectedStrategy
23
+ }
24
+
25
+ const activeLabel = computed(() => {
26
+ const s = banditState.activeStrategy
27
+ if (!s) return '—'
28
+ return getStrategyLabel(s) || s
29
+ })
30
+ const currentUserLabel = computed(() => {
31
+ return sessionUserState.username ? `${sessionUserState.username} posterior` : 'Your posterior'
32
+ })
33
+
34
+ const hasScores = computed(() => banditState.scores && Object.keys(banditState.scores).length > 0)
35
+ const hasFeatures = computed(() => (banditState.lastXVec?.length ?? 0) > 0)
36
+ </script>
37
+
38
+ <template>
39
+ <section class="space-y-4">
40
+ <div class="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-2">
41
+ <div>
42
+ <h2 class="text-lg font-semibold tracking-tight text-slate-900 dark:text-foreground">Bandit intelligence</h2>
43
+ <p class="text-sm text-slate-500 dark:text-muted-foreground mt-0.5">
44
+ Live posteriors, global prior, and reward history — same signals as the chat insights panel.
45
+ </p>
46
+ </div>
47
+ <div
48
+ class="text-xs text-slate-500 dark:text-muted-foreground rounded-full border border-violet-200/80 dark:border-violet-900/50 bg-violet-50/80 dark:bg-violet-950/30 px-3 py-1.5"
49
+ >
50
+ α = 0.05 global blend · synced from chat + API
51
+ </div>
52
+ </div>
53
+
54
+ <!-- KPI strip (Apexify-style) -->
55
+ <div class="grid grid-cols-2 lg:grid-cols-4 gap-3">
56
+ <div
57
+ class="rounded-2xl border border-slate-200/90 dark:border-border bg-white dark:bg-card p-4 shadow-sm"
58
+ :class="banditState.activeStrategy ? 'ring-1 ring-violet-300/70 dark:ring-violet-800/60' : ''"
59
+ >
60
+ <div class="text-[11px] font-medium uppercase tracking-wide text-slate-500 dark:text-muted-foreground">Active strategy</div>
61
+ <div class="text-xl font-semibold mt-2 text-slate-900 dark:text-foreground truncate">{{ activeLabel }}</div>
62
+ <div class="text-xs text-slate-500 dark:text-muted-foreground mt-1 line-clamp-2">
63
+ {{ banditState.activeInstruction || 'Waiting for first message…' }}
64
+ </div>
65
+ </div>
66
+ <div class="rounded-2xl border border-slate-200/90 dark:border-border bg-white dark:bg-card p-4 shadow-sm">
67
+ <div class="text-[11px] font-medium uppercase tracking-wide text-slate-500 dark:text-muted-foreground">Global updates</div>
68
+ <div class="text-2xl font-semibold mt-2 text-slate-900 dark:text-foreground">{{ banditState.globalN }}</div>
69
+ <div class="text-xs text-slate-500 dark:text-muted-foreground mt-1">Reward-driven prior updates</div>
70
+ </div>
71
+ <div class="rounded-2xl border border-slate-200/90 dark:border-border bg-white dark:bg-card p-4 shadow-sm">
72
+ <div class="text-[11px] font-medium uppercase tracking-wide text-slate-500 dark:text-muted-foreground">Users</div>
73
+ <div class="text-2xl font-semibold mt-2 text-slate-900 dark:text-foreground">{{ banditState.nUsers }}</div>
74
+ <div class="text-xs text-slate-500 dark:text-muted-foreground mt-1">Bandit users in engine</div>
75
+ </div>
76
+ <div class="rounded-2xl border border-slate-200/90 dark:border-border bg-white dark:bg-card p-4 shadow-sm">
77
+ <div class="text-[11px] font-medium uppercase tracking-wide text-slate-500 dark:text-muted-foreground">Messages</div>
78
+ <div class="text-2xl font-semibold mt-2 text-slate-900 dark:text-foreground">
79
+ {{ banditState.msgCount == null ? '—' : banditState.msgCount }}
80
+ </div>
81
+ <div class="text-xs text-slate-500 dark:text-muted-foreground mt-1">Adaptive turns (this account)</div>
82
+ </div>
83
+ </div>
84
+
85
+ <!-- Thompson scores -->
86
+ <div
87
+ v-if="hasScores"
88
+ class="rounded-2xl border border-slate-200/90 dark:border-border bg-white dark:bg-card p-5 shadow-sm"
89
+ >
90
+ <div class="flex items-center justify-between mb-4">
91
+ <h3 class="text-sm font-semibold text-slate-900 dark:text-foreground">Thompson sampling — this turn</h3>
92
+ </div>
93
+ <div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-x-6 gap-y-2">
94
+ <div v-for="s in enabledStrategyIds" :key="'ts-' + s" class="flex justify-between text-sm gap-3">
95
+ <span class="text-slate-600 dark:text-muted-foreground truncate">{{ getStrategyLabel(s) }}</span>
96
+ <span class="font-mono text-slate-900 dark:text-foreground tabular-nums">
97
+ {{ banditState.scores?.[s] != null ? banditState.scores![s].toFixed(3) : '—' }}
98
+ </span>
99
+ </div>
100
+ </div>
101
+ </div>
102
+
103
+ <div class="grid grid-cols-1 xl:grid-cols-2 gap-4">
104
+ <!-- Your posterior -->
105
+ <div class="rounded-2xl border border-slate-200/90 dark:border-border bg-white dark:bg-card p-5 shadow-sm">
106
+ <h3 class="text-sm font-semibold text-slate-900 dark:text-foreground mb-1">{{ currentUserLabel }}</h3>
107
+ <p class="text-xs text-slate-500 dark:text-muted-foreground mb-4">P(r=1 | x, a=k) — mean and uncertainty</p>
108
+ <div class="space-y-3">
109
+ <div v-for="s in enabledStrategyIds" :key="'u-' + s" class="group">
110
+ <div class="flex justify-between text-xs mb-1">
111
+ <span class="text-slate-700 dark:text-foreground/90" :class="dimRow(s) ? 'opacity-50' : ''">
112
+ {{ getStrategyLabel(s) }}
113
+ </span>
114
+ <span class="font-mono font-medium text-slate-900 dark:text-foreground tabular-nums">{{ barPct(s, banditState.userPosterior) }}%</span>
115
+ </div>
116
+ <div class="h-2 rounded-full bg-slate-100 dark:bg-muted overflow-hidden">
117
+ <div
118
+ class="h-full rounded-full bg-gradient-to-r from-violet-500 to-violet-600 transition-all"
119
+ :style="{ width: barPct(s, banditState.userPosterior) + '%' }"
120
+ />
121
+ </div>
122
+ <div class="flex items-center gap-2 mt-1 text-[10px] text-slate-500">
123
+ <span class="w-16 shrink-0">Uncertainty</span>
124
+ <div class="flex-1 h-1.5 rounded-full bg-slate-100 dark:bg-muted overflow-hidden">
125
+ <div class="h-full rounded-full bg-amber-400/90" :style="{ width: uncPct(s, banditState.userPosterior) + '%' }" />
126
+ </div>
127
+ </div>
128
+ </div>
129
+ </div>
130
+ </div>
131
+
132
+ <!-- Global prior -->
133
+ <div class="rounded-2xl border border-slate-200/90 dark:border-border bg-white dark:bg-card p-5 shadow-sm">
134
+ <h3 class="text-sm font-semibold text-slate-900 dark:text-foreground mb-1">Global prior — all users</h3>
135
+ <p class="text-xs text-slate-500 dark:text-muted-foreground leading-relaxed mb-4">
136
+ Shared knowledge. Every reward feeds back here at α=0.05.
137
+ </p>
138
+ <div class="flex gap-8 mb-4">
139
+ <div>
140
+ <div class="text-2xl font-semibold text-slate-900 dark:text-foreground">{{ banditState.globalN }}</div>
141
+ <div class="text-[10px] uppercase tracking-wide text-slate-500">updates</div>
142
+ </div>
143
+ <div>
144
+ <div class="text-2xl font-semibold text-slate-900 dark:text-foreground">{{ banditState.nUsers }}</div>
145
+ <div class="text-[10px] uppercase tracking-wide text-slate-500">users</div>
146
+ </div>
147
+ </div>
148
+ <div class="space-y-3">
149
+ <div v-for="s in enabledStrategyIds" :key="'g-' + s">
150
+ <div class="flex justify-between text-xs mb-1">
151
+ <span class="text-slate-700 dark:text-foreground/90">{{ getStrategyLabel(s) }}</span>
152
+ <span class="font-mono tabular-nums text-slate-900 dark:text-foreground">{{ barPct(s, banditState.globalPosterior) }}%</span>
153
+ </div>
154
+ <div class="h-2 rounded-full bg-slate-100 dark:bg-muted overflow-hidden">
155
+ <div
156
+ class="h-full rounded-full bg-gradient-to-r from-emerald-500 to-teal-500"
157
+ :style="{ width: barPct(s, banditState.globalPosterior) + '%' }"
158
+ />
159
+ </div>
160
+ </div>
161
+ </div>
162
+ </div>
163
+ </div>
164
+
165
+ <!-- Baseline synthetic user -->
166
+ <div class="rounded-2xl border border-slate-200/90 dark:border-border bg-white dark:bg-card p-5 shadow-sm">
167
+ <h3 class="text-sm font-semibold text-slate-900 dark:text-foreground mb-1">Baseline user — inheriting prior</h3>
168
+ <p class="text-xs text-slate-500 dark:text-muted-foreground mb-4 max-w-3xl">
169
+ Synthetic fresh-session baseline that tracks the shared global posterior. Bars move when the global prior changes after real reward updates.
170
+ </p>
171
+ <div class="grid sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
172
+ <div v-for="s in enabledStrategyIds" :key="'b-' + s">
173
+ <div class="flex justify-between text-xs mb-1">
174
+ <span class="text-slate-700 dark:text-foreground/90 truncate pr-2">{{ getStrategyLabel(s) }}</span>
175
+ <span class="font-mono tabular-nums shrink-0">{{ barPct(s, banditState.userBPosterior) }}%</span>
176
+ </div>
177
+ <div class="h-2 rounded-full bg-slate-100 dark:bg-muted overflow-hidden">
178
+ <div
179
+ class="h-full rounded-full bg-gradient-to-r from-fuchsia-500 to-violet-600"
180
+ :style="{ width: barPct(s, banditState.userBPosterior) + '%' }"
181
+ />
182
+ </div>
183
+ </div>
184
+ </div>
185
+ </div>
186
+
187
+ <!-- Feature vector -->
188
+ <div
189
+ v-if="hasFeatures"
190
+ class="rounded-2xl border border-slate-200/90 dark:border-border bg-white dark:bg-card p-5 shadow-sm"
191
+ >
192
+ <h3 class="text-sm font-semibold text-slate-900 dark:text-foreground mb-4">Feature vector x ∈ ℝ¹⁰</h3>
193
+ <div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-2 text-sm">
194
+ <template v-for="(name, i) in FEATURE_NAMES" :key="name">
195
+ <div class="flex justify-between gap-4 border-b border-slate-100 dark:border-border/60 py-1.5 last:border-0">
196
+ <span class="text-slate-600 dark:text-muted-foreground font-mono text-xs">{{ name }}</span>
197
+ <span class="font-mono text-slate-900 dark:text-foreground tabular-nums">
198
+ {{ banditState.lastXVec?.[i] != null ? (banditState.lastXVec![i] as number).toFixed(3) : '—' }}
199
+ </span>
200
+ </div>
201
+ </template>
202
+ </div>
203
+ </div>
204
+
205
+ <!-- Reward log table -->
206
+ <div class="rounded-2xl border border-slate-200/90 dark:border-border bg-white dark:bg-card shadow-sm overflow-hidden">
207
+ <div class="px-5 py-4 border-b border-slate-100 dark:border-border flex items-center justify-between">
208
+ <div>
209
+ <h3 class="text-sm font-semibold text-slate-900 dark:text-foreground">Reward log</h3>
210
+ <p class="text-xs text-slate-500 dark:text-muted-foreground mt-0.5">Auto + manual feedback from chat</p>
211
+ </div>
212
+ </div>
213
+ <div class="overflow-x-auto">
214
+ <table class="w-full text-sm">
215
+ <thead>
216
+ <tr class="text-left border-b border-slate-100 dark:border-border bg-slate-50/80 dark:bg-muted/30">
217
+ <th class="py-3 px-5 font-medium text-slate-500 dark:text-muted-foreground">Strategy</th>
218
+ <th class="py-3 px-5 font-medium text-slate-500 dark:text-muted-foreground">Reward</th>
219
+ <th class="py-3 px-5 font-medium text-slate-500 dark:text-muted-foreground">Detail</th>
220
+ <th class="py-3 px-5 font-medium text-slate-500 dark:text-muted-foreground">Source</th>
221
+ </tr>
222
+ </thead>
223
+ <tbody>
224
+ <tr
225
+ v-for="(e, i) in banditState.rewardLog"
226
+ :key="i"
227
+ class="border-b border-slate-100 dark:border-border/60 last:border-0 hover:bg-slate-50/50 dark:hover:bg-muted/20"
228
+ >
229
+ <td class="py-3 px-5 font-medium text-slate-900 dark:text-foreground">
230
+ {{ getStrategyLabel(e.strategy) }}
231
+ </td>
232
+ <td class="py-3 px-5">
233
+ <span
234
+ class="inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium"
235
+ :class="e.reward >= 0.5 ? 'bg-emerald-50 text-emerald-800 dark:bg-emerald-950/40 dark:text-emerald-300' : 'bg-red-50 text-red-800 dark:bg-red-950/40 dark:text-red-300'"
236
+ >
237
+ <span class="h-1.5 w-1.5 rounded-full" :class="e.reward >= 0.5 ? 'bg-emerald-500' : 'bg-red-500'" />
238
+ {{ e.reward.toFixed(2) }}
239
+ </span>
240
+ </td>
241
+ <td class="py-3 px-5 text-slate-600 dark:text-muted-foreground max-w-md truncate">{{ e.detail }}</td>
242
+ <td class="py-3 px-5 text-slate-500 dark:text-muted-foreground text-xs capitalize">{{ e.source }}</td>
243
+ </tr>
244
+ <tr v-if="banditState.rewardLog.length === 0">
245
+ <td class="py-10 px-5 text-center text-slate-500 dark:text-muted-foreground" colspan="4">
246
+ No reward events yet. Send messages and use helpful / not helpful in chat.
247
+ </td>
248
+ </tr>
249
+ </tbody>
250
+ </table>
251
+ </div>
252
+ </div>
253
+ </section>
254
+ </template>
vivek/frontend-vue/src/components/HelloWorld.vue ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang="ts">
2
+ import { ref } from 'vue'
3
+ import viteLogo from '../assets/vite.svg'
4
+ import heroImg from '../assets/hero.png'
5
+ import vueLogo from '../assets/vue.svg'
6
+
7
+ const count = ref(0)
8
+ </script>
9
+
10
+ <template>
11
+ <section id="center">
12
+ <div class="hero">
13
+ <img :src="heroImg" class="base" width="170" height="179" alt="" />
14
+ <img :src="vueLogo" class="framework" alt="Vue logo" />
15
+ <img :src="viteLogo" class="vite" alt="Vite logo" />
16
+ </div>
17
+ <div>
18
+ <h1>Get started</h1>
19
+ <p>Edit <code>src/App.vue</code> and save to test <code>HMR</code></p>
20
+ </div>
21
+ <button class="counter" @click="count++">Count is {{ count }}</button>
22
+ </section>
23
+
24
+ <div class="ticks"></div>
25
+
26
+ <section id="next-steps">
27
+ <div id="docs">
28
+ <svg class="icon" role="presentation" aria-hidden="true">
29
+ <use href="/icons.svg#documentation-icon"></use>
30
+ </svg>
31
+ <h2>Documentation</h2>
32
+ <p>Your questions, answered</p>
33
+ <ul>
34
+ <li>
35
+ <a href="https://vite.dev/" target="_blank">
36
+ <img class="logo" :src="viteLogo" alt="" />
37
+ Explore Vite
38
+ </a>
39
+ </li>
40
+ <li>
41
+ <a href="https://vuejs.org/" target="_blank">
42
+ <img class="button-icon" :src="vueLogo" alt="" />
43
+ Learn more
44
+ </a>
45
+ </li>
46
+ </ul>
47
+ </div>
48
+ <div id="social">
49
+ <svg class="icon" role="presentation" aria-hidden="true">
50
+ <use href="/icons.svg#social-icon"></use>
51
+ </svg>
52
+ <h2>Connect with us</h2>
53
+ <p>Join the Vite community</p>
54
+ <ul>
55
+ <li>
56
+ <a href="https://github.com/vitejs/vite" target="_blank">
57
+ <svg class="button-icon" role="presentation" aria-hidden="true">
58
+ <use href="/icons.svg#github-icon"></use>
59
+ </svg>
60
+ GitHub
61
+ </a>
62
+ </li>
63
+ <li>
64
+ <a href="https://chat.vite.dev/" target="_blank">
65
+ <svg class="button-icon" role="presentation" aria-hidden="true">
66
+ <use href="/icons.svg#discord-icon"></use>
67
+ </svg>
68
+ Discord
69
+ </a>
70
+ </li>
71
+ <li>
72
+ <a href="https://x.com/vite_js" target="_blank">
73
+ <svg class="button-icon" role="presentation" aria-hidden="true">
74
+ <use href="/icons.svg#x-icon"></use>
75
+ </svg>
76
+ X.com
77
+ </a>
78
+ </li>
79
+ <li>
80
+ <a href="https://bsky.app/profile/vite.dev" target="_blank">
81
+ <svg class="button-icon" role="presentation" aria-hidden="true">
82
+ <use href="/icons.svg#bluesky-icon"></use>
83
+ </svg>
84
+ Bluesky
85
+ </a>
86
+ </li>
87
+ </ul>
88
+ </div>
89
+ </section>
90
+
91
+ <div class="ticks"></div>
92
+ <section id="spacer"></section>
93
+ </template>
vivek/frontend-vue/src/components/LiveWidgetFrame.vue ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang="ts">
2
+ import { computed, ref, watch } from 'vue'
3
+ import { buildProgressiveHtmlDoc } from '@/lib/progressiveWidget'
4
+
5
+ /**
6
+ * Progressive iframe renderer for HTML-mode widgets. While content streams,
7
+ * srcdoc is updated at a throttled rate (~120ms) so the browser re-renders
8
+ * without flickering. When `finalized` goes true, we swap to the polished
9
+ * final HTML so design-system injection + truncation fixes take effect.
10
+ */
11
+ const props = defineProps<{
12
+ rawStream: string
13
+ finalHtml?: string
14
+ finalized: boolean
15
+ height?: number
16
+ }>()
17
+
18
+ const throttledDoc = ref('')
19
+ let lastWrite = 0
20
+ let pendingTimer: ReturnType<typeof setTimeout> | null = null
21
+
22
+ function scheduleWrite(next: string) {
23
+ const now = Date.now()
24
+ const THROTTLE_MS = 120
25
+ if (now - lastWrite >= THROTTLE_MS) {
26
+ throttledDoc.value = next
27
+ lastWrite = now
28
+ return
29
+ }
30
+ if (pendingTimer) clearTimeout(pendingTimer)
31
+ pendingTimer = setTimeout(() => {
32
+ throttledDoc.value = next
33
+ lastWrite = Date.now()
34
+ pendingTimer = null
35
+ }, THROTTLE_MS - (now - lastWrite))
36
+ }
37
+
38
+ watch(
39
+ () => [props.rawStream, props.finalized, props.finalHtml],
40
+ () => {
41
+ if (props.finalized && props.finalHtml) {
42
+ throttledDoc.value = props.finalHtml
43
+ return
44
+ }
45
+ const doc = buildProgressiveHtmlDoc(props.rawStream)
46
+ if (doc) scheduleWrite(doc)
47
+ },
48
+ { immediate: true },
49
+ )
50
+
51
+ const frameHeight = computed(() => {
52
+ const raw = Number(props.height || 420)
53
+ if (!Number.isFinite(raw)) return 420
54
+ return Math.min(Math.max(raw, 300), 520)
55
+ })
56
+ </script>
57
+
58
+ <template>
59
+ <div class="relative w-full">
60
+ <iframe
61
+ v-if="throttledDoc"
62
+ :srcdoc="throttledDoc"
63
+ sandbox="allow-scripts allow-same-origin"
64
+ class="w-full widget-frame border-0"
65
+ :style="{ height: `${frameHeight}px`, maxHeight: '56vh' }"
66
+ />
67
+ <div
68
+ v-else
69
+ class="w-full rounded-xl border bg-muted/20 skeleton"
70
+ :style="{ height: `${frameHeight}px`, maxHeight: '56vh' }"
71
+ />
72
+ <div
73
+ v-if="!finalized"
74
+ class="absolute top-2 right-2 text-[10px] px-2 py-1 rounded-full bg-cyan-500/90 text-white shadow-md flex items-center gap-1.5"
75
+ >
76
+ <span class="relative flex h-1.5 w-1.5">
77
+ <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-white opacity-75" />
78
+ <span class="relative inline-flex rounded-full h-1.5 w-1.5 bg-white" />
79
+ </span>
80
+ streaming
81
+ </div>
82
+ </div>
83
+ </template>
84
+
85
+ <style scoped>
86
+ .skeleton {
87
+ position: relative;
88
+ overflow: hidden;
89
+ background: hsl(var(--muted) / 0.3);
90
+ }
91
+ .skeleton::after {
92
+ content: '';
93
+ position: absolute;
94
+ inset: 0;
95
+ background: linear-gradient(
96
+ 90deg,
97
+ transparent 0%,
98
+ hsl(var(--muted) / 0.6) 50%,
99
+ transparent 100%
100
+ );
101
+ animation: shimmerSweep 1.4s linear infinite;
102
+ }
103
+ @keyframes shimmerSweep {
104
+ 0% {
105
+ transform: translateX(-100%);
106
+ }
107
+ 100% {
108
+ transform: translateX(100%);
109
+ }
110
+ }
111
+ </style>
vivek/frontend-vue/src/components/LiveWidgetSchema.vue ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { Motion } from '@motionone/vue'
4
+ import Button from '@/components/ui/Button.vue'
5
+ import WidgetSchemaChart from '@/components/WidgetSchemaChart.vue'
6
+ import { parseProgressiveWidgetSchema, type WidgetBlock } from '@/lib/progressiveWidget'
7
+
8
+ /**
9
+ * Renders widget blocks progressively as JSON streams in. Each block fades up
10
+ * independently (Claude-artifact style) and we show a soft "pending" block for
11
+ * whichever block the model is currently generating.
12
+ */
13
+ const props = defineProps<{
14
+ rawStream: string
15
+ finalized?: boolean
16
+ }>()
17
+
18
+ const state = computed(() => parseProgressiveWidgetSchema(props.rawStream))
19
+ const blocks = computed<WidgetBlock[]>(() => state.value.blocks)
20
+ const pendingType = computed<string | null>(() => (props.finalized ? null : state.value.pendingBlockHint))
21
+
22
+ function pendingLabel(t: string | null): string {
23
+ if (!t) return 'Building block'
24
+ const labels: Record<string, string> = {
25
+ text: 'Writing narrative',
26
+ kpi_row: 'Preparing KPIs',
27
+ chart: 'Rendering chart',
28
+ image: 'Loading image',
29
+ table: 'Composing table',
30
+ action_row: 'Wiring actions',
31
+ }
32
+ return labels[t] || `Building ${t}`
33
+ }
34
+ </script>
35
+
36
+ <template>
37
+ <div class="live-widget space-y-3 text-sm">
38
+ <template v-for="(block, i) in blocks" :key="`lw-${i}`">
39
+ <Motion
40
+ tag="div"
41
+ :initial="{ opacity: 0, y: 8, scale: 0.985 }"
42
+ :animate="{ opacity: 1, y: 0, scale: 1 }"
43
+ :transition="{ duration: 0.32, easing: [0.16, 1, 0.3, 1] }"
44
+ >
45
+ <div v-if="block.type === 'text'" class="whitespace-pre-wrap leading-relaxed">
46
+ {{ (block as { content?: string }).content || '' }}
47
+ </div>
48
+
49
+ <div v-else-if="block.type === 'kpi_row'" class="grid grid-cols-2 sm:grid-cols-3 gap-2">
50
+ <div
51
+ v-for="(it, j) in (block as { items?: { label?: string; value?: string; tone?: string }[] }).items || []"
52
+ :key="j"
53
+ class="rounded-xl border bg-card px-3 py-2 shadow-sm"
54
+ >
55
+ <div class="text-[10px] text-muted-foreground uppercase tracking-wide">{{ it.label }}</div>
56
+ <div
57
+ class="text-lg font-semibold mt-0.5"
58
+ :class="{
59
+ 'text-emerald-600 dark:text-emerald-400': it.tone === 'positive',
60
+ 'text-red-600 dark:text-red-400': it.tone === 'negative',
61
+ }"
62
+ >
63
+ {{ it.value }}
64
+ </div>
65
+ </div>
66
+ </div>
67
+
68
+ <WidgetSchemaChart
69
+ v-else-if="block.type === 'chart'"
70
+ :title="(block as { title?: string }).title"
71
+ :chart="(block as { chart?: Record<string, unknown> }).chart as any || {}"
72
+ />
73
+
74
+ <div v-else-if="block.type === 'image'" class="rounded-xl border bg-card overflow-hidden shadow-sm">
75
+ <div
76
+ v-if="(block as { title?: string }).title"
77
+ class="px-3 py-2 border-b text-xs font-medium"
78
+ >
79
+ {{ (block as { title?: string }).title }}
80
+ </div>
81
+ <div class="p-2 flex justify-center bg-muted/15">
82
+ <img
83
+ :src="(block as { src?: string }).src || ''"
84
+ :alt="(block as { alt?: string }).alt || 'Widget image'"
85
+ class="max-w-full max-h-[min(420px,55vh)] rounded-lg object-contain"
86
+ loading="lazy"
87
+ referrerpolicy="no-referrer"
88
+ />
89
+ </div>
90
+ </div>
91
+
92
+ <div v-else-if="block.type === 'table'" class="rounded-xl border bg-card overflow-hidden">
93
+ <div v-if="(block as { title?: string }).title" class="px-3 py-2 border-b text-xs font-medium">
94
+ {{ (block as { title?: string }).title }}
95
+ </div>
96
+ <div class="overflow-x-auto">
97
+ <table class="w-full text-xs">
98
+ <thead v-if="(block as { columns?: string[] }).columns?.length">
99
+ <tr>
100
+ <th
101
+ v-for="(c, ci) in (block as { columns?: string[] }).columns"
102
+ :key="ci"
103
+ class="text-left px-3 py-2 border-b bg-muted/40 font-medium"
104
+ >
105
+ {{ c }}
106
+ </th>
107
+ </tr>
108
+ </thead>
109
+ <tbody>
110
+ <tr v-for="(row, ri) in (block as { rows?: unknown[][] }).rows || []" :key="ri">
111
+ <td v-for="(cell, ci) in row" :key="ci" class="px-3 py-1.5 border-b border-border/60">
112
+ {{ cell }}
113
+ </td>
114
+ </tr>
115
+ </tbody>
116
+ </table>
117
+ </div>
118
+ </div>
119
+
120
+ <div v-else-if="block.type === 'action_row'" class="flex flex-wrap gap-2">
121
+ <Button
122
+ v-for="(b, bi) in (block as { buttons?: { label?: string; intent?: string }[] }).buttons || []"
123
+ :key="bi"
124
+ type="button"
125
+ variant="outline"
126
+ size="sm"
127
+ disabled
128
+ :title="(b as { intent?: string }).intent ? `Intent: ${(b as { intent?: string }).intent}` : undefined"
129
+ >
130
+ {{ b.label || 'Action' }}
131
+ </Button>
132
+ </div>
133
+
134
+ <div v-else class="rounded-lg border border-dashed border-muted-foreground/30 bg-muted/20 px-3 py-2 text-xs whitespace-pre-wrap">
135
+ {{ JSON.stringify(block, null, 2) }}
136
+ </div>
137
+ </Motion>
138
+ </template>
139
+
140
+ <Transition
141
+ enter-active-class="transition-all duration-300 ease-out"
142
+ enter-from-class="opacity-0 translate-y-1"
143
+ enter-to-class="opacity-100 translate-y-0"
144
+ leave-active-class="transition-all duration-200 ease-in"
145
+ leave-from-class="opacity-100 translate-y-0"
146
+ leave-to-class="opacity-0 -translate-y-1"
147
+ >
148
+ <div
149
+ v-if="pendingType"
150
+ class="pending-block rounded-xl border border-cyan-500/30 bg-cyan-500/5 dark:bg-cyan-950/25 px-3 py-2 flex items-center gap-3"
151
+ >
152
+ <span class="relative flex h-2.5 w-2.5 shrink-0">
153
+ <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-cyan-400 opacity-60" />
154
+ <span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-cyan-500" />
155
+ </span>
156
+ <span class="text-[12px] font-medium text-cyan-900 dark:text-cyan-100">
157
+ {{ pendingLabel(pendingType) }}…
158
+ </span>
159
+ <div class="shimmer flex-1 h-1.5 rounded-full bg-muted/40" />
160
+ </div>
161
+ </Transition>
162
+
163
+ <div v-if="!blocks.length && !pendingType" class="skeleton-stack space-y-2">
164
+ <div class="skeleton h-6 w-2/3 rounded-lg" />
165
+ <div class="skeleton h-20 w-full rounded-xl" />
166
+ <div class="skeleton h-4 w-1/2 rounded" />
167
+ </div>
168
+ </div>
169
+ </template>
170
+
171
+ <style scoped>
172
+ .shimmer {
173
+ position: relative;
174
+ overflow: hidden;
175
+ }
176
+ .shimmer::after {
177
+ content: '';
178
+ position: absolute;
179
+ inset: 0;
180
+ background: linear-gradient(
181
+ 90deg,
182
+ transparent 0%,
183
+ rgba(6, 182, 212, 0.35) 50%,
184
+ transparent 100%
185
+ );
186
+ animation: shimmerSweep 1.3s linear infinite;
187
+ }
188
+ .skeleton {
189
+ position: relative;
190
+ overflow: hidden;
191
+ background: hsl(var(--muted) / 0.45);
192
+ }
193
+ .skeleton::after {
194
+ content: '';
195
+ position: absolute;
196
+ inset: 0;
197
+ background: linear-gradient(
198
+ 90deg,
199
+ transparent 0%,
200
+ hsl(var(--muted) / 0.75) 50%,
201
+ transparent 100%
202
+ );
203
+ animation: shimmerSweep 1.4s linear infinite;
204
+ }
205
+ @keyframes shimmerSweep {
206
+ 0% {
207
+ transform: translateX(-100%);
208
+ }
209
+ 100% {
210
+ transform: translateX(100%);
211
+ }
212
+ }
213
+ </style>