Spaces:
Sleeping
Sleeping
Deploy vivek app to Space: Vue UI (bundled dist), streaming widgets, JSON schema renderer
Browse filesClean 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
- Dockerfile +3 -3
- README.md +4 -4
- anupa/app.py +0 -13
- anupa/backend/config.py +0 -86
- anupa/backend/primitive_widget_map.py +0 -476
- anupa/backend/primitives.json +0 -51
- anupa/backend/server.py +0 -650
- vivek/.env.example +44 -0
- {anupa → vivek}/.gitattributes +0 -0
- {anupa → vivek}/.gitignore +3 -0
- {anupa → vivek}/Dockerfile +0 -0
- {anupa → vivek}/README.md +31 -4
- {anupa → vivek}/UpdatedReadme.md +3 -2
- vivek/app.py +23 -0
- {anupa → vivek}/backend/__init__.py +0 -0
- vivek/backend/auth.py +179 -0
- {anupa → vivek}/backend/combined_prompt.py +424 -100
- vivek/backend/config.py +264 -0
- vivek/backend/db.py +774 -0
- {anupa → vivek}/backend/engine.py +80 -9
- {anupa → vivek}/backend/llm.py +0 -0
- vivek/backend/server.py +1399 -0
- {anupa → vivek}/backend/utils.py +36 -4
- {anupa → vivek}/backend/widget_prompt.py +16 -10
- vivek/backend/widget_stream.py +137 -0
- vivek/frontend-vue/.gitignore +24 -0
- vivek/frontend-vue/README.md +5 -0
- vivek/frontend-vue/components.json +20 -0
- vivek/frontend-vue/dist/assets/index-B2HHLelt.css +0 -0
- vivek/frontend-vue/dist/assets/index-Bq2Moqhb.js +0 -0
- vivek/frontend-vue/dist/favicon.svg +1 -0
- vivek/frontend-vue/dist/icons.svg +24 -0
- vivek/frontend-vue/dist/index.html +14 -0
- vivek/frontend-vue/index.html +13 -0
- vivek/frontend-vue/package-lock.json +2537 -0
- vivek/frontend-vue/package.json +41 -0
- vivek/frontend-vue/public/favicon.svg +1 -0
- vivek/frontend-vue/public/icons.svg +24 -0
- vivek/frontend-vue/src/App.vue +8 -0
- vivek/frontend-vue/src/assets/bg-about.svg +33 -0
- vivek/frontend-vue/src/assets/bg-analytics.svg +30 -0
- vivek/frontend-vue/src/assets/bg-future.svg +31 -0
- vivek/frontend-vue/src/assets/hero.png +0 -0
- vivek/frontend-vue/src/assets/vite.svg +1 -0
- vivek/frontend-vue/src/assets/vue.svg +1 -0
- vivek/frontend-vue/src/components/AnalyticsPanel.vue +205 -0
- vivek/frontend-vue/src/components/BanditInsightsSection.vue +254 -0
- vivek/frontend-vue/src/components/HelloWorld.vue +93 -0
- vivek/frontend-vue/src/components/LiveWidgetFrame.vue +111 -0
- vivek/frontend-vue/src/components/LiveWidgetSchema.vue +213 -0
Dockerfile
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
|
| 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 ./
|
| 10 |
RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt
|
| 11 |
|
| 12 |
-
COPY --chown=user ./
|
| 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 (
|
| 3 |
emoji: "🧠"
|
| 4 |
colorFrom: indigo
|
| 5 |
colorTo: blue
|
|
@@ -7,8 +7,8 @@ sdk: docker
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
# AdaptiveUI (
|
| 11 |
|
| 12 |
Claude-style single-call response + widget HTML rendered in an iframe.
|
| 13 |
|
| 14 |
-
This Space runs the `
|
|
|
|
| 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
|
| 22 |
-
# (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 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 |
-
#
|
| 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 |
-
├──
|
|
|
|
| 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 |
-
-
|
| 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 |
-
-
|
| 91 |
-
-
|
| 92 |
-
- ECharts
|
|
|
|
| 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
|
| 102 |
|
| 103 |
<RESPONSE>
|
| 104 |
...text...
|
| 105 |
</RESPONSE>
|
| 106 |
<WIDGET>
|
| 107 |
-
...widget...
|
| 108 |
</WIDGET>
|
| 109 |
|
| 110 |
Rules:
|
| 111 |
-
-
|
| 112 |
-
-
|
| 113 |
-
- If
|
| 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
|
| 122 |
-
|
| 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 |
-
|
| 130 |
-
-
|
| 131 |
-
-
|
| 132 |
-
-
|
| 133 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 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 |
-
-
|
| 163 |
-
|
| 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 |
-
-
|
|
|
|
| 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 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 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 |
-
|
| 268 |
-
|
| 269 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
═══════════════════════════════════════════════════════
|
| 271 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 295 |
-
-
|
| 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>.
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
{_OUTPUT_CONTRACT_STRICT}
|
| 324 |
|
| 325 |
-
For every response you produce TWO sections
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
-
|
| 379 |
-
-
|
| 380 |
-
-
|
| 381 |
-
-
|
|
|
|
| 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 |
-
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
| 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
|
| 30 |
-
"sigma_inv": {k: self.global_sinv[k].copy() for k in
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
except Exception:
|
| 147 |
pass
|
| 148 |
|
|
@@ -163,7 +176,10 @@ def enforce_response(strategy: str, text: str) -> str:
|
|
| 163 |
["B", "", "", ""],
|
| 164 |
],
|
| 165 |
}
|
| 166 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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:
|
| 80 |
-
th{background:var(--bg3);color:var(--text2);font-weight:500;font-size:
|
| 81 |
-
td{padding:
|
| 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') *
|
| 599 |
-
height += min(h.count('<tr'),
|
| 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,
|
| 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>
|