tfrere HF Staff commited on
Commit
d5079dd
·
0 Parent(s):

feat: initial Reachy Mini Vibe Coder space

Browse files

- Hono backend (Vercel AI SDK + OpenRouter) with HF OAuth middleware
- React frontend with Monaco editor, virtual FS, sandboxed preview iframe
- Embedded build-reachy-voice-app skill loaded at runtime for the agent
- /api/publish route streams createRepo + uploadFiles to a new HF Space
- Docker runtime with envsubst injection of OAuth vars at container start

Made-with: Cursor

Files changed (47) hide show
  1. .dockerignore +7 -0
  2. .gitignore +11 -0
  3. Dockerfile +67 -0
  4. README.md +133 -0
  5. backend/.env.example +12 -0
  6. backend/package-lock.json +1032 -0
  7. backend/package.json +26 -0
  8. backend/src/agent/skill-loader.ts +91 -0
  9. backend/src/agent/skills/build-reachy-voice-app/SKILL.md +92 -0
  10. backend/src/agent/skills/build-reachy-voice-app/architecture.md +108 -0
  11. backend/src/agent/skills/build-reachy-voice-app/deployment.md +196 -0
  12. backend/src/agent/skills/build-reachy-voice-app/motion-agents.md +129 -0
  13. backend/src/agent/skills/build-reachy-voice-app/protocols.md +132 -0
  14. backend/src/agent/skills/build-reachy-voice-app/webrtc-tuning.md +162 -0
  15. backend/src/agent/system-prompt.ts +114 -0
  16. backend/src/agent/tools.ts +115 -0
  17. backend/src/auth.ts +99 -0
  18. backend/src/routes/chat.ts +87 -0
  19. backend/src/routes/models.ts +8 -0
  20. backend/src/routes/publish.ts +144 -0
  21. backend/src/server.ts +104 -0
  22. backend/src/types.ts +8 -0
  23. backend/tsconfig.json +19 -0
  24. docker-entrypoint.d/10-inject-hf-vars.sh +37 -0
  25. docker-entrypoint.d/entrypoint.sh +17 -0
  26. frontend/index.html +49 -0
  27. frontend/package-lock.json +2220 -0
  28. frontend/package.json +29 -0
  29. frontend/src/App.tsx +164 -0
  30. frontend/src/components/ChatPanel.tsx +219 -0
  31. frontend/src/components/CodeEditor.tsx +113 -0
  32. frontend/src/components/HfLogo.tsx +31 -0
  33. frontend/src/components/PreviewFrame.tsx +104 -0
  34. frontend/src/components/PublishDialog.tsx +209 -0
  35. frontend/src/components/SignInScreen.tsx +47 -0
  36. frontend/src/components/TopBar.tsx +94 -0
  37. frontend/src/hooks/useAgentChat.ts +186 -0
  38. frontend/src/hooks/useHfAuth.ts +142 -0
  39. frontend/src/hooks/usePublish.ts +155 -0
  40. frontend/src/hooks/useVirtualFS.ts +108 -0
  41. frontend/src/main.tsx +10 -0
  42. frontend/src/styles.css +798 -0
  43. frontend/src/vite-env.d.ts +20 -0
  44. frontend/tsconfig.app.json +22 -0
  45. frontend/tsconfig.json +7 -0
  46. frontend/tsconfig.node.json +15 -0
  47. frontend/vite.config.ts +24 -0
.dockerignore ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ **/node_modules
2
+ **/dist
3
+ **/.env
4
+ **/.env.local
5
+ .git
6
+ .gitignore
7
+ .DS_Store
.gitignore ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ node_modules/
2
+ dist/
3
+ .env
4
+ .env.local
5
+ *.log
6
+ *.tsbuildinfo
7
+ .DS_Store
8
+ backend/dist/
9
+ frontend/dist/
10
+ .vscode/
11
+ .idea/
Dockerfile ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # syntax=docker/dockerfile:1.7
2
+ # ────────────────────────────────────────────────────────────────────────────
3
+ # Stage 1 · build the React frontend
4
+ # ────────────────────────────────────────────────────────────────────────────
5
+ FROM node:20-slim AS frontend-builder
6
+
7
+ WORKDIR /build/frontend
8
+
9
+ COPY frontend/package.json frontend/package-lock.json* ./
10
+ RUN npm ci || npm install
11
+
12
+ COPY frontend/tsconfig*.json frontend/vite.config.ts frontend/index.html ./
13
+ COPY frontend/src ./src
14
+
15
+ RUN npm run build
16
+
17
+ # ────────────────────────────────────────────────────────────────────────────
18
+ # Stage 2 · build the Hono backend
19
+ # ────────────────────────────────────────────────────────────────────────────
20
+ FROM node:20-slim AS backend-builder
21
+
22
+ WORKDIR /build/backend
23
+
24
+ COPY backend/package.json backend/package-lock.json* ./
25
+ RUN npm ci || npm install
26
+
27
+ COPY backend/tsconfig.json ./
28
+ COPY backend/src ./src
29
+
30
+ RUN npm run build && npm prune --omit=dev
31
+
32
+ # ────────────────────────────────────────────────────────────────────────────
33
+ # Stage 3 · runtime (node + built assets only)
34
+ # ────────────────────────────────────────────────────────────────────────────
35
+ FROM node:20-slim AS runtime
36
+
37
+ # `gettext-base` gives us `envsubst` for the OAuth vars entrypoint.
38
+ RUN apt-get update \
39
+ && apt-get install -y --no-install-recommends gettext-base tini \
40
+ && rm -rf /var/lib/apt/lists/*
41
+
42
+ WORKDIR /app
43
+
44
+ # Backend runtime (compiled JS + production node_modules)
45
+ COPY --from=backend-builder /build/backend/dist /app/backend/dist
46
+ COPY --from=backend-builder /build/backend/node_modules /app/backend/node_modules
47
+ COPY --from=backend-builder /build/backend/package.json /app/backend/package.json
48
+
49
+ # Skills are loaded from source markdown at runtime (skill-loader.ts reads
50
+ # them from disk). Ship them alongside the compiled JS.
51
+ COPY backend/src/agent/skills /app/backend/dist/agent/skills
52
+
53
+ # Frontend static build - server.ts detects this folder automatically.
54
+ COPY --from=frontend-builder /build/frontend/dist /app/frontend/dist
55
+
56
+ # HF OAuth variables entrypoint
57
+ COPY docker-entrypoint.d /app/docker-entrypoint.d
58
+ RUN chmod +x /app/docker-entrypoint.d/*.sh
59
+
60
+ ENV NODE_ENV=production \
61
+ PORT=7860 \
62
+ REQUIRE_HF_AUTH=1
63
+
64
+ EXPOSE 7860
65
+
66
+ ENTRYPOINT ["/usr/bin/tini", "--", "/app/docker-entrypoint.d/entrypoint.sh"]
67
+ CMD ["node", "/app/backend/dist/server.js"]
README.md ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Reachy Mini Vibe Coder
3
+ emoji: 🤖
4
+ colorFrom: indigo
5
+ colorTo: pink
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ hf_oauth: true
10
+ hf_oauth_scopes:
11
+ - openid
12
+ - profile
13
+ - write-repos
14
+ - manage-repos
15
+ short_description: Vibe-code Reachy Mini voice apps and publish them to HF Spaces.
16
+ tags:
17
+ - reachy_mini
18
+ - agent
19
+ - vibe-coding
20
+ ---
21
+
22
+ # Reachy Mini · Vibe Coder
23
+
24
+ A conversational agent that helps you vibe-code browser-based voice apps for
25
+ Reachy Mini robots, then publishes them as Hugging Face Spaces in one click.
26
+
27
+ The agent is grounded in the `build-reachy-voice-app` skill, which documents
28
+ the full architecture (WebRTC 2-hop audio bridge, OpenAI Realtime event
29
+ lifecycle, motion agents, HF OAuth injection, Docker + Nginx deploy). Ask for
30
+ an app, the agent produces the `index.html`, `main.ts`, `style.css`, `Dockerfile`
31
+ and `README.md` that you need, lets you preview them live in a sandboxed
32
+ iframe, and publishes the result to a new HF Space under your account.
33
+
34
+ ## Architecture
35
+
36
+ ```
37
+ frontend/ Vite + React + @ai-sdk/react
38
+ src/
39
+ components/
40
+ TopBar.tsx HF user avatar, model picker, Publish button
41
+ ChatPanel.tsx Agent conversation
42
+ CodeEditor.tsx Monaco, one tab per virtual-FS file
43
+ PreviewFrame.tsx Sandboxed iframe running the generated app
44
+ hooks/
45
+ useAgentChat.ts useChat + client-side tool execution
46
+ useVirtualFS.ts In-memory file tree
47
+ useHfAuth.ts HF OAuth (hfSpacesOauthLogin)
48
+
49
+ backend/ Hono + Vercel AI SDK (ai + openrouter)
50
+ src/
51
+ server.ts Hono app, mounts /api + serves static frontend
52
+ auth.ts Verifies HF access tokens on /api/*
53
+ agent/
54
+ chat.ts streamText() with editor tools + skill tools
55
+ tools.ts writeFile, editFile, deleteFile, readFile, readSkillDoc
56
+ system-prompt.ts Base prompt + injected SKILL.md
57
+ skill-loader.ts Reads skills/ at runtime
58
+ skills/
59
+ build-reachy-voice-app/
60
+ SKILL.md (copied from reachy-mini-project-folder/.cursor/skills/)
61
+ architecture.md
62
+ deployment.md
63
+ motion-agents.md
64
+ protocols.md
65
+ webrtc-tuning.md
66
+ routes/
67
+ publish.ts @huggingface/hub createRepo + uploadFiles
68
+
69
+ docker-entrypoint.d/
70
+ 10-inject-hf-vars.sh envsubst OAUTH_CLIENT_ID / SPACE_* into dist/index.html
71
+ ```
72
+
73
+ ## Local development
74
+
75
+ ### Prerequisites
76
+
77
+ - Node.js 20+
78
+ - An [OpenRouter API key](https://openrouter.ai/keys) (backend)
79
+ - A Hugging Face OAuth app for local dev (see below)
80
+
81
+ ### HF OAuth for localhost
82
+
83
+ On a Space the OAuth client ID is injected automatically. For local dev:
84
+
85
+ 1. Register an OAuth app at <https://huggingface.co/settings/applications/new>
86
+ 2. Fill in:
87
+ - **Homepage URL**: `http://localhost:5173`
88
+ - **Scopes**: `openid`, `profile`, `write-repos`, `manage-repos`
89
+ - **Redirect URIs**: `http://localhost:5173`
90
+ 3. Copy the client ID into `frontend/.env.local`:
91
+ ```
92
+ VITE_HF_OAUTH_CLIENT_ID=xxxx-xxxx-xxxx
93
+ ```
94
+
95
+ ### Run
96
+
97
+ ```bash
98
+ # Terminal 1 - backend
99
+ cd backend
100
+ cp .env.example .env # fill OPENROUTER_API_KEY
101
+ npm install
102
+ npm run dev
103
+
104
+ # Terminal 2 - frontend
105
+ cd frontend
106
+ npm install
107
+ npm run dev
108
+ ```
109
+
110
+ Open <http://localhost:5173>.
111
+
112
+ ## Deploy to HF Spaces
113
+
114
+ Push this repo to a Space with `sdk: docker`. The `docker-entrypoint.d`
115
+ script injects the OAuth credentials from the Space runtime into the built
116
+ `index.html` at container start.
117
+
118
+ ```bash
119
+ git remote add space https://huggingface.co/spaces/<your-username>/reachy-mini-vibe-coder
120
+ git push space main
121
+ ```
122
+
123
+ ## Environment variables (Space secrets)
124
+
125
+ | Variable | Required | Description |
126
+ |----------|----------|-------------|
127
+ | `OPENROUTER_API_KEY` | yes | Powers the agent |
128
+ | `OPENROUTER_MODEL` | no | Default `anthropic/claude-sonnet-4-5` |
129
+
130
+ ## License
131
+
132
+ MIT. The embedded skill files are derived from the `reachy-mini-project-folder`
133
+ internal docs; the underlying SDKs keep their own licenses.
backend/.env.example ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # OpenRouter API key used by the agent
2
+ OPENROUTER_API_KEY=
3
+
4
+ # Default model (can be overridden per-request from the frontend)
5
+ OPENROUTER_MODEL=anthropic/claude-sonnet-4-5
6
+
7
+ # Port Hono listens on (HF Spaces expects 7860)
8
+ PORT=7860
9
+
10
+ # When set, /api/* endpoints require a valid HF access token in the
11
+ # Authorization header. Leave unset during early local dev.
12
+ REQUIRE_HF_AUTH=1
backend/package-lock.json ADDED
@@ -0,0 +1,1032 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "reachy-mini-vibe-coder-backend",
3
+ "version": "0.1.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "reachy-mini-vibe-coder-backend",
9
+ "version": "0.1.0",
10
+ "dependencies": {
11
+ "@ai-sdk/react": "^2.0.104",
12
+ "@hono/node-server": "^1.19.6",
13
+ "@huggingface/hub": "^2.11.0",
14
+ "@openrouter/ai-sdk-provider": "^2.5.1",
15
+ "ai": "^6.0.158",
16
+ "dotenv": "^17.4.1",
17
+ "hono": "^4.10.5",
18
+ "zod": "^4.3.6"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^22.0.0",
22
+ "tsx": "^4.19.0",
23
+ "typescript": "^5.6.0"
24
+ }
25
+ },
26
+ "node_modules/@ai-sdk/gateway": {
27
+ "version": "3.0.104",
28
+ "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.104.tgz",
29
+ "integrity": "sha512-ZKX5n74io8VIRlhIMSLWVlvT3sXC8Z7cZ9GHuWBWZDVi96+62AIsWuLGvMfcBA1STYuSoDrp6rIziZmvrTq0TA==",
30
+ "license": "Apache-2.0",
31
+ "dependencies": {
32
+ "@ai-sdk/provider": "3.0.8",
33
+ "@ai-sdk/provider-utils": "4.0.23",
34
+ "@vercel/oidc": "3.2.0"
35
+ },
36
+ "engines": {
37
+ "node": ">=18"
38
+ },
39
+ "peerDependencies": {
40
+ "zod": "^3.25.76 || ^4.1.8"
41
+ }
42
+ },
43
+ "node_modules/@ai-sdk/gateway/node_modules/@ai-sdk/provider": {
44
+ "version": "3.0.8",
45
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.8.tgz",
46
+ "integrity": "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==",
47
+ "license": "Apache-2.0",
48
+ "dependencies": {
49
+ "json-schema": "^0.4.0"
50
+ },
51
+ "engines": {
52
+ "node": ">=18"
53
+ }
54
+ },
55
+ "node_modules/@ai-sdk/gateway/node_modules/@ai-sdk/provider-utils": {
56
+ "version": "4.0.23",
57
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.23.tgz",
58
+ "integrity": "sha512-z8GlDaCmRSDlqkMF2f4/RFgWxdarvIbyuk+m6WXT1LYgsnGiXRJGTD2Z1+SDl3LqtFuRtGX1aghYvQLoHL/9pg==",
59
+ "license": "Apache-2.0",
60
+ "dependencies": {
61
+ "@ai-sdk/provider": "3.0.8",
62
+ "@standard-schema/spec": "^1.1.0",
63
+ "eventsource-parser": "^3.0.6"
64
+ },
65
+ "engines": {
66
+ "node": ">=18"
67
+ },
68
+ "peerDependencies": {
69
+ "zod": "^3.25.76 || ^4.1.8"
70
+ }
71
+ },
72
+ "node_modules/@ai-sdk/provider": {
73
+ "version": "2.0.1",
74
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-2.0.1.tgz",
75
+ "integrity": "sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng==",
76
+ "license": "Apache-2.0",
77
+ "dependencies": {
78
+ "json-schema": "^0.4.0"
79
+ },
80
+ "engines": {
81
+ "node": ">=18"
82
+ }
83
+ },
84
+ "node_modules/@ai-sdk/provider-utils": {
85
+ "version": "3.0.23",
86
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.23.tgz",
87
+ "integrity": "sha512-60GYsRj5wIJQRcq5YwYJq4KhwLeStceXEJiZdecP1miiH+6FMmrnc7lZDOJoQ6m9lrudEb+uI4LEwddLz5+rPQ==",
88
+ "license": "Apache-2.0",
89
+ "dependencies": {
90
+ "@ai-sdk/provider": "2.0.1",
91
+ "@standard-schema/spec": "^1.0.0",
92
+ "eventsource-parser": "^3.0.6"
93
+ },
94
+ "engines": {
95
+ "node": ">=18"
96
+ },
97
+ "peerDependencies": {
98
+ "zod": "^3.25.76 || ^4.1.8"
99
+ }
100
+ },
101
+ "node_modules/@ai-sdk/react": {
102
+ "version": "2.0.181",
103
+ "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-2.0.181.tgz",
104
+ "integrity": "sha512-Jay7zyuy8+RHHIZdrM1M4KLi8wAbFzyBAb37HfVSLeIE6HiU43qFN5bI+Xyi9p946AmBNZ7tokuxjXhsUS+V4w==",
105
+ "license": "Apache-2.0",
106
+ "dependencies": {
107
+ "@ai-sdk/provider-utils": "3.0.23",
108
+ "ai": "5.0.179",
109
+ "swr": "^2.2.5",
110
+ "throttleit": "2.1.0"
111
+ },
112
+ "engines": {
113
+ "node": ">=18"
114
+ },
115
+ "peerDependencies": {
116
+ "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1",
117
+ "zod": "^3.25.76 || ^4.1.8"
118
+ },
119
+ "peerDependenciesMeta": {
120
+ "zod": {
121
+ "optional": true
122
+ }
123
+ }
124
+ },
125
+ "node_modules/@ai-sdk/react/node_modules/@ai-sdk/gateway": {
126
+ "version": "2.0.82",
127
+ "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.82.tgz",
128
+ "integrity": "sha512-vtoCSEBGPcxzChI3eqe9C9AJSlc/WUZp92tzpOqVd4B6Tnu4583S+qR7TknB0tPta15TEoOIkK0ENW6D/DgRJQ==",
129
+ "license": "Apache-2.0",
130
+ "dependencies": {
131
+ "@ai-sdk/provider": "2.0.1",
132
+ "@ai-sdk/provider-utils": "3.0.23",
133
+ "@vercel/oidc": "3.1.0"
134
+ },
135
+ "engines": {
136
+ "node": ">=18"
137
+ },
138
+ "peerDependencies": {
139
+ "zod": "^3.25.76 || ^4.1.8"
140
+ }
141
+ },
142
+ "node_modules/@ai-sdk/react/node_modules/@vercel/oidc": {
143
+ "version": "3.1.0",
144
+ "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.1.0.tgz",
145
+ "integrity": "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==",
146
+ "license": "Apache-2.0",
147
+ "engines": {
148
+ "node": ">= 20"
149
+ }
150
+ },
151
+ "node_modules/@ai-sdk/react/node_modules/ai": {
152
+ "version": "5.0.179",
153
+ "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.179.tgz",
154
+ "integrity": "sha512-tuq/r2FH/pBuY3jo0yHF3UglDV73WONGLhW80DuwgO6w0ftPIqRsAm5p9cE3Bu4LfEuCkMXpiUG/pQRzqKRRaA==",
155
+ "license": "Apache-2.0",
156
+ "dependencies": {
157
+ "@ai-sdk/gateway": "2.0.82",
158
+ "@ai-sdk/provider": "2.0.1",
159
+ "@ai-sdk/provider-utils": "3.0.23",
160
+ "@opentelemetry/api": "1.9.0"
161
+ },
162
+ "engines": {
163
+ "node": ">=18"
164
+ },
165
+ "peerDependencies": {
166
+ "zod": "^3.25.76 || ^4.1.8"
167
+ }
168
+ },
169
+ "node_modules/@esbuild/aix-ppc64": {
170
+ "version": "0.27.7",
171
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
172
+ "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
173
+ "cpu": [
174
+ "ppc64"
175
+ ],
176
+ "dev": true,
177
+ "license": "MIT",
178
+ "optional": true,
179
+ "os": [
180
+ "aix"
181
+ ],
182
+ "engines": {
183
+ "node": ">=18"
184
+ }
185
+ },
186
+ "node_modules/@esbuild/android-arm": {
187
+ "version": "0.27.7",
188
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
189
+ "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
190
+ "cpu": [
191
+ "arm"
192
+ ],
193
+ "dev": true,
194
+ "license": "MIT",
195
+ "optional": true,
196
+ "os": [
197
+ "android"
198
+ ],
199
+ "engines": {
200
+ "node": ">=18"
201
+ }
202
+ },
203
+ "node_modules/@esbuild/android-arm64": {
204
+ "version": "0.27.7",
205
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
206
+ "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
207
+ "cpu": [
208
+ "arm64"
209
+ ],
210
+ "dev": true,
211
+ "license": "MIT",
212
+ "optional": true,
213
+ "os": [
214
+ "android"
215
+ ],
216
+ "engines": {
217
+ "node": ">=18"
218
+ }
219
+ },
220
+ "node_modules/@esbuild/android-x64": {
221
+ "version": "0.27.7",
222
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
223
+ "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
224
+ "cpu": [
225
+ "x64"
226
+ ],
227
+ "dev": true,
228
+ "license": "MIT",
229
+ "optional": true,
230
+ "os": [
231
+ "android"
232
+ ],
233
+ "engines": {
234
+ "node": ">=18"
235
+ }
236
+ },
237
+ "node_modules/@esbuild/darwin-arm64": {
238
+ "version": "0.27.7",
239
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
240
+ "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
241
+ "cpu": [
242
+ "arm64"
243
+ ],
244
+ "dev": true,
245
+ "license": "MIT",
246
+ "optional": true,
247
+ "os": [
248
+ "darwin"
249
+ ],
250
+ "engines": {
251
+ "node": ">=18"
252
+ }
253
+ },
254
+ "node_modules/@esbuild/darwin-x64": {
255
+ "version": "0.27.7",
256
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
257
+ "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
258
+ "cpu": [
259
+ "x64"
260
+ ],
261
+ "dev": true,
262
+ "license": "MIT",
263
+ "optional": true,
264
+ "os": [
265
+ "darwin"
266
+ ],
267
+ "engines": {
268
+ "node": ">=18"
269
+ }
270
+ },
271
+ "node_modules/@esbuild/freebsd-arm64": {
272
+ "version": "0.27.7",
273
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
274
+ "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
275
+ "cpu": [
276
+ "arm64"
277
+ ],
278
+ "dev": true,
279
+ "license": "MIT",
280
+ "optional": true,
281
+ "os": [
282
+ "freebsd"
283
+ ],
284
+ "engines": {
285
+ "node": ">=18"
286
+ }
287
+ },
288
+ "node_modules/@esbuild/freebsd-x64": {
289
+ "version": "0.27.7",
290
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
291
+ "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
292
+ "cpu": [
293
+ "x64"
294
+ ],
295
+ "dev": true,
296
+ "license": "MIT",
297
+ "optional": true,
298
+ "os": [
299
+ "freebsd"
300
+ ],
301
+ "engines": {
302
+ "node": ">=18"
303
+ }
304
+ },
305
+ "node_modules/@esbuild/linux-arm": {
306
+ "version": "0.27.7",
307
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
308
+ "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
309
+ "cpu": [
310
+ "arm"
311
+ ],
312
+ "dev": true,
313
+ "license": "MIT",
314
+ "optional": true,
315
+ "os": [
316
+ "linux"
317
+ ],
318
+ "engines": {
319
+ "node": ">=18"
320
+ }
321
+ },
322
+ "node_modules/@esbuild/linux-arm64": {
323
+ "version": "0.27.7",
324
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
325
+ "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
326
+ "cpu": [
327
+ "arm64"
328
+ ],
329
+ "dev": true,
330
+ "license": "MIT",
331
+ "optional": true,
332
+ "os": [
333
+ "linux"
334
+ ],
335
+ "engines": {
336
+ "node": ">=18"
337
+ }
338
+ },
339
+ "node_modules/@esbuild/linux-ia32": {
340
+ "version": "0.27.7",
341
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
342
+ "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
343
+ "cpu": [
344
+ "ia32"
345
+ ],
346
+ "dev": true,
347
+ "license": "MIT",
348
+ "optional": true,
349
+ "os": [
350
+ "linux"
351
+ ],
352
+ "engines": {
353
+ "node": ">=18"
354
+ }
355
+ },
356
+ "node_modules/@esbuild/linux-loong64": {
357
+ "version": "0.27.7",
358
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
359
+ "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
360
+ "cpu": [
361
+ "loong64"
362
+ ],
363
+ "dev": true,
364
+ "license": "MIT",
365
+ "optional": true,
366
+ "os": [
367
+ "linux"
368
+ ],
369
+ "engines": {
370
+ "node": ">=18"
371
+ }
372
+ },
373
+ "node_modules/@esbuild/linux-mips64el": {
374
+ "version": "0.27.7",
375
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
376
+ "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
377
+ "cpu": [
378
+ "mips64el"
379
+ ],
380
+ "dev": true,
381
+ "license": "MIT",
382
+ "optional": true,
383
+ "os": [
384
+ "linux"
385
+ ],
386
+ "engines": {
387
+ "node": ">=18"
388
+ }
389
+ },
390
+ "node_modules/@esbuild/linux-ppc64": {
391
+ "version": "0.27.7",
392
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
393
+ "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
394
+ "cpu": [
395
+ "ppc64"
396
+ ],
397
+ "dev": true,
398
+ "license": "MIT",
399
+ "optional": true,
400
+ "os": [
401
+ "linux"
402
+ ],
403
+ "engines": {
404
+ "node": ">=18"
405
+ }
406
+ },
407
+ "node_modules/@esbuild/linux-riscv64": {
408
+ "version": "0.27.7",
409
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
410
+ "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
411
+ "cpu": [
412
+ "riscv64"
413
+ ],
414
+ "dev": true,
415
+ "license": "MIT",
416
+ "optional": true,
417
+ "os": [
418
+ "linux"
419
+ ],
420
+ "engines": {
421
+ "node": ">=18"
422
+ }
423
+ },
424
+ "node_modules/@esbuild/linux-s390x": {
425
+ "version": "0.27.7",
426
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
427
+ "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
428
+ "cpu": [
429
+ "s390x"
430
+ ],
431
+ "dev": true,
432
+ "license": "MIT",
433
+ "optional": true,
434
+ "os": [
435
+ "linux"
436
+ ],
437
+ "engines": {
438
+ "node": ">=18"
439
+ }
440
+ },
441
+ "node_modules/@esbuild/linux-x64": {
442
+ "version": "0.27.7",
443
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
444
+ "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
445
+ "cpu": [
446
+ "x64"
447
+ ],
448
+ "dev": true,
449
+ "license": "MIT",
450
+ "optional": true,
451
+ "os": [
452
+ "linux"
453
+ ],
454
+ "engines": {
455
+ "node": ">=18"
456
+ }
457
+ },
458
+ "node_modules/@esbuild/netbsd-arm64": {
459
+ "version": "0.27.7",
460
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
461
+ "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
462
+ "cpu": [
463
+ "arm64"
464
+ ],
465
+ "dev": true,
466
+ "license": "MIT",
467
+ "optional": true,
468
+ "os": [
469
+ "netbsd"
470
+ ],
471
+ "engines": {
472
+ "node": ">=18"
473
+ }
474
+ },
475
+ "node_modules/@esbuild/netbsd-x64": {
476
+ "version": "0.27.7",
477
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
478
+ "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
479
+ "cpu": [
480
+ "x64"
481
+ ],
482
+ "dev": true,
483
+ "license": "MIT",
484
+ "optional": true,
485
+ "os": [
486
+ "netbsd"
487
+ ],
488
+ "engines": {
489
+ "node": ">=18"
490
+ }
491
+ },
492
+ "node_modules/@esbuild/openbsd-arm64": {
493
+ "version": "0.27.7",
494
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
495
+ "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
496
+ "cpu": [
497
+ "arm64"
498
+ ],
499
+ "dev": true,
500
+ "license": "MIT",
501
+ "optional": true,
502
+ "os": [
503
+ "openbsd"
504
+ ],
505
+ "engines": {
506
+ "node": ">=18"
507
+ }
508
+ },
509
+ "node_modules/@esbuild/openbsd-x64": {
510
+ "version": "0.27.7",
511
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
512
+ "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
513
+ "cpu": [
514
+ "x64"
515
+ ],
516
+ "dev": true,
517
+ "license": "MIT",
518
+ "optional": true,
519
+ "os": [
520
+ "openbsd"
521
+ ],
522
+ "engines": {
523
+ "node": ">=18"
524
+ }
525
+ },
526
+ "node_modules/@esbuild/openharmony-arm64": {
527
+ "version": "0.27.7",
528
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
529
+ "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
530
+ "cpu": [
531
+ "arm64"
532
+ ],
533
+ "dev": true,
534
+ "license": "MIT",
535
+ "optional": true,
536
+ "os": [
537
+ "openharmony"
538
+ ],
539
+ "engines": {
540
+ "node": ">=18"
541
+ }
542
+ },
543
+ "node_modules/@esbuild/sunos-x64": {
544
+ "version": "0.27.7",
545
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
546
+ "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
547
+ "cpu": [
548
+ "x64"
549
+ ],
550
+ "dev": true,
551
+ "license": "MIT",
552
+ "optional": true,
553
+ "os": [
554
+ "sunos"
555
+ ],
556
+ "engines": {
557
+ "node": ">=18"
558
+ }
559
+ },
560
+ "node_modules/@esbuild/win32-arm64": {
561
+ "version": "0.27.7",
562
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
563
+ "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
564
+ "cpu": [
565
+ "arm64"
566
+ ],
567
+ "dev": true,
568
+ "license": "MIT",
569
+ "optional": true,
570
+ "os": [
571
+ "win32"
572
+ ],
573
+ "engines": {
574
+ "node": ">=18"
575
+ }
576
+ },
577
+ "node_modules/@esbuild/win32-ia32": {
578
+ "version": "0.27.7",
579
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
580
+ "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
581
+ "cpu": [
582
+ "ia32"
583
+ ],
584
+ "dev": true,
585
+ "license": "MIT",
586
+ "optional": true,
587
+ "os": [
588
+ "win32"
589
+ ],
590
+ "engines": {
591
+ "node": ">=18"
592
+ }
593
+ },
594
+ "node_modules/@esbuild/win32-x64": {
595
+ "version": "0.27.7",
596
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
597
+ "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
598
+ "cpu": [
599
+ "x64"
600
+ ],
601
+ "dev": true,
602
+ "license": "MIT",
603
+ "optional": true,
604
+ "os": [
605
+ "win32"
606
+ ],
607
+ "engines": {
608
+ "node": ">=18"
609
+ }
610
+ },
611
+ "node_modules/@hono/node-server": {
612
+ "version": "1.19.14",
613
+ "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz",
614
+ "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==",
615
+ "license": "MIT",
616
+ "engines": {
617
+ "node": ">=18.14.1"
618
+ },
619
+ "peerDependencies": {
620
+ "hono": "^4"
621
+ }
622
+ },
623
+ "node_modules/@huggingface/hub": {
624
+ "version": "2.11.0",
625
+ "resolved": "https://registry.npmjs.org/@huggingface/hub/-/hub-2.11.0.tgz",
626
+ "integrity": "sha512-WS6QGaXYeBVFlaB4SOn6z4LGUpLB5kRZNL08uUni4izX353KxiwwZMK5+/AWX86MJh8SMZNa/JFcvFCcQsbszQ==",
627
+ "license": "MIT",
628
+ "dependencies": {
629
+ "@huggingface/tasks": "^0.19.90"
630
+ },
631
+ "bin": {
632
+ "hfjs": "dist/cli.js"
633
+ },
634
+ "engines": {
635
+ "node": ">=18"
636
+ },
637
+ "optionalDependencies": {
638
+ "cli-progress": "^3.12.0"
639
+ }
640
+ },
641
+ "node_modules/@huggingface/tasks": {
642
+ "version": "0.19.90",
643
+ "resolved": "https://registry.npmjs.org/@huggingface/tasks/-/tasks-0.19.90.tgz",
644
+ "integrity": "sha512-nfV9luJbvwGQ/5oKXkKhCV9h4X7mwh1YaGG3ORd6UMLDSwr1OFSSatcBX0O9OtBtmNK19aGSjbLFqqgcIR6+IA==",
645
+ "license": "MIT"
646
+ },
647
+ "node_modules/@openrouter/ai-sdk-provider": {
648
+ "version": "2.8.0",
649
+ "resolved": "https://registry.npmjs.org/@openrouter/ai-sdk-provider/-/ai-sdk-provider-2.8.0.tgz",
650
+ "integrity": "sha512-oDDW/0KMqz4suHVloB9sNv0YyKLGNYf1FTevXH6adDkid5dsmbbcYuiEsbIhpZSZtHa6o5AVjK1jEAfePOLxww==",
651
+ "license": "Apache-2.0",
652
+ "engines": {
653
+ "node": ">=18"
654
+ },
655
+ "peerDependencies": {
656
+ "ai": "^6.0.0",
657
+ "zod": "^3.25.0 || ^4.0.0"
658
+ }
659
+ },
660
+ "node_modules/@opentelemetry/api": {
661
+ "version": "1.9.0",
662
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
663
+ "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
664
+ "license": "Apache-2.0",
665
+ "engines": {
666
+ "node": ">=8.0.0"
667
+ }
668
+ },
669
+ "node_modules/@standard-schema/spec": {
670
+ "version": "1.1.0",
671
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
672
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
673
+ "license": "MIT"
674
+ },
675
+ "node_modules/@types/node": {
676
+ "version": "22.19.17",
677
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz",
678
+ "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==",
679
+ "dev": true,
680
+ "license": "MIT",
681
+ "dependencies": {
682
+ "undici-types": "~6.21.0"
683
+ }
684
+ },
685
+ "node_modules/@vercel/oidc": {
686
+ "version": "3.2.0",
687
+ "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.2.0.tgz",
688
+ "integrity": "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==",
689
+ "license": "Apache-2.0",
690
+ "engines": {
691
+ "node": ">= 20"
692
+ }
693
+ },
694
+ "node_modules/ai": {
695
+ "version": "6.0.168",
696
+ "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.168.tgz",
697
+ "integrity": "sha512-2HqCJuO+1V2aV7vfYs5LFEUfxbkGX+5oa54q/gCCTL7KLTdbxcCu5D7TdLA5kwsrs3Szgjah9q6D9tpjHM3hUQ==",
698
+ "license": "Apache-2.0",
699
+ "peer": true,
700
+ "dependencies": {
701
+ "@ai-sdk/gateway": "3.0.104",
702
+ "@ai-sdk/provider": "3.0.8",
703
+ "@ai-sdk/provider-utils": "4.0.23",
704
+ "@opentelemetry/api": "1.9.0"
705
+ },
706
+ "engines": {
707
+ "node": ">=18"
708
+ },
709
+ "peerDependencies": {
710
+ "zod": "^3.25.76 || ^4.1.8"
711
+ }
712
+ },
713
+ "node_modules/ai/node_modules/@ai-sdk/provider": {
714
+ "version": "3.0.8",
715
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.8.tgz",
716
+ "integrity": "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==",
717
+ "license": "Apache-2.0",
718
+ "dependencies": {
719
+ "json-schema": "^0.4.0"
720
+ },
721
+ "engines": {
722
+ "node": ">=18"
723
+ }
724
+ },
725
+ "node_modules/ai/node_modules/@ai-sdk/provider-utils": {
726
+ "version": "4.0.23",
727
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.23.tgz",
728
+ "integrity": "sha512-z8GlDaCmRSDlqkMF2f4/RFgWxdarvIbyuk+m6WXT1LYgsnGiXRJGTD2Z1+SDl3LqtFuRtGX1aghYvQLoHL/9pg==",
729
+ "license": "Apache-2.0",
730
+ "dependencies": {
731
+ "@ai-sdk/provider": "3.0.8",
732
+ "@standard-schema/spec": "^1.1.0",
733
+ "eventsource-parser": "^3.0.6"
734
+ },
735
+ "engines": {
736
+ "node": ">=18"
737
+ },
738
+ "peerDependencies": {
739
+ "zod": "^3.25.76 || ^4.1.8"
740
+ }
741
+ },
742
+ "node_modules/ansi-regex": {
743
+ "version": "5.0.1",
744
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
745
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
746
+ "license": "MIT",
747
+ "optional": true,
748
+ "engines": {
749
+ "node": ">=8"
750
+ }
751
+ },
752
+ "node_modules/cli-progress": {
753
+ "version": "3.12.0",
754
+ "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz",
755
+ "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==",
756
+ "license": "MIT",
757
+ "optional": true,
758
+ "dependencies": {
759
+ "string-width": "^4.2.3"
760
+ },
761
+ "engines": {
762
+ "node": ">=4"
763
+ }
764
+ },
765
+ "node_modules/dequal": {
766
+ "version": "2.0.3",
767
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
768
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
769
+ "license": "MIT",
770
+ "engines": {
771
+ "node": ">=6"
772
+ }
773
+ },
774
+ "node_modules/dotenv": {
775
+ "version": "17.4.2",
776
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
777
+ "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
778
+ "license": "BSD-2-Clause",
779
+ "engines": {
780
+ "node": ">=12"
781
+ },
782
+ "funding": {
783
+ "url": "https://dotenvx.com"
784
+ }
785
+ },
786
+ "node_modules/emoji-regex": {
787
+ "version": "8.0.0",
788
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
789
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
790
+ "license": "MIT",
791
+ "optional": true
792
+ },
793
+ "node_modules/esbuild": {
794
+ "version": "0.27.7",
795
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
796
+ "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
797
+ "dev": true,
798
+ "hasInstallScript": true,
799
+ "license": "MIT",
800
+ "bin": {
801
+ "esbuild": "bin/esbuild"
802
+ },
803
+ "engines": {
804
+ "node": ">=18"
805
+ },
806
+ "optionalDependencies": {
807
+ "@esbuild/aix-ppc64": "0.27.7",
808
+ "@esbuild/android-arm": "0.27.7",
809
+ "@esbuild/android-arm64": "0.27.7",
810
+ "@esbuild/android-x64": "0.27.7",
811
+ "@esbuild/darwin-arm64": "0.27.7",
812
+ "@esbuild/darwin-x64": "0.27.7",
813
+ "@esbuild/freebsd-arm64": "0.27.7",
814
+ "@esbuild/freebsd-x64": "0.27.7",
815
+ "@esbuild/linux-arm": "0.27.7",
816
+ "@esbuild/linux-arm64": "0.27.7",
817
+ "@esbuild/linux-ia32": "0.27.7",
818
+ "@esbuild/linux-loong64": "0.27.7",
819
+ "@esbuild/linux-mips64el": "0.27.7",
820
+ "@esbuild/linux-ppc64": "0.27.7",
821
+ "@esbuild/linux-riscv64": "0.27.7",
822
+ "@esbuild/linux-s390x": "0.27.7",
823
+ "@esbuild/linux-x64": "0.27.7",
824
+ "@esbuild/netbsd-arm64": "0.27.7",
825
+ "@esbuild/netbsd-x64": "0.27.7",
826
+ "@esbuild/openbsd-arm64": "0.27.7",
827
+ "@esbuild/openbsd-x64": "0.27.7",
828
+ "@esbuild/openharmony-arm64": "0.27.7",
829
+ "@esbuild/sunos-x64": "0.27.7",
830
+ "@esbuild/win32-arm64": "0.27.7",
831
+ "@esbuild/win32-ia32": "0.27.7",
832
+ "@esbuild/win32-x64": "0.27.7"
833
+ }
834
+ },
835
+ "node_modules/eventsource-parser": {
836
+ "version": "3.0.8",
837
+ "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz",
838
+ "integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==",
839
+ "license": "MIT",
840
+ "engines": {
841
+ "node": ">=18.0.0"
842
+ }
843
+ },
844
+ "node_modules/fsevents": {
845
+ "version": "2.3.3",
846
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
847
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
848
+ "dev": true,
849
+ "hasInstallScript": true,
850
+ "license": "MIT",
851
+ "optional": true,
852
+ "os": [
853
+ "darwin"
854
+ ],
855
+ "engines": {
856
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
857
+ }
858
+ },
859
+ "node_modules/get-tsconfig": {
860
+ "version": "4.14.0",
861
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz",
862
+ "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==",
863
+ "dev": true,
864
+ "license": "MIT",
865
+ "dependencies": {
866
+ "resolve-pkg-maps": "^1.0.0"
867
+ },
868
+ "funding": {
869
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
870
+ }
871
+ },
872
+ "node_modules/hono": {
873
+ "version": "4.12.14",
874
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz",
875
+ "integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==",
876
+ "license": "MIT",
877
+ "peer": true,
878
+ "engines": {
879
+ "node": ">=16.9.0"
880
+ }
881
+ },
882
+ "node_modules/is-fullwidth-code-point": {
883
+ "version": "3.0.0",
884
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
885
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
886
+ "license": "MIT",
887
+ "optional": true,
888
+ "engines": {
889
+ "node": ">=8"
890
+ }
891
+ },
892
+ "node_modules/json-schema": {
893
+ "version": "0.4.0",
894
+ "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
895
+ "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
896
+ "license": "(AFL-2.1 OR BSD-3-Clause)"
897
+ },
898
+ "node_modules/react": {
899
+ "version": "19.2.5",
900
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
901
+ "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
902
+ "license": "MIT",
903
+ "peer": true,
904
+ "engines": {
905
+ "node": ">=0.10.0"
906
+ }
907
+ },
908
+ "node_modules/resolve-pkg-maps": {
909
+ "version": "1.0.0",
910
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
911
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
912
+ "dev": true,
913
+ "license": "MIT",
914
+ "funding": {
915
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
916
+ }
917
+ },
918
+ "node_modules/string-width": {
919
+ "version": "4.2.3",
920
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
921
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
922
+ "license": "MIT",
923
+ "optional": true,
924
+ "dependencies": {
925
+ "emoji-regex": "^8.0.0",
926
+ "is-fullwidth-code-point": "^3.0.0",
927
+ "strip-ansi": "^6.0.1"
928
+ },
929
+ "engines": {
930
+ "node": ">=8"
931
+ }
932
+ },
933
+ "node_modules/strip-ansi": {
934
+ "version": "6.0.1",
935
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
936
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
937
+ "license": "MIT",
938
+ "optional": true,
939
+ "dependencies": {
940
+ "ansi-regex": "^5.0.1"
941
+ },
942
+ "engines": {
943
+ "node": ">=8"
944
+ }
945
+ },
946
+ "node_modules/swr": {
947
+ "version": "2.4.1",
948
+ "resolved": "https://registry.npmjs.org/swr/-/swr-2.4.1.tgz",
949
+ "integrity": "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==",
950
+ "license": "MIT",
951
+ "dependencies": {
952
+ "dequal": "^2.0.3",
953
+ "use-sync-external-store": "^1.6.0"
954
+ },
955
+ "peerDependencies": {
956
+ "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
957
+ }
958
+ },
959
+ "node_modules/throttleit": {
960
+ "version": "2.1.0",
961
+ "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz",
962
+ "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==",
963
+ "license": "MIT",
964
+ "engines": {
965
+ "node": ">=18"
966
+ },
967
+ "funding": {
968
+ "url": "https://github.com/sponsors/sindresorhus"
969
+ }
970
+ },
971
+ "node_modules/tsx": {
972
+ "version": "4.21.0",
973
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
974
+ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
975
+ "dev": true,
976
+ "license": "MIT",
977
+ "dependencies": {
978
+ "esbuild": "~0.27.0",
979
+ "get-tsconfig": "^4.7.5"
980
+ },
981
+ "bin": {
982
+ "tsx": "dist/cli.mjs"
983
+ },
984
+ "engines": {
985
+ "node": ">=18.0.0"
986
+ },
987
+ "optionalDependencies": {
988
+ "fsevents": "~2.3.3"
989
+ }
990
+ },
991
+ "node_modules/typescript": {
992
+ "version": "5.9.3",
993
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
994
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
995
+ "dev": true,
996
+ "license": "Apache-2.0",
997
+ "bin": {
998
+ "tsc": "bin/tsc",
999
+ "tsserver": "bin/tsserver"
1000
+ },
1001
+ "engines": {
1002
+ "node": ">=14.17"
1003
+ }
1004
+ },
1005
+ "node_modules/undici-types": {
1006
+ "version": "6.21.0",
1007
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
1008
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
1009
+ "dev": true,
1010
+ "license": "MIT"
1011
+ },
1012
+ "node_modules/use-sync-external-store": {
1013
+ "version": "1.6.0",
1014
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
1015
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
1016
+ "license": "MIT",
1017
+ "peerDependencies": {
1018
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
1019
+ }
1020
+ },
1021
+ "node_modules/zod": {
1022
+ "version": "4.3.6",
1023
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
1024
+ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
1025
+ "license": "MIT",
1026
+ "peer": true,
1027
+ "funding": {
1028
+ "url": "https://github.com/sponsors/colinhacks"
1029
+ }
1030
+ }
1031
+ }
1032
+ }
backend/package.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "reachy-mini-vibe-coder-backend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "tsx watch src/server.ts",
8
+ "build": "tsc",
9
+ "start": "node dist/server.js",
10
+ "typecheck": "tsc --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "@hono/node-server": "^1.19.6",
14
+ "@huggingface/hub": "^2.11.0",
15
+ "@openrouter/ai-sdk-provider": "^2.5.1",
16
+ "ai": "^6.0.158",
17
+ "dotenv": "^17.4.1",
18
+ "hono": "^4.10.5",
19
+ "zod": "^4.3.6"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^22.0.0",
23
+ "tsx": "^4.19.0",
24
+ "typescript": "^5.6.0"
25
+ }
26
+ }
backend/src/agent/skill-loader.ts ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { readFileSync, readdirSync, existsSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+
7
+ /**
8
+ * Skills are markdown files grouped per topic under `agent/skills/<id>/`.
9
+ * Each folder must contain a `SKILL.md` (the hub) and optionally other
10
+ * `.md` support files (referenced by relative links from SKILL.md).
11
+ *
12
+ * Loaded at runtime: tweak the .md files and restart the server, no rebuild.
13
+ */
14
+
15
+ const SKILLS_DIR = path.resolve(__dirname, "./skills");
16
+
17
+ export interface SkillDoc {
18
+ filename: string;
19
+ title: string;
20
+ content: string;
21
+ }
22
+
23
+ export interface Skill {
24
+ id: string;
25
+ hub: SkillDoc;
26
+ docs: SkillDoc[];
27
+ }
28
+
29
+ function extractTitle(markdown: string, fallback: string): string {
30
+ const match = markdown.match(/^#\s+(.+)$/m);
31
+ return match ? match[1].trim() : fallback;
32
+ }
33
+
34
+ export function loadSkill(id: string): Skill | null {
35
+ const dir = path.join(SKILLS_DIR, id);
36
+ if (!existsSync(dir)) return null;
37
+
38
+ const files = readdirSync(dir).filter((f) => f.endsWith(".md"));
39
+ if (files.length === 0) return null;
40
+
41
+ const hubFile = files.find((f) => f.toLowerCase() === "skill.md");
42
+ if (!hubFile) return null;
43
+
44
+ const toDoc = (filename: string): SkillDoc => {
45
+ const content = readFileSync(path.join(dir, filename), "utf-8");
46
+ return {
47
+ filename,
48
+ title: extractTitle(content, filename.replace(/\.md$/, "")),
49
+ content,
50
+ };
51
+ };
52
+
53
+ return {
54
+ id,
55
+ hub: toDoc(hubFile),
56
+ docs: files.filter((f) => f !== hubFile).map(toDoc),
57
+ };
58
+ }
59
+
60
+ export const PRIMARY_SKILL_ID = "build-reachy-voice-app";
61
+
62
+ let cached: Skill | null | undefined;
63
+ export function getPrimarySkill(): Skill | null {
64
+ if (cached === undefined) {
65
+ cached = loadSkill(PRIMARY_SKILL_ID);
66
+ if (cached) {
67
+ const docNames = cached.docs.map((d) => d.filename).join(", ");
68
+ console.log(
69
+ `[skill-loader] Loaded skill "${cached.id}" (hub: ${cached.hub.filename}, docs: ${docNames || "none"})`,
70
+ );
71
+ } else {
72
+ console.warn(
73
+ `[skill-loader] Skill "${PRIMARY_SKILL_ID}" not found under ${SKILLS_DIR}`,
74
+ );
75
+ }
76
+ }
77
+ return cached;
78
+ }
79
+
80
+ export function listSkillDocs(skill: Skill): string[] {
81
+ return skill.docs.map((d) => d.filename.replace(/\.md$/, ""));
82
+ }
83
+
84
+ export function getSkillDoc(skill: Skill, topic: string): SkillDoc | null {
85
+ const normalized = topic.toLowerCase().replace(/\.md$/, "");
86
+ return (
87
+ skill.docs.find(
88
+ (d) => d.filename.toLowerCase().replace(/\.md$/, "") === normalized,
89
+ ) ?? null
90
+ );
91
+ }
backend/src/agent/skills/build-reachy-voice-app/SKILL.md ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ name: build-reachy-voice-app
3
+ description: Build a browser-based voice conversation app that connects a Reachy Mini robot to a real-time speech model (OpenAI Realtime API) via WebRTC. Use when the user wants to create, clone, port, or extend a web app that relays audio between Reachy Mini and a voice LLM, manages motion agents (head wobble, antenna oscillation, dances), exposes tool calls from the model, and deploys to Hugging Face Spaces.
4
+ ---
5
+
6
+ # Build a Reachy Mini voice conversation web app
7
+
8
+ Reference implementation: [`reachy_mini_minimal_conversation/`](../../../reachy_mini_minimal_conversation/).
9
+ Python reference (runs on the robot, use as source of truth for behavior): [`reachy_mini_conversation_app/`](../../../reachy_mini_conversation_app/).
10
+
11
+ ## Architecture: the browser is an audio hub
12
+
13
+ The browser holds **two independent `RTCPeerConnection`s** and bridges their audio tracks:
14
+
15
+ ```
16
+ Robot mic ──WebRTC──▶ Browser ──WebRTC──▶ OpenAI Realtime
17
+ │
18
+ Robot speaker ◀──WebRTC── Browser ◀──WebRTC──────┘
19
+ │
20
+ data channel ◀─────┘
21
+ (tool calls)
22
+ ```
23
+
24
+ - **PC1 (Robot)**: ReachyMini JS SDK, signaling through a HF-hosted server + HF OAuth. Exposes audio tracks + a data channel for JSON commands (`set_full_target`, `play_sound`, ...).
25
+ - **PC2 (OpenAI)**: Direct WebRTC to `api.openai.com/v1/realtime`. One POST with SDP, no renegotiation, data channel for events/tools/transcripts.
26
+ - **Audio relay**: the robot mic track becomes the OpenAI sender input; the OpenAI output track is pushed back to the robot speaker sender via `replaceTrack()` (the robot sender already exists, you do not add a new one).
27
+
28
+ This is fundamentally different from the Python app (which runs ON the robot and talks to the daemon via local Python APIs). Most Python features must be **reimplemented client-side**, not "proxied".
29
+
30
+ ## Stack
31
+
32
+ - Vite + TypeScript, vanilla DOM (no framework).
33
+ - ReachyMini JS SDK loaded from `jsdelivr` as an ES module in `index.html`, exposed as `window.ReachyMini`. Not an npm dep.
34
+ - Deploy target: **Hugging Face Spaces with Docker SDK** (not Static). Nginx serves `dist/` and proxies `/openai/*` -> `api.openai.com` to bypass CORS.
35
+
36
+ ## Critical gotchas (read first, save hours)
37
+
38
+ 1. **OpenAI has no CORS**. Direct `fetch('https://api.openai.com/v1/realtime', ...)` from a browser always fails. Use a same-origin proxy: Vite dev server proxy + Nginx `/openai/` location in prod. See [deployment.md](deployment.md).
39
+ 2. **HF OAuth behaves differently local vs Space**:
40
+ - Local: the user must create an OAuth app manually and paste the `clientId` in your Settings UI. `localhost` requires an explicit clientId.
41
+ - On a Space with `hf_oauth: true` in `README.md` frontmatter: the clientId is available as env vars **server-side only** (`OAUTH_CLIENT_ID`, `SPACE_HOST`, ...). The JS runtime does NOT receive them through `window.huggingface.variables` unless you inject them. Solution: a `docker-entrypoint.d` script that `envsubst`-s placeholders like `__OAUTH_CLIENT_ID__` into `index.html` at container start. See [deployment.md](deployment.md).
42
+ 3. **Robot mic track is not exposed by the SDK**. You must reach into `robot._pc.getReceivers()` and pick the audio receiver's track. Capture it right after `startSession()`.
43
+ 4. **Robot speaker = `replaceTrack()` on an existing sender**, not `addTrack()`. Grab the audio sender from `robot._pc.getSenders()` and swap its track when OpenAI's `ontrack` fires.
44
+ 5. **The daemon cannot play dances by name over WebRTC**. `PlayMoveTaskRequest not yet implemented over WS`. Dances must be evaluated client-side and streamed as `set_full_target` frames. See [protocols.md](protocols.md) and [motion-agents.md](motion-agents.md).
45
+ 6. **`set_full_target` uses flat arrays**, not nested: `head` is 16 floats row-major (4x4 homogeneous), `antennas` is `[right, left]` in radians, `body_yaw` is a scalar radian.
46
+ 7. **Three motion sources fight each other** (wobbler, antennas, tool-driven poses/dances). Serialize them with explicit guards or dances will jitter.
47
+ 8. **WebRTC over WiFi cuts voice** unless you tune Opus FEC, bitrate, and the jitter buffer. See [webrtc-tuning.md](webrtc-tuning.md).
48
+
49
+ ## Minimal build order
50
+
51
+ Follow this order. Each step is independently testable.
52
+
53
+ 1. **Scaffold**: Vite + TS, `index.html` loading ReachyMini SDK from CDN, a single full-screen button.
54
+ 2. **Robot only**: login (HF OAuth), list robots, start session, play a canned head pose via `setHeadPose` to confirm commands work.
55
+ 3. **OpenAI only**: input key in UI (stored in `localStorage`), create PC, `addTransceiver('audio', { direction: 'sendrecv' })`, attach `getUserMedia` mic, POST SDP via your proxy, hear the model reply through default speakers.
56
+ 4. **Bridge**: replace `getUserMedia` with the robot mic track, and route OpenAI `ontrack` to the robot speaker sender.
57
+ 5. **UI state machine**: `signed-out | connected | ready | listening | user-speaking | processing | ai-speaking | error`. Drive CSS animations from states + an `--audio-level` var fed by an `AnalyserNode`.
58
+ 6. **Motion agents** (head wobbler, antennas) reading RMS from the OpenAI output track. See [motion-agents.md](motion-agents.md).
59
+ 7. **Tools**: declare `move_head` and `play_dance` in `session.update`, handle `response.function_call_arguments.done`, reply with `conversation.item.create` (function_call_output) + `response.create`.
60
+ 8. **Reliability** knobs: SDP munging, `playoutDelayHint`, `sender.setParameters`, ICE grace period, silent reconnect, transport pill. See [webrtc-tuning.md](webrtc-tuning.md).
61
+ 9. **Deploy**: Docker + Nginx + OAuth envsubst. See [deployment.md](deployment.md).
62
+
63
+ ## OpenAI Realtime event lifecycle
64
+
65
+ The state machine is driven by events on the OpenAI data channel, not by audio analysis:
66
+
67
+ | OpenAI event | UI state | Motion agents |
68
+ |---|---|---|
69
+ | `session.created` | `connected` -> `ready` after `session.update` ack | idle |
70
+ | `input_audio_buffer.speech_started` | `user-speaking` | freeze antennas, stop wobbler |
71
+ | `input_audio_buffer.speech_stopped` | `processing` | still frozen |
72
+ | `response.audio.delta` (first one) | `ai-speaking` | enable wobbler, antennas resume |
73
+ | `response.done` | `listening` | back to idle |
74
+ | `response.function_call_arguments.done` | (transient, stay in current) | run tool, reply |
75
+
76
+ Register tools **once** on `session.created` via `session.update`. Do not send `session.update` on every turn, it resets transcription.
77
+
78
+ ## Reference files
79
+
80
+ - [architecture.md](architecture.md): detailed 2-hop topology, audio track plumbing code, state machine diagram.
81
+ - [protocols.md](protocols.md): daemon WebRTC data channel commands (what works, what doesn't), OpenAI Realtime events we care about, tool-call envelope format.
82
+ - [motion-agents.md](motion-agents.md): head wobbler math, antenna oscillation, why dances are procedural and how to port them from the Python lib.
83
+ - [webrtc-tuning.md](webrtc-tuning.md): SDP munging for Opus, bitrate caps, jitter buffer hint, ICE handling, auto-reconnect, transport path detection.
84
+ - [deployment.md](deployment.md): Dockerfile + Nginx config, OAuth env injection, HF Space YAML frontmatter, dev proxy.
85
+
86
+ ## Things you should NOT do
87
+
88
+ - Do **not** proxy the Python conversation app logic: it runs on the robot, uses a Python SDK with direct ZMQ/ws access to the daemon, and has features (dance library, `play_move`) that are simply not exposed over the public WebRTC data channel.
89
+ - Do **not** rebuild the ReachyMini JS SDK; it handles HF auth, signaling and WebRTC negotiation. Import it from jsdelivr; its internals (`_pc`, `sendRaw`) are stable enough to use for low-level access.
90
+ - Do **not** fetch dance JSONs from random HF datasets; the real dances are procedural Python functions. If you want a quick win, pre-bake the evaluation offline, but prefer porting the `rhythmic_motion.py` primitives to TS.
91
+ - Do **not** use `addTrack` on the robot PC to play TTS audio; the sender already exists, `replaceTrack` is the only path that works.
92
+ - Do **not** rely on `window.huggingface.variables` for OAuth on Spaces; it is not populated automatically.
backend/src/agent/skills/build-reachy-voice-app/architecture.md ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Architecture details
2
+
3
+ ## Two independent peer connections
4
+
5
+ ```
6
+ ┌──────────────────────────────────────────────────────────────┐
7
+ │ Browser │
8
+ │ │
9
+ │ ┌─────────────┐ ┌──────────────┐ │
10
+ │ │ PC1 │ │ PC2 │ │
11
+ │ │ (Robot) │ │ (OpenAI) │ │
12
+ │ │ │ │ │ │
13
+ │ │ mic track ─┼─────────────────────────┼─► sender │ │
14
+ │ │ │ (audio relay) │ │ │
15
+ │ │ spk sender ◄─────────────────────────┼── recv track │ │
16
+ │ │ │ │ │ │
17
+ │ │ datachan │ │ datachan │ │
18
+ │ │ (JSON) │ │ (events + │ │
19
+ │ │ │ │ tools) │ │
20
+ │ └──────┬──────┘ └──────┬───────┘ │
21
+ │ │ │ │
22
+ └──────────┼───────────────────────────────────────┼───────────┘
23
+ │ WebRTC WebRTC │
24
+ ▼ ▼
25
+ Reachy daemon OpenAI Realtime
26
+ (ws signaling (single POST
27
+ via HF server) SDP handshake)
28
+ ```
29
+
30
+ Both PCs are completely independent. Failure of one must not tear down the other. The audio relay is the only thing that links them.
31
+
32
+ ## Audio plumbing
33
+
34
+ ### Robot mic -> OpenAI
35
+
36
+ After `robot.startSession()`:
37
+
38
+ ```ts
39
+ const robotPc: RTCPeerConnection = robot._pc;
40
+ const micReceiver = robotPc.getReceivers().find(r => r.track?.kind === "audio");
41
+ const robotMicTrack = micReceiver!.track;
42
+ ```
43
+
44
+ Pass this track to the OpenAI client and `addTrack(robotMicTrack, new MediaStream([robotMicTrack]))` on the OpenAI PC before `createOffer()`. Also add a `RTCRtpTransceiver('audio', { direction: 'sendrecv' })` so the model can send audio back.
45
+
46
+ ### OpenAI TTS -> Robot speaker
47
+
48
+ On OpenAI `pc.ontrack`:
49
+
50
+ ```ts
51
+ const spkSender = robotPc.getSenders().find(s => s.track?.kind === "audio");
52
+ await spkSender!.replaceTrack(event.track);
53
+ ```
54
+
55
+ Do not `addTrack`, the robot already has an audio sender wired to the robot's speaker. Replacing its track is what actually routes audio to the robot.
56
+
57
+ Also attach a silent `<audio autoplay>` element to the OpenAI track so the browser keeps the receiver "alive" (some browsers stop decoding when no sink is attached).
58
+
59
+ ## UI state machine
60
+
61
+ Drive the central animated circle from a single `state` variable:
62
+
63
+ ```
64
+ signed-out ─login─▶ authenticated ─pickRobot─▶ connected ─start─▶ ready
65
+ │
66
+ (barge-in)
67
+ ▼
68
+ listening ◀──response.done── ai-speaking ◀─ processing
69
+ │ ▲ ▲
70
+ ▼ │ │
71
+ user-speaking ──speech_stopped──────┴──────────────┘
72
+ ```
73
+
74
+ Use `data-state="..."` on the root and express all animations in CSS. Never mutate inline styles for state changes; only for the per-frame `--audio-level` var.
75
+
76
+ ## Audio level for UI
77
+
78
+ One `AnalyserNode` per source of interest (robot mic for user speaking glow, OpenAI output for AI speaking glow and motion agents). Compute RMS in a `requestAnimationFrame` loop, smooth with a leaky integrator, write to a CSS var:
79
+
80
+ ```ts
81
+ const smoothed = alpha * previous + (1 - alpha) * rms;
82
+ document.documentElement.style.setProperty("--audio-level", smoothed.toFixed(3));
83
+ ```
84
+
85
+ ## Motion agent coordination
86
+
87
+ Three animation sources can drive the head / antennas simultaneously:
88
+
89
+ 1. **Head wobbler**: low-amplitude continuous sway while `ai-speaking`.
90
+ 2. **Antennas oscillator**: slow breathing while idle, freeze on user speech, medium amplitude during AI speech.
91
+ 3. **Tool-driven**: `move_head` (static pose for N seconds) or `play_dance` (procedural stream at 100 Hz).
92
+
93
+ Resolution rules:
94
+ - When a tool fires, set a `toolLock` boolean. Wobbler and antennas become no-ops while true.
95
+ - Dances take exclusive control at 100 Hz via `set_full_target`. On dance end, restore idle pose before releasing the lock.
96
+ - Never interleave `setHeadPose` calls from different agents in the same tick. The daemon's last-write-wins semantics will cause visible jitter.
97
+
98
+ ## Error and reconnect boundaries
99
+
100
+ Distinguish three classes of errors:
101
+
102
+ | Source | Retry? | UX |
103
+ |---|---|---|
104
+ | Robot PC fails | No (fatal) | show error, require full restart |
105
+ | OpenAI PC fails | Yes, 1 silent retry | stay in current state, log warn, escalate to fatal on 2nd failure |
106
+ | Daemon command fails (JSON error on data chan) | No (log) | continue, it's a transient robot issue |
107
+
108
+ Reset the retry counter on `connected`. Reset all timers and locks in the teardown path.
backend/src/agent/skills/build-reachy-voice-app/deployment.md ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Deployment on Hugging Face Spaces
2
+
3
+ ## Why Docker, not Static
4
+
5
+ You need:
6
+ 1. A same-origin proxy for `api.openai.com` (CORS).
7
+ 2. Runtime env-var injection for the HF OAuth clientId.
8
+
9
+ Both require a real server. Use Spaces **Docker SDK** with Nginx; `Static` SDK cannot proxy and has no entrypoint hook.
10
+
11
+ ## `README.md` frontmatter
12
+
13
+ ```yaml
14
+ ---
15
+ title: Reachy Voice Demo
16
+ emoji: 🤖
17
+ colorFrom: indigo
18
+ colorTo: pink
19
+ sdk: docker
20
+ app_port: 8080
21
+ hf_oauth: true
22
+ hf_oauth_scopes:
23
+ - read-repos
24
+ pinned: false
25
+ ---
26
+ ```
27
+
28
+ Key bits:
29
+ - `sdk: docker`, NOT `static`.
30
+ - `app_port: 8080` (match your Nginx `listen`).
31
+ - `hf_oauth: true` makes `OAUTH_CLIENT_ID`, `SPACE_HOST` and friends available as env vars inside the container.
32
+
33
+ ## Dockerfile
34
+
35
+ ```dockerfile
36
+ FROM node:20-alpine AS build
37
+ WORKDIR /app
38
+ COPY package*.json ./
39
+ RUN npm ci
40
+ COPY . .
41
+ RUN npm run build
42
+
43
+ FROM nginx:1.27-alpine
44
+ RUN apk add --no-cache bash gettext
45
+ COPY nginx.conf /etc/nginx/nginx.conf
46
+ COPY --from=build /app/dist /usr/share/nginx/html
47
+ COPY docker-entrypoint.d/ /docker-entrypoint.d/
48
+ RUN chmod +x /docker-entrypoint.d/*.sh
49
+ EXPOSE 8080
50
+ ```
51
+
52
+ The base `nginx` image already runs `/docker-entrypoint.d/*.sh` before starting Nginx.
53
+
54
+ ## Nginx config
55
+
56
+ ```nginx
57
+ worker_processes 1;
58
+ events { worker_connections 1024; }
59
+
60
+ http {
61
+ include /etc/nginx/mime.types;
62
+ default_type application/octet-stream;
63
+ sendfile on;
64
+ keepalive_timeout 65;
65
+
66
+ # Hide version
67
+ server_tokens off;
68
+
69
+ server {
70
+ listen 8080 default_server;
71
+
72
+ root /usr/share/nginx/html;
73
+ index index.html;
74
+
75
+ # SPA fallback
76
+ location / {
77
+ try_files $uri $uri/ /index.html;
78
+ }
79
+
80
+ # Same-origin proxy to OpenAI (bypasses CORS)
81
+ location /openai/ {
82
+ proxy_pass https://api.openai.com/;
83
+ proxy_set_header Host api.openai.com;
84
+ proxy_ssl_server_name on;
85
+ proxy_ssl_name api.openai.com;
86
+ proxy_http_version 1.1;
87
+ proxy_set_header Connection "";
88
+ proxy_read_timeout 600s;
89
+ proxy_send_timeout 600s;
90
+
91
+ # Preserve client Authorization header
92
+ proxy_pass_request_headers on;
93
+ }
94
+ }
95
+ }
96
+ ```
97
+
98
+ Common pitfalls:
99
+ - **421 Misdirected Request**: you forgot `proxy_set_header Host` or `proxy_ssl_name`. OpenAI refuses requests whose SNI/host doesn't match.
100
+ - **500 from Nginx**: missing `resolver` when upstream is HTTPS. Fix: either add `resolver 8.8.8.8 valid=30s;` or ensure `proxy_pass https://api.openai.com/` (without variable) is resolved at boot.
101
+ - **401 pass-through**: good, your proxy works; the key is wrong.
102
+
103
+ ## OAuth env injection
104
+
105
+ HF sets env vars server-side but your bundle is static JS. Inject them at container start with `envsubst`.
106
+
107
+ ### `index.html` placeholders
108
+
109
+ ```html
110
+ <meta name="oauth-client-id" content="__OAUTH_CLIENT_ID__" />
111
+ <meta name="oauth-scopes" content="__OAUTH_SCOPES__" />
112
+ <meta name="space-host" content="__SPACE_HOST__" />
113
+ ```
114
+
115
+ Read them in JS:
116
+
117
+ ```ts
118
+ function meta(name: string, fallback = "") {
119
+ const el = document.querySelector(`meta[name="${name}"]`) as HTMLMetaElement | null;
120
+ const v = el?.content ?? "";
121
+ // the placeholder survives in dev where envsubst hasn't run
122
+ return v.startsWith("__") && v.endsWith("__") ? fallback : v;
123
+ }
124
+ ```
125
+
126
+ ### `docker-entrypoint.d/10-envsubst.sh`
127
+
128
+ ```bash
129
+ #!/bin/bash
130
+ set -euo pipefail
131
+
132
+ TARGET=/usr/share/nginx/html/index.html
133
+
134
+ : "${OAUTH_CLIENT_ID:=}"
135
+ : "${OAUTH_SCOPES:=read-repos}"
136
+ : "${SPACE_HOST:=}"
137
+
138
+ export OAUTH_CLIENT_ID OAUTH_SCOPES SPACE_HOST
139
+
140
+ envsubst '${OAUTH_CLIENT_ID} ${OAUTH_SCOPES} ${SPACE_HOST}' \
141
+ < "$TARGET" > "${TARGET}.tmp" && mv "${TARGET}.tmp" "$TARGET"
142
+ ```
143
+
144
+ Pass the variable list to `envsubst` explicitly; without it, Nginx config `$uri` would also be substituted.
145
+
146
+ ## Dev mode (Vite)
147
+
148
+ Skip Nginx. Use Vite's proxy in `vite.config.ts`:
149
+
150
+ ```ts
151
+ export default defineConfig({
152
+ server: {
153
+ proxy: {
154
+ "/openai": {
155
+ target: "https://api.openai.com",
156
+ changeOrigin: true,
157
+ rewrite: (p) => p.replace(/^\/openai/, ""),
158
+ secure: true,
159
+ },
160
+ },
161
+ },
162
+ });
163
+ ```
164
+
165
+ Local OAuth does NOT work out of the box because HF requires a registered redirect URI. Either:
166
+ - register `http://localhost:5173` as a redirect in your own HF OAuth app and paste the clientId in a Settings UI (store in `localStorage`);
167
+ - or skip OAuth locally and hardcode a robot URL for dev.
168
+
169
+ ## Build + push
170
+
171
+ The repo should have two remotes (see `git-remotes-safety.mdc` in this workspace):
172
+ - `github` (public upstream): never force-push.
173
+ - `space` (HF Space): force-push on your own namespace is fine.
174
+
175
+ ```bash
176
+ npm run build # sanity check
177
+ git add -A && git commit -m "..."
178
+ git push space feat/new-ui:main --force-with-lease
179
+ ```
180
+
181
+ If the Space doesn't rebuild, push an empty commit:
182
+
183
+ ```bash
184
+ git commit --allow-empty -m "chore: trigger rebuild"
185
+ git push space feat/new-ui:main --force-with-lease
186
+ ```
187
+
188
+ ## Post-deploy smoke test
189
+
190
+ - Open the Space, sign in via HF: should NOT say "Missing clientId".
191
+ - List robots: should show your robot (requires the robot to be online and authed with the same HF account).
192
+ - Start session: the central circle should animate `listening`.
193
+ - Speak: state -> `user-speaking` -> `processing` -> `ai-speaking`. Voice reaches the robot's speakers.
194
+ - Say "nod yes": tool fires, head nods.
195
+ - Say "dance dizzy spin": tool fires, dance plays at 100 Hz.
196
+ - Transport pill shows `LAN` on local network, `Relay` through a mobile hotspot.
backend/src/agent/skills/build-reachy-voice-app/motion-agents.md ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Motion agents
2
+
3
+ Three concurrent agents animate the robot. All of them ultimately write to the daemon via `set_full_target` (or equivalent) and must not fight.
4
+
5
+ ## Head wobbler
6
+
7
+ Goal: gentle head sway while the AI is speaking, amplitude proportional to TTS loudness.
8
+
9
+ Recipe:
10
+ - `AnalyserNode` on the OpenAI output track, read time-domain data in a `requestAnimationFrame` loop, compute RMS.
11
+ - Smooth with a leaky integrator (`alpha ~0.12`). Clip to [0, 1].
12
+ - Drive two lissajous-style sinusoids with independent frequencies for roll/pitch, amplitude modulated by the smoothed RMS.
13
+ - Ship a head pose via `robot.setHeadPose(matrix)` every frame, or stop if `audioLevel < epsilon`.
14
+
15
+ ```ts
16
+ const roll = maxRoll * level * Math.sin(2 * Math.PI * 0.9 * t + 0.3);
17
+ const pitch = maxPitch * level * Math.sin(2 * Math.PI * 1.3 * t + 1.1);
18
+ // yaw = 0, small translation optional
19
+ ```
20
+
21
+ Pause (emit identity pose or skip emission) when:
22
+ - tool lock is active
23
+ - dance is playing
24
+ - state is `user-speaking` (barge-in)
25
+
26
+ ## Antennas oscillator
27
+
28
+ Goal: idle "breathing" + reactive motion during AI speech.
29
+
30
+ Two modes:
31
+ - **Idle**: slow symmetric oscillation, `antennas = [A*sin(w*t), A*sin(w*t + pi)]` with small `A` (0.1 rad), period ~4 s.
32
+ - **Speaking**: faster, audio-reactive amplitude, optional asymmetry.
33
+
34
+ Freeze to `[0, 0]` during `user-speaking`. Release tool lock by restoring idle mode gradually over ~300 ms.
35
+
36
+ ## Dances (tool-driven)
37
+
38
+ Dances in the Reachy ecosystem are **procedural Python functions**, not recorded keyframes. They live in `reachy_mini_dances_library/`:
39
+
40
+ - `rhythmic_motion.py`: primitives
41
+ - `oscillation_motion(t_beats, {amplitude, subcycles_per_beat, phase_offset, waveform})` with waveforms `sin | cos | square | triangle | sawtooth`
42
+ - `transient_motion(t_beats, {amplitude, duration, delay, repeat_every})` with smoothstep easing
43
+ - atomic helpers: `atomic_x_pos`, `atomic_roll`, ..., `atomic_antennas_wiggle`, ...
44
+ - `combine_offsets([...])`: sum of `MoveOffsets`
45
+ - `collection/dance.py`: ~19 named dances composed from the primitives (`move_simple_nod`, `move_yeah_nod`, `move_dizzy_spin`, `move_jackson_square`, ...).
46
+ - `dance_move.py`: wraps a move function with its parameters, `evaluate(t_seconds)` converts `t_seconds` to `t_beats` (with a global tempo, typically ~1.9 beats/s) and returns a 4x4 head matrix + antennas + body_yaw via `create_head_pose`.
47
+
48
+ ### Port strategy (recommended)
49
+
50
+ Do not fetch JSON. Port the primitives + collection to TypeScript. Then:
51
+
52
+ ```ts
53
+ class DancePlayer {
54
+ async play(name: string) {
55
+ const move = AVAILABLE_MOVES[name];
56
+ const durationS = (1 / TEMPO_BPS) * move.defaultDurationBeats;
57
+ const start = performance.now();
58
+ const tick = () => {
59
+ const t = (performance.now() - start) / 1000;
60
+ if (t >= durationS) return this.stop();
61
+ const tBeats = t * TEMPO_BPS;
62
+ const offsets = move.fn(tBeats, move.params);
63
+ const head = createHeadPose(offsets.x, offsets.y, offsets.z,
64
+ offsets.roll, offsets.pitch, offsets.yaw);
65
+ robot.sendRaw({
66
+ type: "set_full_target",
67
+ head: Array.from(head), // 16 floats
68
+ antennas: [offsets.antennas[0], offsets.antennas[1]],
69
+ body_yaw: 0,
70
+ });
71
+ this.rafId = requestAnimationFrame(tick);
72
+ };
73
+ tick();
74
+ }
75
+ }
76
+ ```
77
+
78
+ Ship at 100 Hz either via `setInterval(10)` or via a `requestAnimationFrame` loop with a 10 ms check. 60 Hz is acceptable but feels less smooth on fast dances.
79
+
80
+ ### createHeadPose (port of scipy Euler -> matrix)
81
+
82
+ `scipy.spatial.transform.Rotation.from_euler("xyz", [roll, pitch, yaw])` builds `Rx @ Ry @ Rz` with intrinsic rotations. In TS:
83
+
84
+ ```ts
85
+ function createHeadPose(x: number, y: number, z: number,
86
+ roll: number, pitch: number, yaw: number): Float32Array {
87
+ const cr = Math.cos(roll), sr = Math.sin(roll);
88
+ const cp = Math.cos(pitch), sp = Math.sin(pitch);
89
+ const cy = Math.cos(yaw), sy = Math.sin(yaw);
90
+ // R = Rx(roll) @ Ry(pitch) @ Rz(yaw)
91
+ const r00 = cp * cy;
92
+ const r01 = -cp * sy;
93
+ const r02 = sp;
94
+ const r10 = cr * sy + sr * sp * cy;
95
+ const r11 = cr * cy - sr * sp * sy;
96
+ const r12 = -sr * cp;
97
+ const r20 = sr * sy - cr * sp * cy;
98
+ const r21 = sr * cy + cr * sp * sy;
99
+ const r22 = cr * cp;
100
+ const m = new Float32Array(16);
101
+ m[0]=r00; m[1]=r01; m[2]=r02; m[3]=x;
102
+ m[4]=r10; m[5]=r11; m[6]=r12; m[7]=y;
103
+ m[8]=r20; m[9]=r21; m[10]=r22; m[11]=z;
104
+ m[12]=0; m[13]=0; m[14]=0; m[15]=1;
105
+ return m;
106
+ }
107
+ ```
108
+
109
+ Cross-check against Python: pick a few `(x,y,z,r,p,y)` triplets, compute in Python, paste the expected matrix into a unit test.
110
+
111
+ ### Alternative: pre-baked JSON
112
+
113
+ If porting feels heavy, run the Python evaluator offline for each dance at 100 Hz, dump to JSON (`[{head: number[16], antennas: [r,l], body_yaw}, ...]`), ship them statically. Simpler, but you lose parameterisation.
114
+
115
+ ## Tool-driven poses (`move_head`)
116
+
117
+ Tiny tool, much simpler than dances. Keep a dict of named poses:
118
+
119
+ ```ts
120
+ const HEAD_POSES = {
121
+ nod_yes: { x: 0, y: 0, z: 0, roll: 0, pitch: 0.3, yaw: 0 },
122
+ nod_no: { x: 0, y: 0, z: 0, roll: 0, pitch: 0, yaw: 0.4 },
123
+ look_left: { x: 0, y: 0, z: 0, roll: 0, pitch: 0, yaw: 0.5 },
124
+ look_right: { x: 0, y: 0, z: 0, roll: 0, pitch: 0, yaw:-0.5 },
125
+ tilt: { x: 0, y: 0, z: 0, roll: 0.4, pitch: 0, yaw: 0 },
126
+ };
127
+ ```
128
+
129
+ Set tool lock, emit pose, wait `duration_seconds`, restore identity pose, release lock. Reply the tool call once the motion completes so the model can reference it in the next turn.
backend/src/agent/skills/build-reachy-voice-app/protocols.md ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Protocol details
2
+
3
+ ## Reachy Mini daemon data channel
4
+
5
+ Send JSON messages through the robot's data channel. The SDK wraps this as `robot.sendRaw(obj)`. The daemon accepts (verified in `reachy_mini/io/protocol.py` and `ws_server.py`):
6
+
7
+ | Command | Payload | Notes |
8
+ |---|---|---|
9
+ | `set_target` | `{head, antennas, body_yaw}` keyframe | interpolated by daemon |
10
+ | `set_full_target` | `{head: float[16], antennas: [r,l], body_yaw}` | **this is the fast path**, no interpolation, ship at 100 Hz for smooth motion |
11
+ | `set_head_joints` | per-joint angles | rarely needed |
12
+ | `set_body_yaw` | scalar radians | body turntable |
13
+ | `set_antennas` | `[right, left]` radians | |
14
+ | `goto_target` | keyframe + duration | daemon blends |
15
+ | `wake_up` / `goto_sleep` | `{}` | safe boot/shutdown poses |
16
+ | `play_sound` | base64 WAV or URL | daemon speaker out |
17
+ | `set_motor_mode` / `set_torque` | per motor | |
18
+
19
+ ### Not supported over the data channel
20
+
21
+ - **`PlayMoveTaskRequest`** (dances by name): the daemon logs `"PlayMoveTaskRequest not yet implemented over WS"`. Never rely on this.
22
+ - Any task-style long-running request. The data channel is for state updates, not RPC with awaitable results.
23
+
24
+ ### `set_full_target` format
25
+
26
+ Head is a 4x4 homogeneous transformation matrix in **row-major** order, flattened to 16 floats. Construct it with the same convention as `reachy_mini.utils.create_head_pose`:
27
+
28
+ ```
29
+ T = Translation(x, y, z) @ Rx(roll) @ Ry(pitch) @ Rz(yaw)
30
+ ```
31
+
32
+ In TypeScript, compose three 3x3 rotations with scipy's intrinsic-XYZ convention, insert into the top-left of a 4x4, translation in the last column, `[0,0,0,1]` at the bottom row, then flatten row-major.
33
+
34
+ Units: meters for translation, radians for rotations and antennas.
35
+
36
+ ## OpenAI Realtime events we actually use
37
+
38
+ Over the OpenAI data channel:
39
+
40
+ ### Outbound (we send)
41
+
42
+ - `session.update` with `{type: "realtime", model, voice, instructions, tools, turn_detection, input_audio_transcription}` **once** on `session.created`. Re-sending this mid-conversation resets transcription.
43
+ - `conversation.item.create` with a `function_call_output` item, to reply to a tool call.
44
+ - `response.create` right after, to let the model speak again.
45
+
46
+ ### Inbound (we listen)
47
+
48
+ | Event | Action |
49
+ |---|---|
50
+ | `session.created` | send `session.update`, set state `connected` |
51
+ | `session.updated` | state `ready` |
52
+ | `input_audio_buffer.speech_started` | state `user-speaking`, freeze motion |
53
+ | `input_audio_buffer.speech_stopped` | state `processing` |
54
+ | `response.audio.delta` (first) | state `ai-speaking`, enable wobbler |
55
+ | `response.audio_transcript.delta` | optional: live caption |
56
+ | `response.function_call_arguments.delta` | accumulate into `toolCalls[callId].arguments` |
57
+ | `response.function_call_arguments.done` | parse JSON args, execute tool, reply |
58
+ | `response.done` | state `listening` |
59
+ | `error` | log; do not tear down unless data channel closes |
60
+
61
+ ### Tool call envelope
62
+
63
+ Declare tools in `session.update`:
64
+
65
+ ```ts
66
+ {
67
+ type: "function",
68
+ name: "move_head",
69
+ description: "Move the robot head to a named pose for N seconds",
70
+ parameters: {
71
+ type: "object",
72
+ properties: {
73
+ pose: { type: "string", enum: ["nod_yes", "nod_no", "look_left", "look_right", "tilt"] },
74
+ duration_seconds: { type: "number", minimum: 0.3, maximum: 5 }
75
+ },
76
+ required: ["pose"]
77
+ }
78
+ }
79
+ ```
80
+
81
+ Handle:
82
+
83
+ ```ts
84
+ dc.addEventListener("message", (e) => {
85
+ const msg = JSON.parse(e.data);
86
+ if (msg.type === "response.function_call_arguments.done") {
87
+ const args = JSON.parse(msg.arguments);
88
+ const result = await executeTool(msg.name, args);
89
+ dc.send(JSON.stringify({
90
+ type: "conversation.item.create",
91
+ item: {
92
+ type: "function_call_output",
93
+ call_id: msg.call_id,
94
+ output: JSON.stringify(result),
95
+ }
96
+ }));
97
+ dc.send(JSON.stringify({ type: "response.create" }));
98
+ }
99
+ });
100
+ ```
101
+
102
+ `call_id` **must** match. Always respond, even on error (set `output: JSON.stringify({ error: "..." })`), otherwise the model stalls.
103
+
104
+ ## OpenAI Realtime WebRTC handshake
105
+
106
+ One POST, SDP as body, not JSON:
107
+
108
+ ```ts
109
+ const res = await fetch(`${proxy}/openai/v1/realtime?model=gpt-realtime`, {
110
+ method: "POST",
111
+ headers: {
112
+ "Authorization": `Bearer ${apiKey}`,
113
+ "Content-Type": "application/sdp",
114
+ },
115
+ body: offer.sdp,
116
+ });
117
+ const answerSdp = await res.text();
118
+ await pc.setRemoteDescription({ type: "answer", sdp: answerSdp });
119
+ ```
120
+
121
+ - `proxy` is `""` in prod (same origin, Nginx strips `/openai/`) and `""` in dev (Vite proxy rewrites).
122
+ - Status codes seen in the wild: 401 (bad key), 421 (wrong host header via proxy, check your Nginx config), 500 (Nginx upstream issue), 400 (bad SDP, usually m-line order).
123
+
124
+ ## Robot daemon signaling
125
+
126
+ You never touch this directly. The ReachyMini SDK handles:
127
+ - listing robots via HF-hosted server
128
+ - OAuth token exchange
129
+ - WebRTC offer/answer and ICE through the HF signaling relay
130
+ - creating the PC, the data channel, and the initial audio transceivers
131
+
132
+ You only consume `robot._pc`, `robot.startSession()`, `robot.setHeadPose(...)`, `robot.setAntennas(...)`, `robot.sendRaw(...)`, `robot.login(...)`, `robot.listRobots()`.
backend/src/agent/skills/build-reachy-voice-app/webrtc-tuning.md ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # WebRTC reliability tuning
2
+
3
+ Problem: we ship voice over WiFi with the browser as an audio hub. Default WebRTC settings produce audible cutouts when signal dips. The following four knobs, applied together, eliminate most mid-sentence cutouts on typical home WiFi. All four are applied on the OpenAI PC; the robot PC is local to the network (low RTT, no tuning needed).
4
+
5
+ ## 1. SDP munging: Opus FEC + DTX + bitrate
6
+
7
+ Patch the `a=fmtp:<pt>` line for Opus in **both** the local offer and the remote answer (OpenAI's SDP defaults are not great for lossy links).
8
+
9
+ ```ts
10
+ const OPUS_FMTP_OVERRIDES = {
11
+ minptime: "10",
12
+ useinbandfec: "1", // in-band FEC, costs ~10% bitrate, recovers single-packet loss
13
+ usedtx: "0", // disable discontinuous transmission, TTS hates silence gaps
14
+ stereo: "0",
15
+ "sprop-stereo": "0",
16
+ maxaveragebitrate: "32000",
17
+ };
18
+
19
+ function patchOpusFmtp(sdp: string): string {
20
+ // find opus payload type via "a=rtpmap:<pt> opus/48000/2"
21
+ // rewrite the matching "a=fmtp:<pt> ..." line with OPUS_FMTP_OVERRIDES
22
+ // if no fmtp line exists, inject one right after the rtpmap
23
+ }
24
+
25
+ const rawOffer = await pc.createOffer();
26
+ await pc.setLocalDescription({ type: "offer", sdp: patchOpusFmtp(rawOffer.sdp!) });
27
+ // ... POST to OpenAI ...
28
+ const rawAnswer = await res.text();
29
+ await pc.setRemoteDescription({ type: "answer", sdp: patchOpusFmtp(rawAnswer) });
30
+ ```
31
+
32
+ Both patches matter. Patching the offer tells OpenAI what we accept; patching the answer forces the decoder and our encoder into the expected mode.
33
+
34
+ ## 2. `playoutDelayHint` on inbound receivers
35
+
36
+ Increases the jitter buffer from ~40 ms default to 150 ms. Adds some latency but absorbs WiFi burst losses.
37
+
38
+ ```ts
39
+ pc.ontrack = (event) => {
40
+ try {
41
+ (event.receiver as any).playoutDelayHint = 0.15;
42
+ } catch {}
43
+ // ... route to robot speaker ...
44
+ };
45
+ ```
46
+
47
+ 150 ms is a good trade-off for voice chat; anything past 300 ms feels laggy.
48
+
49
+ ## 3. `sender.setParameters` bitrate cap
50
+
51
+ Caps the outgoing audio bitrate to prevent WiFi saturation when several devices share the AP. Has to be done after `setLocalDescription`.
52
+
53
+ ```ts
54
+ async function capSenderBitrate(pc: RTCPeerConnection, maxBitrate: number) {
55
+ const sender = pc.getSenders().find(s => s.track?.kind === "audio");
56
+ if (!sender) return;
57
+ const params = sender.getParameters();
58
+ if (!params.encodings || !params.encodings.length) {
59
+ params.encodings = [{}];
60
+ }
61
+ params.encodings[0].maxBitrate = maxBitrate;
62
+ await sender.setParameters(params);
63
+ }
64
+ ```
65
+
66
+ Call with `32000` to match the Opus `maxaveragebitrate`.
67
+
68
+ ## 4. Robust ICE state handling
69
+
70
+ Default behavior: `iceConnectionState === "disconnected"` fires on any 2-second gap and panics. Over WiFi this happens all the time and auto-heals.
71
+
72
+ ```ts
73
+ const ICE_DISCONNECT_GRACE_MS = 5000;
74
+ let iceGraceTimer: number | null = null;
75
+
76
+ pc.oniceconnectionstatechange = () => {
77
+ const s = pc.iceConnectionState;
78
+ if (s === "connected" || s === "completed") {
79
+ if (iceGraceTimer !== null) { clearTimeout(iceGraceTimer); iceGraceTimer = null; }
80
+ return;
81
+ }
82
+ if (s === "failed") {
83
+ if (iceGraceTimer !== null) { clearTimeout(iceGraceTimer); iceGraceTimer = null; }
84
+ escalateToError(new Error("ICE failed"));
85
+ return;
86
+ }
87
+ if (s === "disconnected") {
88
+ if (iceGraceTimer !== null) return;
89
+ iceGraceTimer = window.setTimeout(() => {
90
+ iceGraceTimer = null;
91
+ if (pc.iceConnectionState === "disconnected") {
92
+ escalateToError(new Error("ICE disconnected beyond grace"));
93
+ }
94
+ }, ICE_DISCONNECT_GRACE_MS);
95
+ }
96
+ };
97
+ ```
98
+
99
+ ## Silent auto-reconnect
100
+
101
+ If ICE escalates, close the OpenAI PC (not the robot PC) and retry once with the same robot mic track. If the second attempt also fails, bubble the error.
102
+
103
+ ```ts
104
+ let reconnecting = false, reconnectAttempts = 0;
105
+
106
+ async function tryReconnectOpenai(robotMicTrack: MediaStreamTrack, cause: Error) {
107
+ if (reconnecting) return;
108
+ if (reconnectAttempts >= 1) { onFatalError(cause); return; }
109
+ reconnecting = true;
110
+ reconnectAttempts += 1;
111
+ pauseMotionAgents();
112
+ openai.close();
113
+ await new Promise(r => setTimeout(r, 500));
114
+ try {
115
+ await connectOpenai(robotMicTrack);
116
+ // reset attempts on `connected`
117
+ } catch (err) {
118
+ onFatalError(err as Error);
119
+ } finally {
120
+ reconnecting = false;
121
+ }
122
+ }
123
+ ```
124
+
125
+ Reset `reconnectAttempts = 0` whenever the new PC reaches `connected`. Reset everything in the `teardown()` path.
126
+
127
+ ## Transport path detection (LAN / Direct / Relay pill)
128
+
129
+ Useful to diagnose WiFi issues: tells the user whether the robot PC went peer-to-peer on the LAN or got relayed through a TURN server.
130
+
131
+ Poll `robot._pc.getStats()` every 3 seconds and read the selected candidate pair:
132
+
133
+ ```ts
134
+ type TransportKind = "lan" | "direct" | "relay" | "checking";
135
+
136
+ function classify(stats: RTCStatsReport): TransportKind {
137
+ let pair: any;
138
+ stats.forEach(r => {
139
+ if (r.type === "candidate-pair" && (r.nominated || r.state === "succeeded") && r.selected !== false) {
140
+ pair = r;
141
+ }
142
+ });
143
+ if (!pair) return "checking";
144
+ const local = stats.get(pair.localCandidateId);
145
+ const remote = stats.get(pair.remoteCandidateId);
146
+ if (!local || !remote) return "checking";
147
+ if (local.candidateType === "relay" || remote.candidateType === "relay") return "relay";
148
+ if (local.candidateType === "host" && remote.candidateType === "host") return "lan";
149
+ return "direct"; // srflx, prflx
150
+ }
151
+ ```
152
+
153
+ Drive a small header pill with CSS classes `transport-{lan|direct|relay|checking}` (green / blue / amber / blinking).
154
+
155
+ Start the monitor after `startSession()` resolves (give ICE ~600 ms to pick a pair), stop it in teardown.
156
+
157
+ ## What NOT to bother with
158
+
159
+ - Custom TURN servers: the HF signaling relay already provides one; switching gains nothing unless you self-host.
160
+ - Disabling audio-level event updates: they are cheap.
161
+ - `RTCPeerConnection.restartIce()`: tested, does not fix ICE failures in this setup; rebuilding the PC is cleaner.
162
+ - FEC on the robot PC: it's on the local network, RTT < 10 ms, losses are zero. Only tune the OpenAI PC.
backend/src/agent/system-prompt.ts ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getPrimarySkill, listSkillDocs } from "./skill-loader.js";
2
+
3
+ const BASE_PROMPT = `You are **Reachy Vibe Coder**, an agent that helps users build browser-based
4
+ voice conversation apps for the Reachy Mini robot. The user describes what
5
+ they want, you produce the source files, the user previews them live in a
6
+ sandboxed iframe and can publish the result to a Hugging Face Space in one
7
+ click.
8
+
9
+ ## How you work
10
+
11
+ You maintain a small **virtual file system** for the current project. Every
12
+ file mutation MUST go through one of these tools:
13
+
14
+ - **write_file(path, content)** — Create or fully overwrite a file. Use for
15
+ new files and when a rewrite is cleaner than a surgical patch.
16
+ - **edit_file(path, oldString, newString)** — Replace an *exact* substring.
17
+ Use for small targeted edits. If \`oldString\` is not unique the edit
18
+ fails - provide more context.
19
+ - **delete_file(path)** — Remove a file.
20
+ - **read_file(path)** — Inspect a file before editing.
21
+ - **list_files()** — List the files currently in the virtual FS.
22
+ - **read_skill_doc(topic)** — Load a detailed skill reference page. Use
23
+ *before* writing code whenever the topic is non-trivial.
24
+
25
+ The virtual FS is rendered as a file tree in the UI and each file is shown
26
+ in a Monaco editor. A sandboxed iframe re-runs the app whenever
27
+ \`index.html\` changes.
28
+
29
+ ## Target project shape
30
+
31
+ A typical Reachy Mini voice app produced by you looks like:
32
+
33
+ \`\`\`
34
+ index.html ReachyMini SDK from CDN + OAuth injection placeholders
35
+ src/main.ts entry point (PC1 robot + PC2 OpenAI + state machine)
36
+ src/style.css
37
+ src/<module>.ts motion agents, OpenAI glue, etc.
38
+ package.json vite + TypeScript, no framework deps
39
+ vite.config.ts dev proxy for /openai/*
40
+ nginx.conf prod proxy for /openai/* (same shape as dev)
41
+ Dockerfile multi-stage Vite build + Nginx runtime
42
+ docker-entrypoint.d/
43
+ 10-inject-hf-vars.sh envsubst OAuth vars at container start
44
+ README.md HF Space frontmatter (sdk: docker, hf_oauth: true)
45
+ \`\`\`
46
+
47
+ The reference implementation is \`reachy_mini_minimal_conversation/\` and the
48
+ full conventions live in the embedded **build-reachy-voice-app** skill (see
49
+ below - call \`read_skill_doc\` for the chapter you need).
50
+
51
+ ## Working loop
52
+
53
+ 1. Start with a \`<thinking>\` block: what is the user asking, what files are
54
+ already there (call \`list_files\` if uncertain), which skill docs do you
55
+ need to re-read before editing.
56
+ 2. Call \`read_skill_doc\` for every relevant chapter (architecture, webrtc
57
+ tuning, deployment, …) *before* writing non-trivial code.
58
+ 3. Mutate the virtual FS through the tools. Prefer \`edit_file\` over
59
+ \`write_file\` for small changes.
60
+ 4. Finish with a short plain-text summary of what you did and what the user
61
+ should test next.
62
+
63
+ ## Non-negotiables
64
+
65
+ - OpenAI's Realtime API has no CORS - every fetch to \`api.openai.com\` MUST
66
+ go through a same-origin proxy (Vite dev + Nginx prod). Never call it
67
+ directly from the browser.
68
+ - On a Space, OAuth variables (\`OAUTH_CLIENT_ID\`, \`SPACE_HOST\`, …) are only
69
+ available server-side. You must inject them into \`index.html\` at
70
+ container start via a \`docker-entrypoint.d\` script using \`envsubst\`.
71
+ - The robot mic track is obtained through \`robot._pc.getReceivers()\`, and
72
+ the robot speaker is updated through \`replaceTrack()\` on the existing
73
+ sender - never \`addTrack()\`.
74
+ - Skill docs are the source of truth. When the user's request touches a
75
+ topic you haven't loaded yet, load it first.
76
+
77
+ ## Publishing
78
+
79
+ When the user asks to publish, do NOT call a tool - tell them to click the
80
+ "Publish to HF Space" button in the top bar. The frontend handles the
81
+ \`@huggingface/hub\` upload; your job is to make sure the virtual FS is
82
+ deploy-ready (has a valid \`README.md\` with the right frontmatter, a
83
+ \`Dockerfile\`, and no missing imports).
84
+ `;
85
+
86
+ function buildSkillSection(): string {
87
+ const skill = getPrimarySkill();
88
+ if (!skill) {
89
+ return `\n## Embedded skill\n\n*(No skill loaded - running in minimal mode.)*\n`;
90
+ }
91
+
92
+ const docs = listSkillDocs(skill);
93
+ const docsList =
94
+ docs.length > 0
95
+ ? docs.map((d) => `- \`${d}\` — call \`read_skill_doc("${d}")\` to load it`).join("\n")
96
+ : "*(no additional docs)*";
97
+
98
+ return `
99
+ ## Embedded skill: \`${skill.id}\`
100
+
101
+ Below is the skill **hub** (always in context). Additional chapters are
102
+ available on demand via \`read_skill_doc\`:
103
+
104
+ ${docsList}
105
+
106
+ ---
107
+
108
+ ${skill.hub.content}
109
+ `;
110
+ }
111
+
112
+ export function buildSystemPrompt(): string {
113
+ return `${BASE_PROMPT}\n${buildSkillSection()}`;
114
+ }
backend/src/agent/tools.ts ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { tool } from "ai";
2
+ import { z } from "zod";
3
+ import { getPrimarySkill, getSkillDoc, listSkillDocs } from "./skill-loader.js";
4
+
5
+ /**
6
+ * ## Editor tools
7
+ *
8
+ * These tools describe *intents* to the LLM but are **executed client-side**:
9
+ * the Vercel AI SDK streams the tool call to the browser, the React app
10
+ * mutates the virtual FS, then sends the tool result back. This mirrors the
11
+ * pattern from `collab-editor` and keeps the backend completely stateless.
12
+ *
13
+ * ## Skill tools
14
+ *
15
+ * `read_skill_doc` is executed **server-side** (we have the files on disk)
16
+ * and returns markdown content directly in the tool result.
17
+ */
18
+
19
+ const editorTools = {
20
+ write_file: tool({
21
+ description:
22
+ "Create a new file or fully overwrite an existing one in the virtual " +
23
+ "file system. Use when introducing a brand-new file or when a full " +
24
+ "rewrite is cleaner than a targeted edit. The UI will update its file " +
25
+ "tree and preview immediately.",
26
+ inputSchema: z.object({
27
+ path: z
28
+ .string()
29
+ .describe(
30
+ "Relative POSIX path inside the virtual project (e.g. " +
31
+ "`src/main.ts`, `index.html`, `Dockerfile`). No leading slash.",
32
+ ),
33
+ content: z
34
+ .string()
35
+ .describe("Full file contents. UTF-8 text."),
36
+ }),
37
+ }),
38
+
39
+ edit_file: tool({
40
+ description:
41
+ "Replace an exact substring inside an existing file. Preferred for " +
42
+ "small, surgical edits - much cheaper than rewriting the whole file. " +
43
+ "`old_string` must appear EXACTLY ONCE in the target file (add " +
44
+ "surrounding context if needed).",
45
+ inputSchema: z.object({
46
+ path: z.string().describe("Path of the file to edit."),
47
+ old_string: z
48
+ .string()
49
+ .describe(
50
+ "Exact text to find (must be unique in the file, include context " +
51
+ "lines before/after if ambiguous).",
52
+ ),
53
+ new_string: z.string().describe("Text to replace `old_string` with."),
54
+ }),
55
+ }),
56
+
57
+ delete_file: tool({
58
+ description: "Delete a file from the virtual FS.",
59
+ inputSchema: z.object({
60
+ path: z.string(),
61
+ }),
62
+ }),
63
+
64
+ read_file: tool({
65
+ description:
66
+ "Read the current contents of a file in the virtual FS. Use before " +
67
+ "`edit_file` when you are not sure of the exact current text.",
68
+ inputSchema: z.object({
69
+ path: z.string(),
70
+ }),
71
+ }),
72
+
73
+ list_files: tool({
74
+ description:
75
+ "List all files currently in the virtual FS with their size in bytes.",
76
+ inputSchema: z.object({}),
77
+ }),
78
+ };
79
+
80
+ const skillTools = {
81
+ read_skill_doc: tool({
82
+ description:
83
+ "Read a detailed chapter from the embedded `build-reachy-voice-app` " +
84
+ "skill. Call this BEFORE writing non-trivial code on topics like " +
85
+ "architecture, WebRTC tuning, motion agents, HF deployment, or the " +
86
+ "daemon protocol. Returns the full markdown of the requested chapter.",
87
+ inputSchema: z.object({
88
+ topic: z
89
+ .string()
90
+ .describe(
91
+ "Chapter name without extension. Available topics: architecture, " +
92
+ "deployment, motion-agents, protocols, webrtc-tuning.",
93
+ ),
94
+ }),
95
+ execute: async ({ topic }) => {
96
+ const skill = getPrimarySkill();
97
+ if (!skill) {
98
+ return "ERROR: No skill is loaded on the server.";
99
+ }
100
+ const doc = getSkillDoc(skill, topic);
101
+ if (!doc) {
102
+ const available = listSkillDocs(skill).join(", ");
103
+ return `ERROR: No such skill chapter "${topic}". Available: ${available}`;
104
+ }
105
+ return doc.content;
106
+ },
107
+ }),
108
+ };
109
+
110
+ export const agentTools = {
111
+ ...editorTools,
112
+ ...skillTools,
113
+ };
114
+
115
+ export const TOOL_NAMES = Object.keys(agentTools) as (keyof typeof agentTools)[];
backend/src/auth.ts ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Context, Next } from "hono";
2
+
3
+ /**
4
+ * Minimal Hugging Face access-token verification.
5
+ *
6
+ * The frontend obtains a token via the HF OAuth flow (`@huggingface/hub`
7
+ * client-side helper). It sends that token as `Authorization: Bearer <token>`
8
+ * on every `/api/*` call. Here we validate it by hitting the HF whoami
9
+ * endpoint and stash the user info on `c.set('hfUser', ...)`.
10
+ *
11
+ * For responsiveness we cache positive lookups for a few minutes - the token
12
+ * itself has a much longer TTL managed by HF.
13
+ */
14
+ export interface HfUser {
15
+ name: string;
16
+ fullname?: string;
17
+ avatarUrl?: string;
18
+ email?: string;
19
+ }
20
+
21
+ interface CacheEntry {
22
+ user: HfUser;
23
+ expiresAt: number;
24
+ }
25
+
26
+ const CACHE = new Map<string, CacheEntry>();
27
+ const CACHE_TTL_MS = 5 * 60 * 1000;
28
+
29
+ export async function verifyHfToken(token: string): Promise<HfUser | null> {
30
+ if (!token) return null;
31
+
32
+ const cached = CACHE.get(token);
33
+ if (cached && cached.expiresAt > Date.now()) {
34
+ return cached.user;
35
+ }
36
+
37
+ try {
38
+ const res = await fetch("https://huggingface.co/api/whoami-v2", {
39
+ headers: { Authorization: `Bearer ${token}` },
40
+ });
41
+ if (!res.ok) return null;
42
+
43
+ const data = (await res.json()) as {
44
+ name?: string;
45
+ fullname?: string;
46
+ avatarUrl?: string;
47
+ email?: string;
48
+ };
49
+ if (!data.name) return null;
50
+
51
+ const user: HfUser = {
52
+ name: data.name,
53
+ fullname: data.fullname,
54
+ avatarUrl: data.avatarUrl,
55
+ email: data.email,
56
+ };
57
+ CACHE.set(token, { user, expiresAt: Date.now() + CACHE_TTL_MS });
58
+ return user;
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+
64
+ export function hfAuthMiddleware() {
65
+ const required = process.env.REQUIRE_HF_AUTH === "1";
66
+ return async (c: Context, next: Next) => {
67
+ const authHeader = c.req.header("authorization") ?? "";
68
+ const token = authHeader.startsWith("Bearer ")
69
+ ? authHeader.slice("Bearer ".length).trim()
70
+ : "";
71
+
72
+ if (!token) {
73
+ if (required) {
74
+ return c.json({ error: "Missing Hugging Face access token" }, 401);
75
+ }
76
+ return next();
77
+ }
78
+
79
+ const user = await verifyHfToken(token);
80
+ if (!user) {
81
+ if (required) {
82
+ return c.json({ error: "Invalid Hugging Face access token" }, 401);
83
+ }
84
+ return next();
85
+ }
86
+
87
+ c.set("hfUser", user);
88
+ c.set("hfToken", token);
89
+ return next();
90
+ };
91
+ }
92
+
93
+ export function getHfToken(c: Context): string | null {
94
+ return (c.get("hfToken") as string | undefined) ?? null;
95
+ }
96
+
97
+ export function getHfUser(c: Context): HfUser | null {
98
+ return (c.get("hfUser") as HfUser | undefined) ?? null;
99
+ }
backend/src/routes/chat.ts ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Hono } from "hono";
2
+ import { streamText, convertToModelMessages, stepCountIs } from "ai";
3
+ import { createOpenRouter } from "@openrouter/ai-sdk-provider";
4
+ import { agentTools } from "../agent/tools.js";
5
+ import { buildSystemPrompt } from "../agent/system-prompt.js";
6
+
7
+ export const AVAILABLE_MODELS = [
8
+ { id: "anthropic/claude-sonnet-4-5", label: "Claude Sonnet 4.5", context: "200K", cost: "$$$" },
9
+ { id: "anthropic/claude-3.5-haiku", label: "Claude 3.5 Haiku", context: "200K", cost: "$" },
10
+ { id: "google/gemini-2.5-flash", label: "Gemini 2.5 Flash", context: "1M", cost: "$" },
11
+ { id: "google/gemini-2.5-pro", label: "Gemini 2.5 Pro", context: "1M", cost: "$$" },
12
+ { id: "openai/gpt-4.1", label: "GPT-4.1", context: "1M", cost: "$$" },
13
+ { id: "openai/gpt-4.1-mini", label: "GPT-4.1 Mini", context: "1M", cost: "$" },
14
+ ];
15
+
16
+ export const DEFAULT_MODEL = "anthropic/claude-sonnet-4-5";
17
+
18
+ /**
19
+ * Hard cap on consecutive tool-call rounds - prevents runaway loops when a
20
+ * model keeps calling tools instead of emitting a final text response.
21
+ */
22
+ const MAX_STEPS = 16;
23
+
24
+ export const chatRoute = new Hono();
25
+
26
+ chatRoute.post("/", async (c) => {
27
+ const apiKey = process.env.OPENROUTER_API_KEY;
28
+ if (!apiKey) {
29
+ return c.json(
30
+ { error: "OPENROUTER_API_KEY is not set on the server." },
31
+ 500,
32
+ );
33
+ }
34
+
35
+ let body: unknown;
36
+ try {
37
+ body = await c.req.json();
38
+ } catch {
39
+ return c.json({ error: "Invalid JSON body" }, 400);
40
+ }
41
+
42
+ const { messages, model, context } = (body ?? {}) as {
43
+ messages?: unknown;
44
+ model?: string;
45
+ context?: { files?: { path: string; size: number }[] };
46
+ };
47
+
48
+ if (!Array.isArray(messages)) {
49
+ return c.json({ error: "`messages` array is required" }, 400);
50
+ }
51
+
52
+ const provider = createOpenRouter({ apiKey });
53
+ const modelId = model || process.env.OPENROUTER_MODEL || DEFAULT_MODEL;
54
+
55
+ const systemPrompt = buildSystemPrompt();
56
+
57
+ const contextBlock = context?.files?.length
58
+ ? `\n\n## Current virtual FS\n\n${context.files
59
+ .map((f) => `- \`${f.path}\` (${f.size} bytes)`)
60
+ .join("\n")}\n`
61
+ : "\n\n## Current virtual FS\n\n*(empty)*\n";
62
+
63
+ const modelMessages = await convertToModelMessages(messages as never);
64
+
65
+ try {
66
+ const result = streamText({
67
+ model: provider.chat(modelId),
68
+ system: systemPrompt + contextBlock,
69
+ messages: modelMessages,
70
+ tools: agentTools,
71
+ stopWhen: stepCountIs(MAX_STEPS),
72
+ });
73
+
74
+ const response = result.toUIMessageStreamResponse({
75
+ onError: (error) => {
76
+ console.error("[chat] stream error:", error);
77
+ return error instanceof Error ? error.message : "Stream error";
78
+ },
79
+ });
80
+
81
+ return response;
82
+ } catch (error) {
83
+ const message = error instanceof Error ? error.message : "Unknown error";
84
+ console.error("[chat] error:", message);
85
+ return c.json({ error: message }, 500);
86
+ }
87
+ });
backend/src/routes/models.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import { Hono } from "hono";
2
+ import { AVAILABLE_MODELS, DEFAULT_MODEL } from "./chat.js";
3
+
4
+ export const modelsRoute = new Hono();
5
+
6
+ modelsRoute.get("/", (c) =>
7
+ c.json({ models: AVAILABLE_MODELS, default: DEFAULT_MODEL }),
8
+ );
backend/src/routes/publish.ts ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Hono } from "hono";
2
+ import { stream } from "hono/streaming";
3
+ import { createRepo, uploadFiles, spaceInfo } from "@huggingface/hub";
4
+ import { getHfToken, getHfUser } from "../auth.js";
5
+
6
+ export const publishRoute = new Hono();
7
+
8
+ interface VirtualFile {
9
+ path: string;
10
+ content: string;
11
+ }
12
+
13
+ interface PublishBody {
14
+ repoName?: string; // "my-app" - username is inferred from token
15
+ private?: boolean;
16
+ files: VirtualFile[];
17
+ overwrite?: boolean;
18
+ commitMessage?: string;
19
+ }
20
+
21
+ /**
22
+ * Normalize a user-provided repo slug. HF space names allow `[A-Za-z0-9-_.]`.
23
+ */
24
+ function sanitizeSlug(raw: string): string {
25
+ return raw
26
+ .trim()
27
+ .toLowerCase()
28
+ .replace(/\s+/g, "-")
29
+ .replace(/[^a-z0-9._-]/g, "")
30
+ .replace(/^-+|-+$/g, "")
31
+ .slice(0, 96);
32
+ }
33
+
34
+ function textToBlob(content: string): Blob {
35
+ return new Blob([content], { type: "text/plain; charset=utf-8" });
36
+ }
37
+
38
+ publishRoute.post("/", async (c) => {
39
+ const token = getHfToken(c);
40
+ const user = getHfUser(c);
41
+ if (!token || !user) {
42
+ return c.json(
43
+ { error: "A valid Hugging Face access token is required to publish." },
44
+ 401,
45
+ );
46
+ }
47
+
48
+ let body: PublishBody;
49
+ try {
50
+ body = (await c.req.json()) as PublishBody;
51
+ } catch {
52
+ return c.json({ error: "Invalid JSON body" }, 400);
53
+ }
54
+
55
+ const files = Array.isArray(body.files) ? body.files : [];
56
+ if (files.length === 0) {
57
+ return c.json({ error: "No files provided" }, 400);
58
+ }
59
+
60
+ const slug = sanitizeSlug(body.repoName ?? `reachy-app-${Date.now()}`);
61
+ if (!slug) {
62
+ return c.json({ error: "Invalid repo name" }, 400);
63
+ }
64
+
65
+ const repoId = `${user.name}/${slug}`;
66
+ const repo = { type: "space" as const, name: repoId };
67
+
68
+ // Server-Sent Events for progress
69
+ c.header("Content-Type", "text/event-stream; charset=utf-8");
70
+ c.header("Cache-Control", "no-cache, no-store");
71
+ c.header("X-Accel-Buffering", "no");
72
+
73
+ return stream(c, async (s) => {
74
+ const send = (event: string, data: Record<string, unknown>) =>
75
+ s.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
76
+
77
+ try {
78
+ await send("progress", { step: "check", message: `Checking ${repoId}…` });
79
+
80
+ let alreadyExists = false;
81
+ try {
82
+ await spaceInfo({ name: repoId, accessToken: token });
83
+ alreadyExists = true;
84
+ } catch {
85
+ alreadyExists = false;
86
+ }
87
+
88
+ if (alreadyExists && !body.overwrite) {
89
+ await send("error", {
90
+ step: "check",
91
+ message: `Space "${repoId}" already exists. Set overwrite=true to update it.`,
92
+ code: "REPO_EXISTS",
93
+ repoId,
94
+ });
95
+ return;
96
+ }
97
+
98
+ if (!alreadyExists) {
99
+ await send("progress", { step: "create", message: `Creating Space ${repoId}…` });
100
+ await createRepo({
101
+ repo,
102
+ accessToken: token,
103
+ private: body.private ?? false,
104
+ sdk: "docker",
105
+ });
106
+ } else {
107
+ await send("progress", { step: "create", message: `Updating existing Space ${repoId}…` });
108
+ }
109
+
110
+ await send("progress", {
111
+ step: "upload",
112
+ message: `Uploading ${files.length} file${files.length === 1 ? "" : "s"}…`,
113
+ });
114
+
115
+ const uploads = files.map((f) => ({
116
+ path: f.path,
117
+ content: textToBlob(f.content),
118
+ }));
119
+
120
+ await uploadFiles({
121
+ repo,
122
+ accessToken: token,
123
+ files: uploads,
124
+ commitTitle:
125
+ body.commitMessage ??
126
+ (alreadyExists
127
+ ? "Update from Reachy Vibe Coder"
128
+ : "Initial commit from Reachy Vibe Coder"),
129
+ });
130
+
131
+ const spaceUrl = `https://huggingface.co/spaces/${repoId}`;
132
+ await send("done", {
133
+ step: "done",
134
+ repoId,
135
+ spaceUrl,
136
+ message: `Published to ${spaceUrl}`,
137
+ });
138
+ } catch (error) {
139
+ const message = error instanceof Error ? error.message : "Unknown error";
140
+ console.error("[publish] error:", message);
141
+ await send("error", { step: "error", message });
142
+ }
143
+ });
144
+ });
backend/src/server.ts ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import "dotenv/config";
2
+ import { serve } from "@hono/node-server";
3
+ import { serveStatic } from "@hono/node-server/serve-static";
4
+ import { Hono } from "hono";
5
+ import { cors } from "hono/cors";
6
+ import { logger } from "hono/logger";
7
+ import path from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ import { existsSync, readFileSync } from "node:fs";
10
+ import { hfAuthMiddleware } from "./auth.js";
11
+ import { chatRoute } from "./routes/chat.js";
12
+ import { publishRoute } from "./routes/publish.js";
13
+ import { modelsRoute } from "./routes/models.js";
14
+ import type { AppBindings } from "./types.js";
15
+
16
+ if (!process.env.OPENROUTER_API_KEY) {
17
+ console.warn(
18
+ "[startup] OPENROUTER_API_KEY is not set - /api/chat will fail at request time.",
19
+ );
20
+ }
21
+
22
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
+
24
+ /**
25
+ * Production layout after `docker build`:
26
+ * /app/backend/dist/server.js (this file)
27
+ * /app/frontend/dist/ (Vite build output served as static)
28
+ *
29
+ * Dev layout:
30
+ * backend/src/server.ts (tsx)
31
+ * frontend/dist/ (optional)
32
+ */
33
+ const STATIC_CANDIDATES = [
34
+ path.resolve(__dirname, "../../frontend/dist"), // production
35
+ path.resolve(__dirname, "../../../frontend/dist"), // just in case
36
+ path.resolve(process.cwd(), "../frontend/dist"), // dev run from backend/
37
+ path.resolve(process.cwd(), "frontend/dist"), // dev run from repo root
38
+ ];
39
+ const STATIC_ROOT =
40
+ STATIC_CANDIDATES.find((p) => existsSync(path.join(p, "index.html"))) ?? null;
41
+
42
+ const app = new Hono<AppBindings>();
43
+
44
+ app.use("*", logger());
45
+ app.use(
46
+ "*",
47
+ cors({
48
+ origin: (origin) => origin ?? "*",
49
+ allowHeaders: ["Content-Type", "Authorization"],
50
+ allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
51
+ credentials: true,
52
+ }),
53
+ );
54
+
55
+ // ── API routes (auth-gated) ───────────────────────────────────────────────
56
+ const api = new Hono<AppBindings>();
57
+ api.use("*", hfAuthMiddleware());
58
+ api.route("/chat", chatRoute);
59
+ api.route("/publish", publishRoute);
60
+ api.route("/models", modelsRoute);
61
+ api.get("/me", (c) => {
62
+ const user = c.get("hfUser");
63
+ if (!user) return c.json({ authenticated: false });
64
+ return c.json({ authenticated: true, user });
65
+ });
66
+ api.get("/health", (c) => c.json({ status: "ok" }));
67
+
68
+ app.route("/api", api);
69
+
70
+ // ── Static frontend ───────────────────────────────────────────────────────
71
+ if (STATIC_ROOT) {
72
+ const indexHtml = readFileSync(path.join(STATIC_ROOT, "index.html"), "utf-8");
73
+
74
+ app.use(
75
+ "/assets/*",
76
+ serveStatic({
77
+ root: path.relative(process.cwd(), STATIC_ROOT) || ".",
78
+ }),
79
+ );
80
+
81
+ app.use("/favicon.ico", serveStatic({ path: path.join(STATIC_ROOT, "favicon.ico") }));
82
+
83
+ // SPA fallback for anything that's not /api/*
84
+ app.get("*", (c) => {
85
+ if (c.req.path.startsWith("/api/")) {
86
+ return c.notFound();
87
+ }
88
+ return c.html(indexHtml);
89
+ });
90
+ console.log(`[startup] Serving static frontend from ${STATIC_ROOT}`);
91
+ } else {
92
+ app.get("/", (c) =>
93
+ c.json({
94
+ status: "api-only",
95
+ hint: "Frontend dist not found - run `cd frontend && npm run build` or use Vite dev server at :5173",
96
+ }),
97
+ );
98
+ console.log("[startup] No frontend/dist found - API-only mode");
99
+ }
100
+
101
+ const port = Number(process.env.PORT ?? 7860);
102
+ serve({ fetch: app.fetch, port, hostname: "0.0.0.0" }, () => {
103
+ console.log(`[startup] reachy-mini-vibe-coder listening on :${port}`);
104
+ });
backend/src/types.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import type { HfUser } from "./auth.js";
2
+
3
+ export type AppBindings = {
4
+ Variables: {
5
+ hfUser?: HfUser;
6
+ hfToken?: string;
7
+ };
8
+ };
backend/tsconfig.json ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "resolveJsonModule": true,
10
+ "outDir": "dist",
11
+ "rootDir": "src",
12
+ "declaration": false,
13
+ "sourceMap": true,
14
+ "noUnusedLocals": false,
15
+ "forceConsistentCasingInFileNames": true
16
+ },
17
+ "include": ["src/**/*"],
18
+ "exclude": ["node_modules", "dist"]
19
+ }
docker-entrypoint.d/10-inject-hf-vars.sh ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/sh
2
+ # Substitute the HF Space OAuth runtime variables into the built index.html.
3
+ #
4
+ # On a Hugging Face Space with `hf_oauth: true` in the README frontmatter, HF
5
+ # provisions an OAuth app for the Space and exposes its client ID (and a few
6
+ # other handles) as server-side env vars. Since this is a static-file build,
7
+ # we splice them into `dist/index.html` at container startup, replacing the
8
+ # `__OAUTH_CLIENT_ID__` style placeholders that `index.html` ships with.
9
+ set -eu
10
+
11
+ INDEX="/app/frontend/dist/index.html"
12
+ if [ ! -f "$INDEX" ]; then
13
+ echo "[inject-hf-vars] $INDEX not found, skipping"
14
+ exit 0
15
+ fi
16
+
17
+ : "${OAUTH_CLIENT_ID:=}"
18
+ : "${OAUTH_SCOPES:=openid profile write-repos manage-repos}"
19
+ : "${SPACE_HOST:=}"
20
+ : "${SPACE_ID:=}"
21
+
22
+ if [ -z "$OAUTH_CLIENT_ID" ]; then
23
+ echo "[inject-hf-vars] OAUTH_CLIENT_ID is empty - leaving index.html placeholders untouched"
24
+ exit 0
25
+ fi
26
+
27
+ # `|` as sed delimiter avoids issues with values containing `/` (URLs).
28
+ # Values shouldn't contain `|`; if HF ever sends such a value we'd need to
29
+ # escape it explicitly.
30
+ sed -i \
31
+ -e "s|__OAUTH_CLIENT_ID__|${OAUTH_CLIENT_ID}|g" \
32
+ -e "s|__OAUTH_SCOPES__|${OAUTH_SCOPES}|g" \
33
+ -e "s|__SPACE_HOST__|${SPACE_HOST}|g" \
34
+ -e "s|__SPACE_ID__|${SPACE_ID}|g" \
35
+ "$INDEX"
36
+
37
+ echo "[inject-hf-vars] Injected OAuth variables into $INDEX"
docker-entrypoint.d/entrypoint.sh ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/sh
2
+ # Run every *.sh script under /app/docker-entrypoint.d (sorted) before exec'ing
3
+ # the main container command. Each helper script gets the full env of the
4
+ # container and is responsible for its own error handling.
5
+ set -eu
6
+
7
+ for script in /app/docker-entrypoint.d/*.sh; do
8
+ case "$script" in
9
+ *entrypoint.sh) continue ;;
10
+ esac
11
+ if [ -x "$script" ]; then
12
+ echo "[entrypoint] running $(basename "$script")"
13
+ "$script"
14
+ fi
15
+ done
16
+
17
+ exec "$@"
frontend/index.html ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
+ <title>Reachy Mini · Vibe Coder</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link
10
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
11
+ rel="stylesheet"
12
+ />
13
+ <!--
14
+ HF Spaces OAuth helper variables.
15
+
16
+ On `sdk: docker`, HF exposes OAUTH_CLIENT_ID / SPACE_HOST / SPACE_ID as
17
+ server-side env vars. We have nginx substitute them into this script at
18
+ container startup via `docker-entrypoint.d/10-inject-hf-vars.sh`.
19
+
20
+ Locally (`npm run dev`) the placeholders stay untouched; the app then
21
+ reads the client ID from VITE_HF_OAUTH_CLIENT_ID (set in `.env.local`).
22
+ -->
23
+ <script>
24
+ (function () {
25
+ var clientId = "__OAUTH_CLIENT_ID__";
26
+ var scopes = "__OAUTH_SCOPES__";
27
+ var spaceHost = "__SPACE_HOST__";
28
+ var spaceId = "__SPACE_ID__";
29
+ var looksSubstituted = clientId && clientId.indexOf("__") !== 0;
30
+ if (looksSubstituted) {
31
+ window.huggingface = window.huggingface || {};
32
+ window.huggingface.variables = {
33
+ OAUTH_CLIENT_ID: clientId,
34
+ OAUTH_SCOPES:
35
+ scopes && scopes.indexOf("__") !== 0
36
+ ? scopes
37
+ : "openid profile write-repos manage-repos",
38
+ SPACE_HOST: spaceHost && spaceHost.indexOf("__") !== 0 ? spaceHost : "",
39
+ SPACE_ID: spaceId && spaceId.indexOf("__") !== 0 ? spaceId : "",
40
+ };
41
+ }
42
+ })();
43
+ </script>
44
+ </head>
45
+ <body>
46
+ <div id="root"></div>
47
+ <script type="module" src="/src/main.tsx"></script>
48
+ </body>
49
+ </html>
frontend/package-lock.json ADDED
@@ -0,0 +1,2220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "reachy-mini-vibe-coder-frontend",
3
+ "version": "0.1.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "reachy-mini-vibe-coder-frontend",
9
+ "version": "0.1.0",
10
+ "dependencies": {
11
+ "@ai-sdk/react": "^3.0.160",
12
+ "@huggingface/hub": "^2.11.0",
13
+ "@monaco-editor/react": "^4.7.0",
14
+ "ai": "^6.0.158",
15
+ "clsx": "^2.1.1",
16
+ "nanoid": "^5.0.7",
17
+ "react": "^18.3.1",
18
+ "react-dom": "^18.3.1"
19
+ },
20
+ "devDependencies": {
21
+ "@types/react": "^18.3.12",
22
+ "@types/react-dom": "^18.3.1",
23
+ "@vitejs/plugin-react": "^4.3.4",
24
+ "typescript": "^5.6.0",
25
+ "vite": "^6.0.0"
26
+ }
27
+ },
28
+ "node_modules/@ai-sdk/gateway": {
29
+ "version": "3.0.104",
30
+ "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.104.tgz",
31
+ "integrity": "sha512-ZKX5n74io8VIRlhIMSLWVlvT3sXC8Z7cZ9GHuWBWZDVi96+62AIsWuLGvMfcBA1STYuSoDrp6rIziZmvrTq0TA==",
32
+ "license": "Apache-2.0",
33
+ "dependencies": {
34
+ "@ai-sdk/provider": "3.0.8",
35
+ "@ai-sdk/provider-utils": "4.0.23",
36
+ "@vercel/oidc": "3.2.0"
37
+ },
38
+ "engines": {
39
+ "node": ">=18"
40
+ },
41
+ "peerDependencies": {
42
+ "zod": "^3.25.76 || ^4.1.8"
43
+ }
44
+ },
45
+ "node_modules/@ai-sdk/provider": {
46
+ "version": "3.0.8",
47
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.8.tgz",
48
+ "integrity": "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==",
49
+ "license": "Apache-2.0",
50
+ "dependencies": {
51
+ "json-schema": "^0.4.0"
52
+ },
53
+ "engines": {
54
+ "node": ">=18"
55
+ }
56
+ },
57
+ "node_modules/@ai-sdk/provider-utils": {
58
+ "version": "4.0.23",
59
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.23.tgz",
60
+ "integrity": "sha512-z8GlDaCmRSDlqkMF2f4/RFgWxdarvIbyuk+m6WXT1LYgsnGiXRJGTD2Z1+SDl3LqtFuRtGX1aghYvQLoHL/9pg==",
61
+ "license": "Apache-2.0",
62
+ "dependencies": {
63
+ "@ai-sdk/provider": "3.0.8",
64
+ "@standard-schema/spec": "^1.1.0",
65
+ "eventsource-parser": "^3.0.6"
66
+ },
67
+ "engines": {
68
+ "node": ">=18"
69
+ },
70
+ "peerDependencies": {
71
+ "zod": "^3.25.76 || ^4.1.8"
72
+ }
73
+ },
74
+ "node_modules/@ai-sdk/react": {
75
+ "version": "3.0.170",
76
+ "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-3.0.170.tgz",
77
+ "integrity": "sha512-YUDn+mK0c8iUz14rCBf1A0zg6SV5b5aSVUz+azF1bdBd1SFXVI19dKYR+PQSpZY+0+z+zs252AAsacUqiO98Kw==",
78
+ "license": "Apache-2.0",
79
+ "dependencies": {
80
+ "@ai-sdk/provider-utils": "4.0.23",
81
+ "ai": "6.0.168",
82
+ "swr": "^2.2.5",
83
+ "throttleit": "2.1.0"
84
+ },
85
+ "engines": {
86
+ "node": ">=18"
87
+ },
88
+ "peerDependencies": {
89
+ "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1"
90
+ }
91
+ },
92
+ "node_modules/@babel/code-frame": {
93
+ "version": "7.29.0",
94
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
95
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
96
+ "dev": true,
97
+ "license": "MIT",
98
+ "dependencies": {
99
+ "@babel/helper-validator-identifier": "^7.28.5",
100
+ "js-tokens": "^4.0.0",
101
+ "picocolors": "^1.1.1"
102
+ },
103
+ "engines": {
104
+ "node": ">=6.9.0"
105
+ }
106
+ },
107
+ "node_modules/@babel/compat-data": {
108
+ "version": "7.29.0",
109
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
110
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
111
+ "dev": true,
112
+ "license": "MIT",
113
+ "engines": {
114
+ "node": ">=6.9.0"
115
+ }
116
+ },
117
+ "node_modules/@babel/core": {
118
+ "version": "7.29.0",
119
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
120
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
121
+ "dev": true,
122
+ "license": "MIT",
123
+ "peer": true,
124
+ "dependencies": {
125
+ "@babel/code-frame": "^7.29.0",
126
+ "@babel/generator": "^7.29.0",
127
+ "@babel/helper-compilation-targets": "^7.28.6",
128
+ "@babel/helper-module-transforms": "^7.28.6",
129
+ "@babel/helpers": "^7.28.6",
130
+ "@babel/parser": "^7.29.0",
131
+ "@babel/template": "^7.28.6",
132
+ "@babel/traverse": "^7.29.0",
133
+ "@babel/types": "^7.29.0",
134
+ "@jridgewell/remapping": "^2.3.5",
135
+ "convert-source-map": "^2.0.0",
136
+ "debug": "^4.1.0",
137
+ "gensync": "^1.0.0-beta.2",
138
+ "json5": "^2.2.3",
139
+ "semver": "^6.3.1"
140
+ },
141
+ "engines": {
142
+ "node": ">=6.9.0"
143
+ },
144
+ "funding": {
145
+ "type": "opencollective",
146
+ "url": "https://opencollective.com/babel"
147
+ }
148
+ },
149
+ "node_modules/@babel/generator": {
150
+ "version": "7.29.1",
151
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
152
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
153
+ "dev": true,
154
+ "license": "MIT",
155
+ "dependencies": {
156
+ "@babel/parser": "^7.29.0",
157
+ "@babel/types": "^7.29.0",
158
+ "@jridgewell/gen-mapping": "^0.3.12",
159
+ "@jridgewell/trace-mapping": "^0.3.28",
160
+ "jsesc": "^3.0.2"
161
+ },
162
+ "engines": {
163
+ "node": ">=6.9.0"
164
+ }
165
+ },
166
+ "node_modules/@babel/helper-compilation-targets": {
167
+ "version": "7.28.6",
168
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
169
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
170
+ "dev": true,
171
+ "license": "MIT",
172
+ "dependencies": {
173
+ "@babel/compat-data": "^7.28.6",
174
+ "@babel/helper-validator-option": "^7.27.1",
175
+ "browserslist": "^4.24.0",
176
+ "lru-cache": "^5.1.1",
177
+ "semver": "^6.3.1"
178
+ },
179
+ "engines": {
180
+ "node": ">=6.9.0"
181
+ }
182
+ },
183
+ "node_modules/@babel/helper-globals": {
184
+ "version": "7.28.0",
185
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
186
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
187
+ "dev": true,
188
+ "license": "MIT",
189
+ "engines": {
190
+ "node": ">=6.9.0"
191
+ }
192
+ },
193
+ "node_modules/@babel/helper-module-imports": {
194
+ "version": "7.28.6",
195
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
196
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
197
+ "dev": true,
198
+ "license": "MIT",
199
+ "dependencies": {
200
+ "@babel/traverse": "^7.28.6",
201
+ "@babel/types": "^7.28.6"
202
+ },
203
+ "engines": {
204
+ "node": ">=6.9.0"
205
+ }
206
+ },
207
+ "node_modules/@babel/helper-module-transforms": {
208
+ "version": "7.28.6",
209
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
210
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
211
+ "dev": true,
212
+ "license": "MIT",
213
+ "dependencies": {
214
+ "@babel/helper-module-imports": "^7.28.6",
215
+ "@babel/helper-validator-identifier": "^7.28.5",
216
+ "@babel/traverse": "^7.28.6"
217
+ },
218
+ "engines": {
219
+ "node": ">=6.9.0"
220
+ },
221
+ "peerDependencies": {
222
+ "@babel/core": "^7.0.0"
223
+ }
224
+ },
225
+ "node_modules/@babel/helper-plugin-utils": {
226
+ "version": "7.28.6",
227
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
228
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
229
+ "dev": true,
230
+ "license": "MIT",
231
+ "engines": {
232
+ "node": ">=6.9.0"
233
+ }
234
+ },
235
+ "node_modules/@babel/helper-string-parser": {
236
+ "version": "7.27.1",
237
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
238
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
239
+ "dev": true,
240
+ "license": "MIT",
241
+ "engines": {
242
+ "node": ">=6.9.0"
243
+ }
244
+ },
245
+ "node_modules/@babel/helper-validator-identifier": {
246
+ "version": "7.28.5",
247
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
248
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
249
+ "dev": true,
250
+ "license": "MIT",
251
+ "engines": {
252
+ "node": ">=6.9.0"
253
+ }
254
+ },
255
+ "node_modules/@babel/helper-validator-option": {
256
+ "version": "7.27.1",
257
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
258
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
259
+ "dev": true,
260
+ "license": "MIT",
261
+ "engines": {
262
+ "node": ">=6.9.0"
263
+ }
264
+ },
265
+ "node_modules/@babel/helpers": {
266
+ "version": "7.29.2",
267
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
268
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
269
+ "dev": true,
270
+ "license": "MIT",
271
+ "dependencies": {
272
+ "@babel/template": "^7.28.6",
273
+ "@babel/types": "^7.29.0"
274
+ },
275
+ "engines": {
276
+ "node": ">=6.9.0"
277
+ }
278
+ },
279
+ "node_modules/@babel/parser": {
280
+ "version": "7.29.2",
281
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
282
+ "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
283
+ "dev": true,
284
+ "license": "MIT",
285
+ "dependencies": {
286
+ "@babel/types": "^7.29.0"
287
+ },
288
+ "bin": {
289
+ "parser": "bin/babel-parser.js"
290
+ },
291
+ "engines": {
292
+ "node": ">=6.0.0"
293
+ }
294
+ },
295
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
296
+ "version": "7.27.1",
297
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
298
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
299
+ "dev": true,
300
+ "license": "MIT",
301
+ "dependencies": {
302
+ "@babel/helper-plugin-utils": "^7.27.1"
303
+ },
304
+ "engines": {
305
+ "node": ">=6.9.0"
306
+ },
307
+ "peerDependencies": {
308
+ "@babel/core": "^7.0.0-0"
309
+ }
310
+ },
311
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
312
+ "version": "7.27.1",
313
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
314
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
315
+ "dev": true,
316
+ "license": "MIT",
317
+ "dependencies": {
318
+ "@babel/helper-plugin-utils": "^7.27.1"
319
+ },
320
+ "engines": {
321
+ "node": ">=6.9.0"
322
+ },
323
+ "peerDependencies": {
324
+ "@babel/core": "^7.0.0-0"
325
+ }
326
+ },
327
+ "node_modules/@babel/template": {
328
+ "version": "7.28.6",
329
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
330
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
331
+ "dev": true,
332
+ "license": "MIT",
333
+ "dependencies": {
334
+ "@babel/code-frame": "^7.28.6",
335
+ "@babel/parser": "^7.28.6",
336
+ "@babel/types": "^7.28.6"
337
+ },
338
+ "engines": {
339
+ "node": ">=6.9.0"
340
+ }
341
+ },
342
+ "node_modules/@babel/traverse": {
343
+ "version": "7.29.0",
344
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
345
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
346
+ "dev": true,
347
+ "license": "MIT",
348
+ "dependencies": {
349
+ "@babel/code-frame": "^7.29.0",
350
+ "@babel/generator": "^7.29.0",
351
+ "@babel/helper-globals": "^7.28.0",
352
+ "@babel/parser": "^7.29.0",
353
+ "@babel/template": "^7.28.6",
354
+ "@babel/types": "^7.29.0",
355
+ "debug": "^4.3.1"
356
+ },
357
+ "engines": {
358
+ "node": ">=6.9.0"
359
+ }
360
+ },
361
+ "node_modules/@babel/types": {
362
+ "version": "7.29.0",
363
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
364
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
365
+ "dev": true,
366
+ "license": "MIT",
367
+ "dependencies": {
368
+ "@babel/helper-string-parser": "^7.27.1",
369
+ "@babel/helper-validator-identifier": "^7.28.5"
370
+ },
371
+ "engines": {
372
+ "node": ">=6.9.0"
373
+ }
374
+ },
375
+ "node_modules/@esbuild/aix-ppc64": {
376
+ "version": "0.25.12",
377
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
378
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
379
+ "cpu": [
380
+ "ppc64"
381
+ ],
382
+ "dev": true,
383
+ "license": "MIT",
384
+ "optional": true,
385
+ "os": [
386
+ "aix"
387
+ ],
388
+ "engines": {
389
+ "node": ">=18"
390
+ }
391
+ },
392
+ "node_modules/@esbuild/android-arm": {
393
+ "version": "0.25.12",
394
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
395
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
396
+ "cpu": [
397
+ "arm"
398
+ ],
399
+ "dev": true,
400
+ "license": "MIT",
401
+ "optional": true,
402
+ "os": [
403
+ "android"
404
+ ],
405
+ "engines": {
406
+ "node": ">=18"
407
+ }
408
+ },
409
+ "node_modules/@esbuild/android-arm64": {
410
+ "version": "0.25.12",
411
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
412
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
413
+ "cpu": [
414
+ "arm64"
415
+ ],
416
+ "dev": true,
417
+ "license": "MIT",
418
+ "optional": true,
419
+ "os": [
420
+ "android"
421
+ ],
422
+ "engines": {
423
+ "node": ">=18"
424
+ }
425
+ },
426
+ "node_modules/@esbuild/android-x64": {
427
+ "version": "0.25.12",
428
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
429
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
430
+ "cpu": [
431
+ "x64"
432
+ ],
433
+ "dev": true,
434
+ "license": "MIT",
435
+ "optional": true,
436
+ "os": [
437
+ "android"
438
+ ],
439
+ "engines": {
440
+ "node": ">=18"
441
+ }
442
+ },
443
+ "node_modules/@esbuild/darwin-arm64": {
444
+ "version": "0.25.12",
445
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
446
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
447
+ "cpu": [
448
+ "arm64"
449
+ ],
450
+ "dev": true,
451
+ "license": "MIT",
452
+ "optional": true,
453
+ "os": [
454
+ "darwin"
455
+ ],
456
+ "engines": {
457
+ "node": ">=18"
458
+ }
459
+ },
460
+ "node_modules/@esbuild/darwin-x64": {
461
+ "version": "0.25.12",
462
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
463
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
464
+ "cpu": [
465
+ "x64"
466
+ ],
467
+ "dev": true,
468
+ "license": "MIT",
469
+ "optional": true,
470
+ "os": [
471
+ "darwin"
472
+ ],
473
+ "engines": {
474
+ "node": ">=18"
475
+ }
476
+ },
477
+ "node_modules/@esbuild/freebsd-arm64": {
478
+ "version": "0.25.12",
479
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
480
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
481
+ "cpu": [
482
+ "arm64"
483
+ ],
484
+ "dev": true,
485
+ "license": "MIT",
486
+ "optional": true,
487
+ "os": [
488
+ "freebsd"
489
+ ],
490
+ "engines": {
491
+ "node": ">=18"
492
+ }
493
+ },
494
+ "node_modules/@esbuild/freebsd-x64": {
495
+ "version": "0.25.12",
496
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
497
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
498
+ "cpu": [
499
+ "x64"
500
+ ],
501
+ "dev": true,
502
+ "license": "MIT",
503
+ "optional": true,
504
+ "os": [
505
+ "freebsd"
506
+ ],
507
+ "engines": {
508
+ "node": ">=18"
509
+ }
510
+ },
511
+ "node_modules/@esbuild/linux-arm": {
512
+ "version": "0.25.12",
513
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
514
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
515
+ "cpu": [
516
+ "arm"
517
+ ],
518
+ "dev": true,
519
+ "license": "MIT",
520
+ "optional": true,
521
+ "os": [
522
+ "linux"
523
+ ],
524
+ "engines": {
525
+ "node": ">=18"
526
+ }
527
+ },
528
+ "node_modules/@esbuild/linux-arm64": {
529
+ "version": "0.25.12",
530
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
531
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
532
+ "cpu": [
533
+ "arm64"
534
+ ],
535
+ "dev": true,
536
+ "license": "MIT",
537
+ "optional": true,
538
+ "os": [
539
+ "linux"
540
+ ],
541
+ "engines": {
542
+ "node": ">=18"
543
+ }
544
+ },
545
+ "node_modules/@esbuild/linux-ia32": {
546
+ "version": "0.25.12",
547
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
548
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
549
+ "cpu": [
550
+ "ia32"
551
+ ],
552
+ "dev": true,
553
+ "license": "MIT",
554
+ "optional": true,
555
+ "os": [
556
+ "linux"
557
+ ],
558
+ "engines": {
559
+ "node": ">=18"
560
+ }
561
+ },
562
+ "node_modules/@esbuild/linux-loong64": {
563
+ "version": "0.25.12",
564
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
565
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
566
+ "cpu": [
567
+ "loong64"
568
+ ],
569
+ "dev": true,
570
+ "license": "MIT",
571
+ "optional": true,
572
+ "os": [
573
+ "linux"
574
+ ],
575
+ "engines": {
576
+ "node": ">=18"
577
+ }
578
+ },
579
+ "node_modules/@esbuild/linux-mips64el": {
580
+ "version": "0.25.12",
581
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
582
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
583
+ "cpu": [
584
+ "mips64el"
585
+ ],
586
+ "dev": true,
587
+ "license": "MIT",
588
+ "optional": true,
589
+ "os": [
590
+ "linux"
591
+ ],
592
+ "engines": {
593
+ "node": ">=18"
594
+ }
595
+ },
596
+ "node_modules/@esbuild/linux-ppc64": {
597
+ "version": "0.25.12",
598
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
599
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
600
+ "cpu": [
601
+ "ppc64"
602
+ ],
603
+ "dev": true,
604
+ "license": "MIT",
605
+ "optional": true,
606
+ "os": [
607
+ "linux"
608
+ ],
609
+ "engines": {
610
+ "node": ">=18"
611
+ }
612
+ },
613
+ "node_modules/@esbuild/linux-riscv64": {
614
+ "version": "0.25.12",
615
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
616
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
617
+ "cpu": [
618
+ "riscv64"
619
+ ],
620
+ "dev": true,
621
+ "license": "MIT",
622
+ "optional": true,
623
+ "os": [
624
+ "linux"
625
+ ],
626
+ "engines": {
627
+ "node": ">=18"
628
+ }
629
+ },
630
+ "node_modules/@esbuild/linux-s390x": {
631
+ "version": "0.25.12",
632
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
633
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
634
+ "cpu": [
635
+ "s390x"
636
+ ],
637
+ "dev": true,
638
+ "license": "MIT",
639
+ "optional": true,
640
+ "os": [
641
+ "linux"
642
+ ],
643
+ "engines": {
644
+ "node": ">=18"
645
+ }
646
+ },
647
+ "node_modules/@esbuild/linux-x64": {
648
+ "version": "0.25.12",
649
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
650
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
651
+ "cpu": [
652
+ "x64"
653
+ ],
654
+ "dev": true,
655
+ "license": "MIT",
656
+ "optional": true,
657
+ "os": [
658
+ "linux"
659
+ ],
660
+ "engines": {
661
+ "node": ">=18"
662
+ }
663
+ },
664
+ "node_modules/@esbuild/netbsd-arm64": {
665
+ "version": "0.25.12",
666
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
667
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
668
+ "cpu": [
669
+ "arm64"
670
+ ],
671
+ "dev": true,
672
+ "license": "MIT",
673
+ "optional": true,
674
+ "os": [
675
+ "netbsd"
676
+ ],
677
+ "engines": {
678
+ "node": ">=18"
679
+ }
680
+ },
681
+ "node_modules/@esbuild/netbsd-x64": {
682
+ "version": "0.25.12",
683
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
684
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
685
+ "cpu": [
686
+ "x64"
687
+ ],
688
+ "dev": true,
689
+ "license": "MIT",
690
+ "optional": true,
691
+ "os": [
692
+ "netbsd"
693
+ ],
694
+ "engines": {
695
+ "node": ">=18"
696
+ }
697
+ },
698
+ "node_modules/@esbuild/openbsd-arm64": {
699
+ "version": "0.25.12",
700
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
701
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
702
+ "cpu": [
703
+ "arm64"
704
+ ],
705
+ "dev": true,
706
+ "license": "MIT",
707
+ "optional": true,
708
+ "os": [
709
+ "openbsd"
710
+ ],
711
+ "engines": {
712
+ "node": ">=18"
713
+ }
714
+ },
715
+ "node_modules/@esbuild/openbsd-x64": {
716
+ "version": "0.25.12",
717
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
718
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
719
+ "cpu": [
720
+ "x64"
721
+ ],
722
+ "dev": true,
723
+ "license": "MIT",
724
+ "optional": true,
725
+ "os": [
726
+ "openbsd"
727
+ ],
728
+ "engines": {
729
+ "node": ">=18"
730
+ }
731
+ },
732
+ "node_modules/@esbuild/openharmony-arm64": {
733
+ "version": "0.25.12",
734
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
735
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
736
+ "cpu": [
737
+ "arm64"
738
+ ],
739
+ "dev": true,
740
+ "license": "MIT",
741
+ "optional": true,
742
+ "os": [
743
+ "openharmony"
744
+ ],
745
+ "engines": {
746
+ "node": ">=18"
747
+ }
748
+ },
749
+ "node_modules/@esbuild/sunos-x64": {
750
+ "version": "0.25.12",
751
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
752
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
753
+ "cpu": [
754
+ "x64"
755
+ ],
756
+ "dev": true,
757
+ "license": "MIT",
758
+ "optional": true,
759
+ "os": [
760
+ "sunos"
761
+ ],
762
+ "engines": {
763
+ "node": ">=18"
764
+ }
765
+ },
766
+ "node_modules/@esbuild/win32-arm64": {
767
+ "version": "0.25.12",
768
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
769
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
770
+ "cpu": [
771
+ "arm64"
772
+ ],
773
+ "dev": true,
774
+ "license": "MIT",
775
+ "optional": true,
776
+ "os": [
777
+ "win32"
778
+ ],
779
+ "engines": {
780
+ "node": ">=18"
781
+ }
782
+ },
783
+ "node_modules/@esbuild/win32-ia32": {
784
+ "version": "0.25.12",
785
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
786
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
787
+ "cpu": [
788
+ "ia32"
789
+ ],
790
+ "dev": true,
791
+ "license": "MIT",
792
+ "optional": true,
793
+ "os": [
794
+ "win32"
795
+ ],
796
+ "engines": {
797
+ "node": ">=18"
798
+ }
799
+ },
800
+ "node_modules/@esbuild/win32-x64": {
801
+ "version": "0.25.12",
802
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
803
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
804
+ "cpu": [
805
+ "x64"
806
+ ],
807
+ "dev": true,
808
+ "license": "MIT",
809
+ "optional": true,
810
+ "os": [
811
+ "win32"
812
+ ],
813
+ "engines": {
814
+ "node": ">=18"
815
+ }
816
+ },
817
+ "node_modules/@huggingface/hub": {
818
+ "version": "2.11.0",
819
+ "resolved": "https://registry.npmjs.org/@huggingface/hub/-/hub-2.11.0.tgz",
820
+ "integrity": "sha512-WS6QGaXYeBVFlaB4SOn6z4LGUpLB5kRZNL08uUni4izX353KxiwwZMK5+/AWX86MJh8SMZNa/JFcvFCcQsbszQ==",
821
+ "license": "MIT",
822
+ "dependencies": {
823
+ "@huggingface/tasks": "^0.19.90"
824
+ },
825
+ "bin": {
826
+ "hfjs": "dist/cli.js"
827
+ },
828
+ "engines": {
829
+ "node": ">=18"
830
+ },
831
+ "optionalDependencies": {
832
+ "cli-progress": "^3.12.0"
833
+ }
834
+ },
835
+ "node_modules/@huggingface/tasks": {
836
+ "version": "0.19.90",
837
+ "resolved": "https://registry.npmjs.org/@huggingface/tasks/-/tasks-0.19.90.tgz",
838
+ "integrity": "sha512-nfV9luJbvwGQ/5oKXkKhCV9h4X7mwh1YaGG3ORd6UMLDSwr1OFSSatcBX0O9OtBtmNK19aGSjbLFqqgcIR6+IA==",
839
+ "license": "MIT"
840
+ },
841
+ "node_modules/@jridgewell/gen-mapping": {
842
+ "version": "0.3.13",
843
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
844
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
845
+ "dev": true,
846
+ "license": "MIT",
847
+ "dependencies": {
848
+ "@jridgewell/sourcemap-codec": "^1.5.0",
849
+ "@jridgewell/trace-mapping": "^0.3.24"
850
+ }
851
+ },
852
+ "node_modules/@jridgewell/remapping": {
853
+ "version": "2.3.5",
854
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
855
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
856
+ "dev": true,
857
+ "license": "MIT",
858
+ "dependencies": {
859
+ "@jridgewell/gen-mapping": "^0.3.5",
860
+ "@jridgewell/trace-mapping": "^0.3.24"
861
+ }
862
+ },
863
+ "node_modules/@jridgewell/resolve-uri": {
864
+ "version": "3.1.2",
865
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
866
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
867
+ "dev": true,
868
+ "license": "MIT",
869
+ "engines": {
870
+ "node": ">=6.0.0"
871
+ }
872
+ },
873
+ "node_modules/@jridgewell/sourcemap-codec": {
874
+ "version": "1.5.5",
875
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
876
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
877
+ "dev": true,
878
+ "license": "MIT"
879
+ },
880
+ "node_modules/@jridgewell/trace-mapping": {
881
+ "version": "0.3.31",
882
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
883
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
884
+ "dev": true,
885
+ "license": "MIT",
886
+ "dependencies": {
887
+ "@jridgewell/resolve-uri": "^3.1.0",
888
+ "@jridgewell/sourcemap-codec": "^1.4.14"
889
+ }
890
+ },
891
+ "node_modules/@monaco-editor/loader": {
892
+ "version": "1.7.0",
893
+ "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz",
894
+ "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==",
895
+ "license": "MIT",
896
+ "dependencies": {
897
+ "state-local": "^1.0.6"
898
+ }
899
+ },
900
+ "node_modules/@monaco-editor/react": {
901
+ "version": "4.7.0",
902
+ "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz",
903
+ "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==",
904
+ "license": "MIT",
905
+ "dependencies": {
906
+ "@monaco-editor/loader": "^1.5.0"
907
+ },
908
+ "peerDependencies": {
909
+ "monaco-editor": ">= 0.25.0 < 1",
910
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
911
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
912
+ }
913
+ },
914
+ "node_modules/@opentelemetry/api": {
915
+ "version": "1.9.0",
916
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
917
+ "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
918
+ "license": "Apache-2.0",
919
+ "engines": {
920
+ "node": ">=8.0.0"
921
+ }
922
+ },
923
+ "node_modules/@rolldown/pluginutils": {
924
+ "version": "1.0.0-beta.27",
925
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
926
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
927
+ "dev": true,
928
+ "license": "MIT"
929
+ },
930
+ "node_modules/@rollup/rollup-android-arm-eabi": {
931
+ "version": "4.60.2",
932
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz",
933
+ "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==",
934
+ "cpu": [
935
+ "arm"
936
+ ],
937
+ "dev": true,
938
+ "license": "MIT",
939
+ "optional": true,
940
+ "os": [
941
+ "android"
942
+ ]
943
+ },
944
+ "node_modules/@rollup/rollup-android-arm64": {
945
+ "version": "4.60.2",
946
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz",
947
+ "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==",
948
+ "cpu": [
949
+ "arm64"
950
+ ],
951
+ "dev": true,
952
+ "license": "MIT",
953
+ "optional": true,
954
+ "os": [
955
+ "android"
956
+ ]
957
+ },
958
+ "node_modules/@rollup/rollup-darwin-arm64": {
959
+ "version": "4.60.2",
960
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz",
961
+ "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==",
962
+ "cpu": [
963
+ "arm64"
964
+ ],
965
+ "dev": true,
966
+ "license": "MIT",
967
+ "optional": true,
968
+ "os": [
969
+ "darwin"
970
+ ]
971
+ },
972
+ "node_modules/@rollup/rollup-darwin-x64": {
973
+ "version": "4.60.2",
974
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz",
975
+ "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==",
976
+ "cpu": [
977
+ "x64"
978
+ ],
979
+ "dev": true,
980
+ "license": "MIT",
981
+ "optional": true,
982
+ "os": [
983
+ "darwin"
984
+ ]
985
+ },
986
+ "node_modules/@rollup/rollup-freebsd-arm64": {
987
+ "version": "4.60.2",
988
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz",
989
+ "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==",
990
+ "cpu": [
991
+ "arm64"
992
+ ],
993
+ "dev": true,
994
+ "license": "MIT",
995
+ "optional": true,
996
+ "os": [
997
+ "freebsd"
998
+ ]
999
+ },
1000
+ "node_modules/@rollup/rollup-freebsd-x64": {
1001
+ "version": "4.60.2",
1002
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz",
1003
+ "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==",
1004
+ "cpu": [
1005
+ "x64"
1006
+ ],
1007
+ "dev": true,
1008
+ "license": "MIT",
1009
+ "optional": true,
1010
+ "os": [
1011
+ "freebsd"
1012
+ ]
1013
+ },
1014
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
1015
+ "version": "4.60.2",
1016
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz",
1017
+ "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==",
1018
+ "cpu": [
1019
+ "arm"
1020
+ ],
1021
+ "dev": true,
1022
+ "license": "MIT",
1023
+ "optional": true,
1024
+ "os": [
1025
+ "linux"
1026
+ ]
1027
+ },
1028
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
1029
+ "version": "4.60.2",
1030
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz",
1031
+ "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==",
1032
+ "cpu": [
1033
+ "arm"
1034
+ ],
1035
+ "dev": true,
1036
+ "license": "MIT",
1037
+ "optional": true,
1038
+ "os": [
1039
+ "linux"
1040
+ ]
1041
+ },
1042
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
1043
+ "version": "4.60.2",
1044
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz",
1045
+ "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==",
1046
+ "cpu": [
1047
+ "arm64"
1048
+ ],
1049
+ "dev": true,
1050
+ "license": "MIT",
1051
+ "optional": true,
1052
+ "os": [
1053
+ "linux"
1054
+ ]
1055
+ },
1056
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
1057
+ "version": "4.60.2",
1058
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz",
1059
+ "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==",
1060
+ "cpu": [
1061
+ "arm64"
1062
+ ],
1063
+ "dev": true,
1064
+ "license": "MIT",
1065
+ "optional": true,
1066
+ "os": [
1067
+ "linux"
1068
+ ]
1069
+ },
1070
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
1071
+ "version": "4.60.2",
1072
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz",
1073
+ "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==",
1074
+ "cpu": [
1075
+ "loong64"
1076
+ ],
1077
+ "dev": true,
1078
+ "license": "MIT",
1079
+ "optional": true,
1080
+ "os": [
1081
+ "linux"
1082
+ ]
1083
+ },
1084
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
1085
+ "version": "4.60.2",
1086
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz",
1087
+ "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==",
1088
+ "cpu": [
1089
+ "loong64"
1090
+ ],
1091
+ "dev": true,
1092
+ "license": "MIT",
1093
+ "optional": true,
1094
+ "os": [
1095
+ "linux"
1096
+ ]
1097
+ },
1098
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
1099
+ "version": "4.60.2",
1100
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz",
1101
+ "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==",
1102
+ "cpu": [
1103
+ "ppc64"
1104
+ ],
1105
+ "dev": true,
1106
+ "license": "MIT",
1107
+ "optional": true,
1108
+ "os": [
1109
+ "linux"
1110
+ ]
1111
+ },
1112
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
1113
+ "version": "4.60.2",
1114
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz",
1115
+ "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==",
1116
+ "cpu": [
1117
+ "ppc64"
1118
+ ],
1119
+ "dev": true,
1120
+ "license": "MIT",
1121
+ "optional": true,
1122
+ "os": [
1123
+ "linux"
1124
+ ]
1125
+ },
1126
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
1127
+ "version": "4.60.2",
1128
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz",
1129
+ "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==",
1130
+ "cpu": [
1131
+ "riscv64"
1132
+ ],
1133
+ "dev": true,
1134
+ "license": "MIT",
1135
+ "optional": true,
1136
+ "os": [
1137
+ "linux"
1138
+ ]
1139
+ },
1140
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
1141
+ "version": "4.60.2",
1142
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz",
1143
+ "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==",
1144
+ "cpu": [
1145
+ "riscv64"
1146
+ ],
1147
+ "dev": true,
1148
+ "license": "MIT",
1149
+ "optional": true,
1150
+ "os": [
1151
+ "linux"
1152
+ ]
1153
+ },
1154
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
1155
+ "version": "4.60.2",
1156
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz",
1157
+ "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==",
1158
+ "cpu": [
1159
+ "s390x"
1160
+ ],
1161
+ "dev": true,
1162
+ "license": "MIT",
1163
+ "optional": true,
1164
+ "os": [
1165
+ "linux"
1166
+ ]
1167
+ },
1168
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
1169
+ "version": "4.60.2",
1170
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz",
1171
+ "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==",
1172
+ "cpu": [
1173
+ "x64"
1174
+ ],
1175
+ "dev": true,
1176
+ "license": "MIT",
1177
+ "optional": true,
1178
+ "os": [
1179
+ "linux"
1180
+ ]
1181
+ },
1182
+ "node_modules/@rollup/rollup-linux-x64-musl": {
1183
+ "version": "4.60.2",
1184
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz",
1185
+ "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==",
1186
+ "cpu": [
1187
+ "x64"
1188
+ ],
1189
+ "dev": true,
1190
+ "license": "MIT",
1191
+ "optional": true,
1192
+ "os": [
1193
+ "linux"
1194
+ ]
1195
+ },
1196
+ "node_modules/@rollup/rollup-openbsd-x64": {
1197
+ "version": "4.60.2",
1198
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz",
1199
+ "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==",
1200
+ "cpu": [
1201
+ "x64"
1202
+ ],
1203
+ "dev": true,
1204
+ "license": "MIT",
1205
+ "optional": true,
1206
+ "os": [
1207
+ "openbsd"
1208
+ ]
1209
+ },
1210
+ "node_modules/@rollup/rollup-openharmony-arm64": {
1211
+ "version": "4.60.2",
1212
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz",
1213
+ "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==",
1214
+ "cpu": [
1215
+ "arm64"
1216
+ ],
1217
+ "dev": true,
1218
+ "license": "MIT",
1219
+ "optional": true,
1220
+ "os": [
1221
+ "openharmony"
1222
+ ]
1223
+ },
1224
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
1225
+ "version": "4.60.2",
1226
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz",
1227
+ "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==",
1228
+ "cpu": [
1229
+ "arm64"
1230
+ ],
1231
+ "dev": true,
1232
+ "license": "MIT",
1233
+ "optional": true,
1234
+ "os": [
1235
+ "win32"
1236
+ ]
1237
+ },
1238
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
1239
+ "version": "4.60.2",
1240
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz",
1241
+ "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==",
1242
+ "cpu": [
1243
+ "ia32"
1244
+ ],
1245
+ "dev": true,
1246
+ "license": "MIT",
1247
+ "optional": true,
1248
+ "os": [
1249
+ "win32"
1250
+ ]
1251
+ },
1252
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
1253
+ "version": "4.60.2",
1254
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz",
1255
+ "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==",
1256
+ "cpu": [
1257
+ "x64"
1258
+ ],
1259
+ "dev": true,
1260
+ "license": "MIT",
1261
+ "optional": true,
1262
+ "os": [
1263
+ "win32"
1264
+ ]
1265
+ },
1266
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
1267
+ "version": "4.60.2",
1268
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz",
1269
+ "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==",
1270
+ "cpu": [
1271
+ "x64"
1272
+ ],
1273
+ "dev": true,
1274
+ "license": "MIT",
1275
+ "optional": true,
1276
+ "os": [
1277
+ "win32"
1278
+ ]
1279
+ },
1280
+ "node_modules/@standard-schema/spec": {
1281
+ "version": "1.1.0",
1282
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
1283
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
1284
+ "license": "MIT"
1285
+ },
1286
+ "node_modules/@types/babel__core": {
1287
+ "version": "7.20.5",
1288
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
1289
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
1290
+ "dev": true,
1291
+ "license": "MIT",
1292
+ "dependencies": {
1293
+ "@babel/parser": "^7.20.7",
1294
+ "@babel/types": "^7.20.7",
1295
+ "@types/babel__generator": "*",
1296
+ "@types/babel__template": "*",
1297
+ "@types/babel__traverse": "*"
1298
+ }
1299
+ },
1300
+ "node_modules/@types/babel__generator": {
1301
+ "version": "7.27.0",
1302
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
1303
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
1304
+ "dev": true,
1305
+ "license": "MIT",
1306
+ "dependencies": {
1307
+ "@babel/types": "^7.0.0"
1308
+ }
1309
+ },
1310
+ "node_modules/@types/babel__template": {
1311
+ "version": "7.4.4",
1312
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
1313
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
1314
+ "dev": true,
1315
+ "license": "MIT",
1316
+ "dependencies": {
1317
+ "@babel/parser": "^7.1.0",
1318
+ "@babel/types": "^7.0.0"
1319
+ }
1320
+ },
1321
+ "node_modules/@types/babel__traverse": {
1322
+ "version": "7.28.0",
1323
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
1324
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
1325
+ "dev": true,
1326
+ "license": "MIT",
1327
+ "dependencies": {
1328
+ "@babel/types": "^7.28.2"
1329
+ }
1330
+ },
1331
+ "node_modules/@types/estree": {
1332
+ "version": "1.0.8",
1333
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
1334
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
1335
+ "dev": true,
1336
+ "license": "MIT"
1337
+ },
1338
+ "node_modules/@types/prop-types": {
1339
+ "version": "15.7.15",
1340
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
1341
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
1342
+ "dev": true,
1343
+ "license": "MIT"
1344
+ },
1345
+ "node_modules/@types/react": {
1346
+ "version": "18.3.28",
1347
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
1348
+ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
1349
+ "dev": true,
1350
+ "license": "MIT",
1351
+ "peer": true,
1352
+ "dependencies": {
1353
+ "@types/prop-types": "*",
1354
+ "csstype": "^3.2.2"
1355
+ }
1356
+ },
1357
+ "node_modules/@types/react-dom": {
1358
+ "version": "18.3.7",
1359
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
1360
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
1361
+ "dev": true,
1362
+ "license": "MIT",
1363
+ "peerDependencies": {
1364
+ "@types/react": "^18.0.0"
1365
+ }
1366
+ },
1367
+ "node_modules/@types/trusted-types": {
1368
+ "version": "2.0.7",
1369
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
1370
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
1371
+ "license": "MIT",
1372
+ "optional": true
1373
+ },
1374
+ "node_modules/@vercel/oidc": {
1375
+ "version": "3.2.0",
1376
+ "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.2.0.tgz",
1377
+ "integrity": "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==",
1378
+ "license": "Apache-2.0",
1379
+ "engines": {
1380
+ "node": ">= 20"
1381
+ }
1382
+ },
1383
+ "node_modules/@vitejs/plugin-react": {
1384
+ "version": "4.7.0",
1385
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
1386
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
1387
+ "dev": true,
1388
+ "license": "MIT",
1389
+ "dependencies": {
1390
+ "@babel/core": "^7.28.0",
1391
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
1392
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
1393
+ "@rolldown/pluginutils": "1.0.0-beta.27",
1394
+ "@types/babel__core": "^7.20.5",
1395
+ "react-refresh": "^0.17.0"
1396
+ },
1397
+ "engines": {
1398
+ "node": "^14.18.0 || >=16.0.0"
1399
+ },
1400
+ "peerDependencies": {
1401
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
1402
+ }
1403
+ },
1404
+ "node_modules/ai": {
1405
+ "version": "6.0.168",
1406
+ "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.168.tgz",
1407
+ "integrity": "sha512-2HqCJuO+1V2aV7vfYs5LFEUfxbkGX+5oa54q/gCCTL7KLTdbxcCu5D7TdLA5kwsrs3Szgjah9q6D9tpjHM3hUQ==",
1408
+ "license": "Apache-2.0",
1409
+ "dependencies": {
1410
+ "@ai-sdk/gateway": "3.0.104",
1411
+ "@ai-sdk/provider": "3.0.8",
1412
+ "@ai-sdk/provider-utils": "4.0.23",
1413
+ "@opentelemetry/api": "1.9.0"
1414
+ },
1415
+ "engines": {
1416
+ "node": ">=18"
1417
+ },
1418
+ "peerDependencies": {
1419
+ "zod": "^3.25.76 || ^4.1.8"
1420
+ }
1421
+ },
1422
+ "node_modules/ansi-regex": {
1423
+ "version": "5.0.1",
1424
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
1425
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
1426
+ "license": "MIT",
1427
+ "optional": true,
1428
+ "engines": {
1429
+ "node": ">=8"
1430
+ }
1431
+ },
1432
+ "node_modules/baseline-browser-mapping": {
1433
+ "version": "2.10.20",
1434
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.20.tgz",
1435
+ "integrity": "sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==",
1436
+ "dev": true,
1437
+ "license": "Apache-2.0",
1438
+ "bin": {
1439
+ "baseline-browser-mapping": "dist/cli.cjs"
1440
+ },
1441
+ "engines": {
1442
+ "node": ">=6.0.0"
1443
+ }
1444
+ },
1445
+ "node_modules/browserslist": {
1446
+ "version": "4.28.2",
1447
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
1448
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
1449
+ "dev": true,
1450
+ "funding": [
1451
+ {
1452
+ "type": "opencollective",
1453
+ "url": "https://opencollective.com/browserslist"
1454
+ },
1455
+ {
1456
+ "type": "tidelift",
1457
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1458
+ },
1459
+ {
1460
+ "type": "github",
1461
+ "url": "https://github.com/sponsors/ai"
1462
+ }
1463
+ ],
1464
+ "license": "MIT",
1465
+ "peer": true,
1466
+ "dependencies": {
1467
+ "baseline-browser-mapping": "^2.10.12",
1468
+ "caniuse-lite": "^1.0.30001782",
1469
+ "electron-to-chromium": "^1.5.328",
1470
+ "node-releases": "^2.0.36",
1471
+ "update-browserslist-db": "^1.2.3"
1472
+ },
1473
+ "bin": {
1474
+ "browserslist": "cli.js"
1475
+ },
1476
+ "engines": {
1477
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
1478
+ }
1479
+ },
1480
+ "node_modules/caniuse-lite": {
1481
+ "version": "1.0.30001790",
1482
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001790.tgz",
1483
+ "integrity": "sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==",
1484
+ "dev": true,
1485
+ "funding": [
1486
+ {
1487
+ "type": "opencollective",
1488
+ "url": "https://opencollective.com/browserslist"
1489
+ },
1490
+ {
1491
+ "type": "tidelift",
1492
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
1493
+ },
1494
+ {
1495
+ "type": "github",
1496
+ "url": "https://github.com/sponsors/ai"
1497
+ }
1498
+ ],
1499
+ "license": "CC-BY-4.0"
1500
+ },
1501
+ "node_modules/cli-progress": {
1502
+ "version": "3.12.0",
1503
+ "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz",
1504
+ "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==",
1505
+ "license": "MIT",
1506
+ "optional": true,
1507
+ "dependencies": {
1508
+ "string-width": "^4.2.3"
1509
+ },
1510
+ "engines": {
1511
+ "node": ">=4"
1512
+ }
1513
+ },
1514
+ "node_modules/clsx": {
1515
+ "version": "2.1.1",
1516
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
1517
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
1518
+ "license": "MIT",
1519
+ "engines": {
1520
+ "node": ">=6"
1521
+ }
1522
+ },
1523
+ "node_modules/convert-source-map": {
1524
+ "version": "2.0.0",
1525
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
1526
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
1527
+ "dev": true,
1528
+ "license": "MIT"
1529
+ },
1530
+ "node_modules/csstype": {
1531
+ "version": "3.2.3",
1532
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
1533
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
1534
+ "dev": true,
1535
+ "license": "MIT"
1536
+ },
1537
+ "node_modules/debug": {
1538
+ "version": "4.4.3",
1539
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
1540
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1541
+ "dev": true,
1542
+ "license": "MIT",
1543
+ "dependencies": {
1544
+ "ms": "^2.1.3"
1545
+ },
1546
+ "engines": {
1547
+ "node": ">=6.0"
1548
+ },
1549
+ "peerDependenciesMeta": {
1550
+ "supports-color": {
1551
+ "optional": true
1552
+ }
1553
+ }
1554
+ },
1555
+ "node_modules/dequal": {
1556
+ "version": "2.0.3",
1557
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
1558
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
1559
+ "license": "MIT",
1560
+ "engines": {
1561
+ "node": ">=6"
1562
+ }
1563
+ },
1564
+ "node_modules/dompurify": {
1565
+ "version": "3.2.7",
1566
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
1567
+ "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
1568
+ "license": "(MPL-2.0 OR Apache-2.0)",
1569
+ "optionalDependencies": {
1570
+ "@types/trusted-types": "^2.0.7"
1571
+ }
1572
+ },
1573
+ "node_modules/electron-to-chromium": {
1574
+ "version": "1.5.343",
1575
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.343.tgz",
1576
+ "integrity": "sha512-YHnQ3MXI08icvL9ZKnEBy05F2EQ8ob01UaMOuMbM8l+4UcAq6MPPbBTJBbsBUg3H8JeZNt+O4fjsoWth3p6IFg==",
1577
+ "dev": true,
1578
+ "license": "ISC"
1579
+ },
1580
+ "node_modules/emoji-regex": {
1581
+ "version": "8.0.0",
1582
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
1583
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
1584
+ "license": "MIT",
1585
+ "optional": true
1586
+ },
1587
+ "node_modules/esbuild": {
1588
+ "version": "0.25.12",
1589
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
1590
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
1591
+ "dev": true,
1592
+ "hasInstallScript": true,
1593
+ "license": "MIT",
1594
+ "bin": {
1595
+ "esbuild": "bin/esbuild"
1596
+ },
1597
+ "engines": {
1598
+ "node": ">=18"
1599
+ },
1600
+ "optionalDependencies": {
1601
+ "@esbuild/aix-ppc64": "0.25.12",
1602
+ "@esbuild/android-arm": "0.25.12",
1603
+ "@esbuild/android-arm64": "0.25.12",
1604
+ "@esbuild/android-x64": "0.25.12",
1605
+ "@esbuild/darwin-arm64": "0.25.12",
1606
+ "@esbuild/darwin-x64": "0.25.12",
1607
+ "@esbuild/freebsd-arm64": "0.25.12",
1608
+ "@esbuild/freebsd-x64": "0.25.12",
1609
+ "@esbuild/linux-arm": "0.25.12",
1610
+ "@esbuild/linux-arm64": "0.25.12",
1611
+ "@esbuild/linux-ia32": "0.25.12",
1612
+ "@esbuild/linux-loong64": "0.25.12",
1613
+ "@esbuild/linux-mips64el": "0.25.12",
1614
+ "@esbuild/linux-ppc64": "0.25.12",
1615
+ "@esbuild/linux-riscv64": "0.25.12",
1616
+ "@esbuild/linux-s390x": "0.25.12",
1617
+ "@esbuild/linux-x64": "0.25.12",
1618
+ "@esbuild/netbsd-arm64": "0.25.12",
1619
+ "@esbuild/netbsd-x64": "0.25.12",
1620
+ "@esbuild/openbsd-arm64": "0.25.12",
1621
+ "@esbuild/openbsd-x64": "0.25.12",
1622
+ "@esbuild/openharmony-arm64": "0.25.12",
1623
+ "@esbuild/sunos-x64": "0.25.12",
1624
+ "@esbuild/win32-arm64": "0.25.12",
1625
+ "@esbuild/win32-ia32": "0.25.12",
1626
+ "@esbuild/win32-x64": "0.25.12"
1627
+ }
1628
+ },
1629
+ "node_modules/escalade": {
1630
+ "version": "3.2.0",
1631
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
1632
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
1633
+ "dev": true,
1634
+ "license": "MIT",
1635
+ "engines": {
1636
+ "node": ">=6"
1637
+ }
1638
+ },
1639
+ "node_modules/eventsource-parser": {
1640
+ "version": "3.0.8",
1641
+ "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz",
1642
+ "integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==",
1643
+ "license": "MIT",
1644
+ "engines": {
1645
+ "node": ">=18.0.0"
1646
+ }
1647
+ },
1648
+ "node_modules/fdir": {
1649
+ "version": "6.5.0",
1650
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
1651
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
1652
+ "dev": true,
1653
+ "license": "MIT",
1654
+ "engines": {
1655
+ "node": ">=12.0.0"
1656
+ },
1657
+ "peerDependencies": {
1658
+ "picomatch": "^3 || ^4"
1659
+ },
1660
+ "peerDependenciesMeta": {
1661
+ "picomatch": {
1662
+ "optional": true
1663
+ }
1664
+ }
1665
+ },
1666
+ "node_modules/fsevents": {
1667
+ "version": "2.3.3",
1668
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1669
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1670
+ "dev": true,
1671
+ "hasInstallScript": true,
1672
+ "license": "MIT",
1673
+ "optional": true,
1674
+ "os": [
1675
+ "darwin"
1676
+ ],
1677
+ "engines": {
1678
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1679
+ }
1680
+ },
1681
+ "node_modules/gensync": {
1682
+ "version": "1.0.0-beta.2",
1683
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
1684
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
1685
+ "dev": true,
1686
+ "license": "MIT",
1687
+ "engines": {
1688
+ "node": ">=6.9.0"
1689
+ }
1690
+ },
1691
+ "node_modules/is-fullwidth-code-point": {
1692
+ "version": "3.0.0",
1693
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
1694
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
1695
+ "license": "MIT",
1696
+ "optional": true,
1697
+ "engines": {
1698
+ "node": ">=8"
1699
+ }
1700
+ },
1701
+ "node_modules/js-tokens": {
1702
+ "version": "4.0.0",
1703
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
1704
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
1705
+ "license": "MIT"
1706
+ },
1707
+ "node_modules/jsesc": {
1708
+ "version": "3.1.0",
1709
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
1710
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
1711
+ "dev": true,
1712
+ "license": "MIT",
1713
+ "bin": {
1714
+ "jsesc": "bin/jsesc"
1715
+ },
1716
+ "engines": {
1717
+ "node": ">=6"
1718
+ }
1719
+ },
1720
+ "node_modules/json-schema": {
1721
+ "version": "0.4.0",
1722
+ "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
1723
+ "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
1724
+ "license": "(AFL-2.1 OR BSD-3-Clause)"
1725
+ },
1726
+ "node_modules/json5": {
1727
+ "version": "2.2.3",
1728
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
1729
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
1730
+ "dev": true,
1731
+ "license": "MIT",
1732
+ "bin": {
1733
+ "json5": "lib/cli.js"
1734
+ },
1735
+ "engines": {
1736
+ "node": ">=6"
1737
+ }
1738
+ },
1739
+ "node_modules/loose-envify": {
1740
+ "version": "1.4.0",
1741
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
1742
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
1743
+ "license": "MIT",
1744
+ "dependencies": {
1745
+ "js-tokens": "^3.0.0 || ^4.0.0"
1746
+ },
1747
+ "bin": {
1748
+ "loose-envify": "cli.js"
1749
+ }
1750
+ },
1751
+ "node_modules/lru-cache": {
1752
+ "version": "5.1.1",
1753
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
1754
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
1755
+ "dev": true,
1756
+ "license": "ISC",
1757
+ "dependencies": {
1758
+ "yallist": "^3.0.2"
1759
+ }
1760
+ },
1761
+ "node_modules/marked": {
1762
+ "version": "14.0.0",
1763
+ "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
1764
+ "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
1765
+ "license": "MIT",
1766
+ "bin": {
1767
+ "marked": "bin/marked.js"
1768
+ },
1769
+ "engines": {
1770
+ "node": ">= 18"
1771
+ }
1772
+ },
1773
+ "node_modules/monaco-editor": {
1774
+ "version": "0.55.1",
1775
+ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
1776
+ "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
1777
+ "license": "MIT",
1778
+ "peer": true,
1779
+ "dependencies": {
1780
+ "dompurify": "3.2.7",
1781
+ "marked": "14.0.0"
1782
+ }
1783
+ },
1784
+ "node_modules/ms": {
1785
+ "version": "2.1.3",
1786
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1787
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1788
+ "dev": true,
1789
+ "license": "MIT"
1790
+ },
1791
+ "node_modules/nanoid": {
1792
+ "version": "5.1.9",
1793
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.9.tgz",
1794
+ "integrity": "sha512-ZUvP7KeBLe3OZ1ypw6dI/TzYJuvHP77IM4Ry73waSQTLn8/g8rpdjfyVAh7t1/+FjBtG4lCP42MEbDxOsRpBMw==",
1795
+ "funding": [
1796
+ {
1797
+ "type": "github",
1798
+ "url": "https://github.com/sponsors/ai"
1799
+ }
1800
+ ],
1801
+ "license": "MIT",
1802
+ "bin": {
1803
+ "nanoid": "bin/nanoid.js"
1804
+ },
1805
+ "engines": {
1806
+ "node": "^18 || >=20"
1807
+ }
1808
+ },
1809
+ "node_modules/node-releases": {
1810
+ "version": "2.0.38",
1811
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz",
1812
+ "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==",
1813
+ "dev": true,
1814
+ "license": "MIT"
1815
+ },
1816
+ "node_modules/picocolors": {
1817
+ "version": "1.1.1",
1818
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1819
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1820
+ "dev": true,
1821
+ "license": "ISC"
1822
+ },
1823
+ "node_modules/picomatch": {
1824
+ "version": "4.0.4",
1825
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
1826
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
1827
+ "dev": true,
1828
+ "license": "MIT",
1829
+ "peer": true,
1830
+ "engines": {
1831
+ "node": ">=12"
1832
+ },
1833
+ "funding": {
1834
+ "url": "https://github.com/sponsors/jonschlinkert"
1835
+ }
1836
+ },
1837
+ "node_modules/postcss": {
1838
+ "version": "8.5.10",
1839
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
1840
+ "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
1841
+ "dev": true,
1842
+ "funding": [
1843
+ {
1844
+ "type": "opencollective",
1845
+ "url": "https://opencollective.com/postcss/"
1846
+ },
1847
+ {
1848
+ "type": "tidelift",
1849
+ "url": "https://tidelift.com/funding/github/npm/postcss"
1850
+ },
1851
+ {
1852
+ "type": "github",
1853
+ "url": "https://github.com/sponsors/ai"
1854
+ }
1855
+ ],
1856
+ "license": "MIT",
1857
+ "dependencies": {
1858
+ "nanoid": "^3.3.11",
1859
+ "picocolors": "^1.1.1",
1860
+ "source-map-js": "^1.2.1"
1861
+ },
1862
+ "engines": {
1863
+ "node": "^10 || ^12 || >=14"
1864
+ }
1865
+ },
1866
+ "node_modules/postcss/node_modules/nanoid": {
1867
+ "version": "3.3.11",
1868
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
1869
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
1870
+ "dev": true,
1871
+ "funding": [
1872
+ {
1873
+ "type": "github",
1874
+ "url": "https://github.com/sponsors/ai"
1875
+ }
1876
+ ],
1877
+ "license": "MIT",
1878
+ "bin": {
1879
+ "nanoid": "bin/nanoid.cjs"
1880
+ },
1881
+ "engines": {
1882
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1883
+ }
1884
+ },
1885
+ "node_modules/react": {
1886
+ "version": "18.3.1",
1887
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
1888
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
1889
+ "license": "MIT",
1890
+ "peer": true,
1891
+ "dependencies": {
1892
+ "loose-envify": "^1.1.0"
1893
+ },
1894
+ "engines": {
1895
+ "node": ">=0.10.0"
1896
+ }
1897
+ },
1898
+ "node_modules/react-dom": {
1899
+ "version": "18.3.1",
1900
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
1901
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
1902
+ "license": "MIT",
1903
+ "peer": true,
1904
+ "dependencies": {
1905
+ "loose-envify": "^1.1.0",
1906
+ "scheduler": "^0.23.2"
1907
+ },
1908
+ "peerDependencies": {
1909
+ "react": "^18.3.1"
1910
+ }
1911
+ },
1912
+ "node_modules/react-refresh": {
1913
+ "version": "0.17.0",
1914
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
1915
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
1916
+ "dev": true,
1917
+ "license": "MIT",
1918
+ "engines": {
1919
+ "node": ">=0.10.0"
1920
+ }
1921
+ },
1922
+ "node_modules/rollup": {
1923
+ "version": "4.60.2",
1924
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz",
1925
+ "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==",
1926
+ "dev": true,
1927
+ "license": "MIT",
1928
+ "dependencies": {
1929
+ "@types/estree": "1.0.8"
1930
+ },
1931
+ "bin": {
1932
+ "rollup": "dist/bin/rollup"
1933
+ },
1934
+ "engines": {
1935
+ "node": ">=18.0.0",
1936
+ "npm": ">=8.0.0"
1937
+ },
1938
+ "optionalDependencies": {
1939
+ "@rollup/rollup-android-arm-eabi": "4.60.2",
1940
+ "@rollup/rollup-android-arm64": "4.60.2",
1941
+ "@rollup/rollup-darwin-arm64": "4.60.2",
1942
+ "@rollup/rollup-darwin-x64": "4.60.2",
1943
+ "@rollup/rollup-freebsd-arm64": "4.60.2",
1944
+ "@rollup/rollup-freebsd-x64": "4.60.2",
1945
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.2",
1946
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.2",
1947
+ "@rollup/rollup-linux-arm64-gnu": "4.60.2",
1948
+ "@rollup/rollup-linux-arm64-musl": "4.60.2",
1949
+ "@rollup/rollup-linux-loong64-gnu": "4.60.2",
1950
+ "@rollup/rollup-linux-loong64-musl": "4.60.2",
1951
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.2",
1952
+ "@rollup/rollup-linux-ppc64-musl": "4.60.2",
1953
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.2",
1954
+ "@rollup/rollup-linux-riscv64-musl": "4.60.2",
1955
+ "@rollup/rollup-linux-s390x-gnu": "4.60.2",
1956
+ "@rollup/rollup-linux-x64-gnu": "4.60.2",
1957
+ "@rollup/rollup-linux-x64-musl": "4.60.2",
1958
+ "@rollup/rollup-openbsd-x64": "4.60.2",
1959
+ "@rollup/rollup-openharmony-arm64": "4.60.2",
1960
+ "@rollup/rollup-win32-arm64-msvc": "4.60.2",
1961
+ "@rollup/rollup-win32-ia32-msvc": "4.60.2",
1962
+ "@rollup/rollup-win32-x64-gnu": "4.60.2",
1963
+ "@rollup/rollup-win32-x64-msvc": "4.60.2",
1964
+ "fsevents": "~2.3.2"
1965
+ }
1966
+ },
1967
+ "node_modules/scheduler": {
1968
+ "version": "0.23.2",
1969
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
1970
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
1971
+ "license": "MIT",
1972
+ "dependencies": {
1973
+ "loose-envify": "^1.1.0"
1974
+ }
1975
+ },
1976
+ "node_modules/semver": {
1977
+ "version": "6.3.1",
1978
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
1979
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
1980
+ "dev": true,
1981
+ "license": "ISC",
1982
+ "bin": {
1983
+ "semver": "bin/semver.js"
1984
+ }
1985
+ },
1986
+ "node_modules/source-map-js": {
1987
+ "version": "1.2.1",
1988
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
1989
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
1990
+ "dev": true,
1991
+ "license": "BSD-3-Clause",
1992
+ "engines": {
1993
+ "node": ">=0.10.0"
1994
+ }
1995
+ },
1996
+ "node_modules/state-local": {
1997
+ "version": "1.0.7",
1998
+ "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
1999
+ "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==",
2000
+ "license": "MIT"
2001
+ },
2002
+ "node_modules/string-width": {
2003
+ "version": "4.2.3",
2004
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
2005
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
2006
+ "license": "MIT",
2007
+ "optional": true,
2008
+ "dependencies": {
2009
+ "emoji-regex": "^8.0.0",
2010
+ "is-fullwidth-code-point": "^3.0.0",
2011
+ "strip-ansi": "^6.0.1"
2012
+ },
2013
+ "engines": {
2014
+ "node": ">=8"
2015
+ }
2016
+ },
2017
+ "node_modules/strip-ansi": {
2018
+ "version": "6.0.1",
2019
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
2020
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
2021
+ "license": "MIT",
2022
+ "optional": true,
2023
+ "dependencies": {
2024
+ "ansi-regex": "^5.0.1"
2025
+ },
2026
+ "engines": {
2027
+ "node": ">=8"
2028
+ }
2029
+ },
2030
+ "node_modules/swr": {
2031
+ "version": "2.4.1",
2032
+ "resolved": "https://registry.npmjs.org/swr/-/swr-2.4.1.tgz",
2033
+ "integrity": "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==",
2034
+ "license": "MIT",
2035
+ "dependencies": {
2036
+ "dequal": "^2.0.3",
2037
+ "use-sync-external-store": "^1.6.0"
2038
+ },
2039
+ "peerDependencies": {
2040
+ "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
2041
+ }
2042
+ },
2043
+ "node_modules/throttleit": {
2044
+ "version": "2.1.0",
2045
+ "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz",
2046
+ "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==",
2047
+ "license": "MIT",
2048
+ "engines": {
2049
+ "node": ">=18"
2050
+ },
2051
+ "funding": {
2052
+ "url": "https://github.com/sponsors/sindresorhus"
2053
+ }
2054
+ },
2055
+ "node_modules/tinyglobby": {
2056
+ "version": "0.2.16",
2057
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
2058
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
2059
+ "dev": true,
2060
+ "license": "MIT",
2061
+ "dependencies": {
2062
+ "fdir": "^6.5.0",
2063
+ "picomatch": "^4.0.4"
2064
+ },
2065
+ "engines": {
2066
+ "node": ">=12.0.0"
2067
+ },
2068
+ "funding": {
2069
+ "url": "https://github.com/sponsors/SuperchupuDev"
2070
+ }
2071
+ },
2072
+ "node_modules/typescript": {
2073
+ "version": "5.9.3",
2074
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
2075
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
2076
+ "dev": true,
2077
+ "license": "Apache-2.0",
2078
+ "bin": {
2079
+ "tsc": "bin/tsc",
2080
+ "tsserver": "bin/tsserver"
2081
+ },
2082
+ "engines": {
2083
+ "node": ">=14.17"
2084
+ }
2085
+ },
2086
+ "node_modules/update-browserslist-db": {
2087
+ "version": "1.2.3",
2088
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
2089
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
2090
+ "dev": true,
2091
+ "funding": [
2092
+ {
2093
+ "type": "opencollective",
2094
+ "url": "https://opencollective.com/browserslist"
2095
+ },
2096
+ {
2097
+ "type": "tidelift",
2098
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
2099
+ },
2100
+ {
2101
+ "type": "github",
2102
+ "url": "https://github.com/sponsors/ai"
2103
+ }
2104
+ ],
2105
+ "license": "MIT",
2106
+ "dependencies": {
2107
+ "escalade": "^3.2.0",
2108
+ "picocolors": "^1.1.1"
2109
+ },
2110
+ "bin": {
2111
+ "update-browserslist-db": "cli.js"
2112
+ },
2113
+ "peerDependencies": {
2114
+ "browserslist": ">= 4.21.0"
2115
+ }
2116
+ },
2117
+ "node_modules/use-sync-external-store": {
2118
+ "version": "1.6.0",
2119
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
2120
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
2121
+ "license": "MIT",
2122
+ "peerDependencies": {
2123
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
2124
+ }
2125
+ },
2126
+ "node_modules/vite": {
2127
+ "version": "6.4.2",
2128
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
2129
+ "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
2130
+ "dev": true,
2131
+ "license": "MIT",
2132
+ "peer": true,
2133
+ "dependencies": {
2134
+ "esbuild": "^0.25.0",
2135
+ "fdir": "^6.4.4",
2136
+ "picomatch": "^4.0.2",
2137
+ "postcss": "^8.5.3",
2138
+ "rollup": "^4.34.9",
2139
+ "tinyglobby": "^0.2.13"
2140
+ },
2141
+ "bin": {
2142
+ "vite": "bin/vite.js"
2143
+ },
2144
+ "engines": {
2145
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
2146
+ },
2147
+ "funding": {
2148
+ "url": "https://github.com/vitejs/vite?sponsor=1"
2149
+ },
2150
+ "optionalDependencies": {
2151
+ "fsevents": "~2.3.3"
2152
+ },
2153
+ "peerDependencies": {
2154
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
2155
+ "jiti": ">=1.21.0",
2156
+ "less": "*",
2157
+ "lightningcss": "^1.21.0",
2158
+ "sass": "*",
2159
+ "sass-embedded": "*",
2160
+ "stylus": "*",
2161
+ "sugarss": "*",
2162
+ "terser": "^5.16.0",
2163
+ "tsx": "^4.8.1",
2164
+ "yaml": "^2.4.2"
2165
+ },
2166
+ "peerDependenciesMeta": {
2167
+ "@types/node": {
2168
+ "optional": true
2169
+ },
2170
+ "jiti": {
2171
+ "optional": true
2172
+ },
2173
+ "less": {
2174
+ "optional": true
2175
+ },
2176
+ "lightningcss": {
2177
+ "optional": true
2178
+ },
2179
+ "sass": {
2180
+ "optional": true
2181
+ },
2182
+ "sass-embedded": {
2183
+ "optional": true
2184
+ },
2185
+ "stylus": {
2186
+ "optional": true
2187
+ },
2188
+ "sugarss": {
2189
+ "optional": true
2190
+ },
2191
+ "terser": {
2192
+ "optional": true
2193
+ },
2194
+ "tsx": {
2195
+ "optional": true
2196
+ },
2197
+ "yaml": {
2198
+ "optional": true
2199
+ }
2200
+ }
2201
+ },
2202
+ "node_modules/yallist": {
2203
+ "version": "3.1.1",
2204
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
2205
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
2206
+ "dev": true,
2207
+ "license": "ISC"
2208
+ },
2209
+ "node_modules/zod": {
2210
+ "version": "4.3.6",
2211
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
2212
+ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
2213
+ "license": "MIT",
2214
+ "peer": true,
2215
+ "funding": {
2216
+ "url": "https://github.com/sponsors/colinhacks"
2217
+ }
2218
+ }
2219
+ }
2220
+ }
frontend/package.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "reachy-mini-vibe-coder-frontend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "preview": "vite preview",
10
+ "typecheck": "tsc -b --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "@ai-sdk/react": "^3.0.160",
14
+ "@huggingface/hub": "^2.11.0",
15
+ "@monaco-editor/react": "^4.7.0",
16
+ "ai": "^6.0.158",
17
+ "clsx": "^2.1.1",
18
+ "nanoid": "^5.0.7",
19
+ "react": "^18.3.1",
20
+ "react-dom": "^18.3.1"
21
+ },
22
+ "devDependencies": {
23
+ "@types/react": "^18.3.12",
24
+ "@types/react-dom": "^18.3.1",
25
+ "@vitejs/plugin-react": "^4.3.4",
26
+ "typescript": "^5.6.0",
27
+ "vite": "^6.0.0"
28
+ }
29
+ }
frontend/src/App.tsx ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import { useHfAuth } from "./hooks/useHfAuth";
3
+ import { useVirtualFS } from "./hooks/useVirtualFS";
4
+ import { useAgentChat } from "./hooks/useAgentChat";
5
+ import { SignInScreen } from "./components/SignInScreen";
6
+ import { TopBar } from "./components/TopBar";
7
+ import { ChatPanel } from "./components/ChatPanel";
8
+ import { CodeEditor } from "./components/CodeEditor";
9
+ import { PreviewFrame } from "./components/PreviewFrame";
10
+ import { PublishDialog } from "./components/PublishDialog";
11
+
12
+ interface Model {
13
+ id: string;
14
+ label: string;
15
+ }
16
+
17
+ type Tab = "code" | "preview";
18
+
19
+ export function App() {
20
+ const auth = useHfAuth();
21
+ const vfs = useVirtualFS();
22
+
23
+ const [models, setModels] = useState<Model[]>([]);
24
+ const [selectedModel, setSelectedModel] = useState<string | null>(null);
25
+ const modelRef = useRef<string | null>(selectedModel);
26
+ modelRef.current = selectedModel;
27
+
28
+ const [activeTab, setActiveTab] = useState<Tab>("code");
29
+ const [publishOpen, setPublishOpen] = useState(false);
30
+
31
+ const chat = useAgentChat({
32
+ accessToken: auth.accessToken,
33
+ vfs,
34
+ modelRef,
35
+ });
36
+
37
+ useEffect(() => {
38
+ if (!auth.isSignedIn || !auth.accessToken) return;
39
+ let cancelled = false;
40
+ fetch("/api/models", {
41
+ headers: { Authorization: `Bearer ${auth.accessToken}` },
42
+ })
43
+ .then((r) => (r.ok ? r.json() : null))
44
+ .then((data) => {
45
+ if (cancelled || !data) return;
46
+ setModels(data.models ?? []);
47
+ if (data.default && !selectedModel) setSelectedModel(data.default);
48
+ })
49
+ .catch(() => {});
50
+ return () => {
51
+ cancelled = true;
52
+ };
53
+ }, [auth.isSignedIn, auth.accessToken, selectedModel]);
54
+
55
+ const handleReset = useCallback(() => {
56
+ if (
57
+ !confirm(
58
+ "Clear the chat history and all files in the virtual FS? This cannot be undone.",
59
+ )
60
+ ) {
61
+ return;
62
+ }
63
+ chat.clearMessages();
64
+ vfs.clear();
65
+ }, [chat, vfs]);
66
+
67
+ const handleFileChange = useCallback(
68
+ (path: string, content: string) => {
69
+ vfs.writeFile(path, content);
70
+ },
71
+ [vfs],
72
+ );
73
+
74
+ if (!auth.isReady) {
75
+ return (
76
+ <div className="app-shell">
77
+ <div style={{ margin: "auto", color: "var(--text-faint)" }}>
78
+ Loading…
79
+ </div>
80
+ </div>
81
+ );
82
+ }
83
+
84
+ if (!auth.isSignedIn) {
85
+ return (
86
+ <div className="app-shell">
87
+ <SignInScreen
88
+ onSignIn={auth.signIn}
89
+ isSigningIn={auth.isSigningIn}
90
+ error={auth.error}
91
+ hasClientId={!!auth.clientId}
92
+ />
93
+ </div>
94
+ );
95
+ }
96
+
97
+ const canPublish = vfs.files.length > 0;
98
+
99
+ return (
100
+ <div className="app-shell">
101
+ <TopBar
102
+ user={auth.user}
103
+ models={models}
104
+ selectedModel={selectedModel}
105
+ onModelChange={setSelectedModel}
106
+ onPublish={() => setPublishOpen(true)}
107
+ onSignOut={auth.signOut}
108
+ onReset={handleReset}
109
+ canPublish={canPublish}
110
+ />
111
+
112
+ <div className="app-main">
113
+ <ChatPanel
114
+ messages={chat.messages}
115
+ isLoading={chat.isLoading}
116
+ input={chat.input}
117
+ setInput={chat.setInput}
118
+ onSend={chat.sendMessage}
119
+ onStop={chat.stop}
120
+ error={chat.error ?? null}
121
+ />
122
+
123
+ <div className="right-pane">
124
+ <div className="tab-bar">
125
+ <button
126
+ type="button"
127
+ className={`tab ${activeTab === "code" ? "active" : ""}`}
128
+ onClick={() => setActiveTab("code")}
129
+ >
130
+ Code · {vfs.files.length}
131
+ </button>
132
+ <button
133
+ type="button"
134
+ className={`tab ${activeTab === "preview" ? "active" : ""}`}
135
+ onClick={() => setActiveTab("preview")}
136
+ >
137
+ Preview
138
+ </button>
139
+ <div className="tab-spacer" />
140
+ </div>
141
+
142
+ <div
143
+ className={`pane-content ${activeTab === "code" ? "" : "hidden"}`}
144
+ >
145
+ <CodeEditor files={vfs.files} onChange={handleFileChange} />
146
+ </div>
147
+ <div
148
+ className={`pane-content ${activeTab === "preview" ? "" : "hidden"}`}
149
+ >
150
+ <PreviewFrame files={vfs.files} version={vfs.version} />
151
+ </div>
152
+ </div>
153
+ </div>
154
+
155
+ <PublishDialog
156
+ open={publishOpen}
157
+ onClose={() => setPublishOpen(false)}
158
+ files={vfs.files}
159
+ accessToken={auth.accessToken}
160
+ defaultUsername={auth.user?.preferred_username ?? null}
161
+ />
162
+ </div>
163
+ );
164
+ }
frontend/src/components/ChatPanel.tsx ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { UIMessage } from "ai";
2
+ import { useEffect, useRef } from "react";
3
+
4
+ interface ChatPanelProps {
5
+ messages: UIMessage[];
6
+ isLoading: boolean;
7
+ input: string;
8
+ setInput: (v: string) => void;
9
+ onSend: (text: string) => void;
10
+ onStop: () => void;
11
+ error: Error | null;
12
+ }
13
+
14
+ const EXAMPLE_PROMPTS = [
15
+ "Create a minimal Reachy Mini voice app that just connects to OpenAI Realtime and lets me talk.",
16
+ "Add a head-wobbler motion agent that reacts to the AI voice's RMS level.",
17
+ "Add a play_dance tool call that streams a procedural happy dance to the robot.",
18
+ ];
19
+
20
+ function renderMessagePart(
21
+ part: UIMessage["parts"][number],
22
+ idx: number,
23
+ ): React.ReactNode {
24
+ const type = part.type;
25
+
26
+ if (type === "text") {
27
+ const text = (part as { text: string }).text;
28
+ if (!text.trim()) return null;
29
+ return (
30
+ <div key={idx} className="bubble-agent-text">
31
+ {text}
32
+ </div>
33
+ );
34
+ }
35
+
36
+ if (typeof type === "string" && type.startsWith("tool-")) {
37
+ const toolPart = part as {
38
+ type: string;
39
+ state?: string;
40
+ input?: Record<string, unknown>;
41
+ output?: unknown;
42
+ errorText?: string;
43
+ };
44
+ const toolName = type.slice("tool-".length);
45
+ const state = toolPart.state ?? "unknown";
46
+ const statusClass =
47
+ state === "output-available" || state === "result"
48
+ ? "status-done"
49
+ : state === "output-error" || state === "error"
50
+ ? "status-error"
51
+ : "";
52
+
53
+ let detail = "";
54
+ if (toolPart.input) {
55
+ const input = toolPart.input;
56
+ if (toolName === "write_file" && typeof input.path === "string") {
57
+ detail = `${input.path}${typeof input.content === "string" ? ` · ${input.content.length} chars` : ""}`;
58
+ } else if (toolName === "edit_file" && typeof input.path === "string") {
59
+ detail = String(input.path);
60
+ } else if (
61
+ (toolName === "read_file" ||
62
+ toolName === "delete_file") &&
63
+ typeof input.path === "string"
64
+ ) {
65
+ detail = String(input.path);
66
+ } else if (toolName === "read_skill_doc" && typeof input.topic === "string") {
67
+ detail = String(input.topic);
68
+ }
69
+ }
70
+
71
+ return (
72
+ <div key={idx} className={`bubble-tool ${statusClass}`}>
73
+ <div className="bubble-tool-header">
74
+ <span className="dot" />
75
+ <span>{toolLabel(toolName)}</span>
76
+ {detail && <span className="mono" style={{ color: "var(--text-faint)" }}>· {detail}</span>}
77
+ </div>
78
+ {toolPart.errorText && (
79
+ <div className="bubble-tool-detail">{toolPart.errorText}</div>
80
+ )}
81
+ </div>
82
+ );
83
+ }
84
+
85
+ return null;
86
+ }
87
+
88
+ function toolLabel(name: string): string {
89
+ const map: Record<string, string> = {
90
+ write_file: "Writing file",
91
+ edit_file: "Editing file",
92
+ delete_file: "Deleting file",
93
+ read_file: "Reading file",
94
+ list_files: "Listing files",
95
+ read_skill_doc: "Loading skill chapter",
96
+ };
97
+ return map[name] ?? name;
98
+ }
99
+
100
+ export function ChatPanel({
101
+ messages,
102
+ isLoading,
103
+ input,
104
+ setInput,
105
+ onSend,
106
+ onStop,
107
+ error,
108
+ }: ChatPanelProps) {
109
+ const scrollRef = useRef<HTMLDivElement>(null);
110
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
111
+
112
+ useEffect(() => {
113
+ scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight });
114
+ }, [messages]);
115
+
116
+ const handleSubmit = (e?: React.FormEvent) => {
117
+ e?.preventDefault();
118
+ if (isLoading) {
119
+ onStop();
120
+ return;
121
+ }
122
+ const text = input.trim();
123
+ if (!text) return;
124
+ onSend(text);
125
+ };
126
+
127
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
128
+ if (e.key === "Enter" && !e.shiftKey) {
129
+ e.preventDefault();
130
+ handleSubmit();
131
+ }
132
+ };
133
+
134
+ const showEmpty = messages.length === 0;
135
+
136
+ return (
137
+ <aside className="chat-panel">
138
+ <div className="chat-messages" ref={scrollRef}>
139
+ {showEmpty && (
140
+ <div className="chat-empty">
141
+ <p>
142
+ Describe the Reachy Mini voice app you want to build. The agent
143
+ knows the full <code>build-reachy-voice-app</code> skill (WebRTC
144
+ 2-hop, OpenAI Realtime, motion agents, HF deployment) and will
145
+ create the files in the virtual FS on the right.
146
+ </p>
147
+ <div className="chat-empty-examples">
148
+ {EXAMPLE_PROMPTS.map((p) => (
149
+ <button
150
+ key={p}
151
+ type="button"
152
+ className="chat-empty-example"
153
+ onClick={() => setInput(p)}
154
+ >
155
+ {p}
156
+ </button>
157
+ ))}
158
+ </div>
159
+ </div>
160
+ )}
161
+
162
+ {messages.map((m) =>
163
+ m.role === "user" ? (
164
+ <div key={m.id} className="bubble-user">
165
+ {m.parts
166
+ .map((p) => ((p as { type: string }).type === "text" ? (p as { text: string }).text : ""))
167
+ .join("")}
168
+ </div>
169
+ ) : (
170
+ <div key={m.id} className="bubble-agent">
171
+ {m.parts.map((p, i) => renderMessagePart(p, i))}
172
+ </div>
173
+ ),
174
+ )}
175
+
176
+ {error && (
177
+ <div className="signin-error" style={{ alignSelf: "stretch" }}>
178
+ {error.message}
179
+ </div>
180
+ )}
181
+ </div>
182
+
183
+ <form className="chat-composer" onSubmit={handleSubmit}>
184
+ <div className="chat-composer-field">
185
+ <textarea
186
+ ref={textareaRef}
187
+ value={input}
188
+ onChange={(e) => setInput(e.target.value)}
189
+ onKeyDown={handleKeyDown}
190
+ placeholder={
191
+ isLoading
192
+ ? "Agent is working…"
193
+ : "Describe what to build or change…"
194
+ }
195
+ rows={1}
196
+ />
197
+ <button
198
+ type="submit"
199
+ className="chat-send"
200
+ disabled={!isLoading && input.trim().length === 0}
201
+ title={isLoading ? "Stop" : "Send"}
202
+ >
203
+ {isLoading ? "■" : "↑"}
204
+ </button>
205
+ </div>
206
+ <div className="chat-status">
207
+ {isLoading ? (
208
+ <>
209
+ <span className="spinner" />
210
+ <span>Agent is thinking…</span>
211
+ </>
212
+ ) : (
213
+ <span>Enter to send · Shift+Enter for newline</span>
214
+ )}
215
+ </div>
216
+ </form>
217
+ </aside>
218
+ );
219
+ }
frontend/src/components/CodeEditor.tsx ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Editor from "@monaco-editor/react";
2
+ import { useEffect, useMemo, useState } from "react";
3
+ import type { VirtualFile } from "../hooks/useVirtualFS";
4
+
5
+ interface CodeEditorProps {
6
+ files: VirtualFile[];
7
+ onChange: (path: string, content: string) => void;
8
+ }
9
+
10
+ function guessLanguage(path: string): string {
11
+ const ext = path.split(".").pop()?.toLowerCase() ?? "";
12
+ switch (ext) {
13
+ case "ts":
14
+ case "tsx":
15
+ return "typescript";
16
+ case "js":
17
+ case "jsx":
18
+ return "javascript";
19
+ case "html":
20
+ return "html";
21
+ case "css":
22
+ return "css";
23
+ case "json":
24
+ return "json";
25
+ case "md":
26
+ return "markdown";
27
+ case "yml":
28
+ case "yaml":
29
+ return "yaml";
30
+ case "sh":
31
+ return "shell";
32
+ default:
33
+ return "plaintext";
34
+ }
35
+ }
36
+
37
+ export function CodeEditor({ files, onChange }: CodeEditorProps) {
38
+ const [activePath, setActivePath] = useState<string | null>(null);
39
+
40
+ useEffect(() => {
41
+ if (!activePath && files.length > 0) {
42
+ const preferred = files.find(
43
+ (f) => f.path === "index.html" || f.path === "src/main.ts",
44
+ );
45
+ setActivePath(preferred?.path ?? files[0].path);
46
+ return;
47
+ }
48
+ if (activePath && !files.some((f) => f.path === activePath)) {
49
+ setActivePath(files[0]?.path ?? null);
50
+ }
51
+ }, [files, activePath]);
52
+
53
+ const activeFile = useMemo(
54
+ () => files.find((f) => f.path === activePath) ?? null,
55
+ [files, activePath],
56
+ );
57
+
58
+ if (files.length === 0) {
59
+ return (
60
+ <div className="code-layout">
61
+ <div className="file-tree">
62
+ <div className="file-tree-empty">No files yet.</div>
63
+ </div>
64
+ <div className="editor-area">
65
+ <div className="editor-placeholder">
66
+ The agent will write files here. Ask it to create an app.
67
+ </div>
68
+ </div>
69
+ </div>
70
+ );
71
+ }
72
+
73
+ return (
74
+ <div className="code-layout">
75
+ <div className="file-tree">
76
+ {files.map((f) => (
77
+ <div
78
+ key={f.path}
79
+ className={`file-tree-item ${activePath === f.path ? "active" : ""}`}
80
+ onClick={() => setActivePath(f.path)}
81
+ >
82
+ <span>{f.path}</span>
83
+ <span className="file-tree-size">{f.content.length}B</span>
84
+ </div>
85
+ ))}
86
+ </div>
87
+ <div className="editor-area">
88
+ {activeFile ? (
89
+ <Editor
90
+ key={activeFile.path}
91
+ height="100%"
92
+ theme="vs-dark"
93
+ language={guessLanguage(activeFile.path)}
94
+ value={activeFile.content}
95
+ onChange={(v) => onChange(activeFile.path, v ?? "")}
96
+ options={{
97
+ fontSize: 12,
98
+ fontFamily:
99
+ '"JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace',
100
+ minimap: { enabled: false },
101
+ scrollBeyondLastLine: false,
102
+ tabSize: 2,
103
+ wordWrap: "on",
104
+ automaticLayout: true,
105
+ }}
106
+ />
107
+ ) : (
108
+ <div className="editor-placeholder">Select a file from the tree.</div>
109
+ )}
110
+ </div>
111
+ </div>
112
+ );
113
+ }
frontend/src/components/HfLogo.tsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * The official Hugging Face emoji logo. Drop-in SVG; the container controls the
3
+ * size via font-size or an explicit width/height prop.
4
+ */
5
+ export function HfLogo({ size = 20 }: { size?: number }) {
6
+ return (
7
+ <svg
8
+ viewBox="0 0 96 96"
9
+ width={size}
10
+ height={size}
11
+ aria-hidden="true"
12
+ style={{ display: "inline-block" }}
13
+ >
14
+ <defs>
15
+ <radialGradient id="hf-face" cx="0.5" cy="0.5" r="0.5">
16
+ <stop offset="0%" stopColor="#FFD21E" />
17
+ <stop offset="100%" stopColor="#FFAC03" />
18
+ </radialGradient>
19
+ </defs>
20
+ <circle cx="48" cy="48" r="44" fill="url(#hf-face)" stroke="#1A1A1A" strokeWidth="4" />
21
+ <circle cx="32" cy="42" r="6" fill="#1A1A1A" />
22
+ <circle cx="64" cy="42" r="6" fill="#1A1A1A" />
23
+ <path
24
+ d="M30 60 Q48 76 66 60 Q66 70 48 72 Q30 70 30 60 Z"
25
+ fill="#1A1A1A"
26
+ />
27
+ <circle cx="22" cy="56" r="5" fill="#FF7979" opacity="0.8" />
28
+ <circle cx="74" cy="56" r="5" fill="#FF7979" opacity="0.8" />
29
+ </svg>
30
+ );
31
+ }
frontend/src/components/PreviewFrame.tsx ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useMemo, useRef, useState } from "react";
2
+ import type { VirtualFile } from "../hooks/useVirtualFS";
3
+
4
+ interface PreviewFrameProps {
5
+ files: VirtualFile[];
6
+ version: number;
7
+ }
8
+
9
+ /**
10
+ * Render the virtual FS inside a sandboxed iframe.
11
+ *
12
+ * Strategy: we build a single HTML document by taking `index.html` from the
13
+ * virtual FS and inlining every other text asset (CSS + JS/TS). This is
14
+ * deliberately simple and works for the kind of minimal Vite apps the agent
15
+ * produces.
16
+ *
17
+ * Notes / limitations:
18
+ * - `.ts` files are shipped as `type="module"` with an esbuild-free path:
19
+ * they run in the iframe as-is, so only ES2022-compatible TS (= "TypeScript
20
+ * lite") works. Real Vite builds are what we ship to HF Spaces.
21
+ * - WebRTC/OpenAI calls will fail in the preview sandbox because of CORS and
22
+ * missing OAuth. The preview is there to catch JS errors and see the UI.
23
+ * - We strip the `<script type="module" src="/src/main.ts">` tag when the
24
+ * corresponding file is inlined just above it.
25
+ */
26
+ function buildPreviewDoc(files: VirtualFile[]): string | null {
27
+ const fileMap = new Map(files.map((f) => [f.path, f.content]));
28
+
29
+ const rawHtml = fileMap.get("index.html");
30
+ if (!rawHtml) return null;
31
+
32
+ // Inline every local text asset referenced through href="/..." or src="/..."
33
+ const inlineAsset = (_match: string, attr: string, _q: string, path: string): string => {
34
+ const normalized = path.replace(/^\//, "");
35
+ const content = fileMap.get(normalized);
36
+ if (content === undefined) return _match;
37
+
38
+ if (attr === "href" && normalized.endsWith(".css")) {
39
+ return `<style data-inlined="${normalized}">\n${content}\n</style>`;
40
+ }
41
+ if (
42
+ attr === "src" &&
43
+ (normalized.endsWith(".js") ||
44
+ normalized.endsWith(".mjs") ||
45
+ normalized.endsWith(".ts") ||
46
+ normalized.endsWith(".tsx"))
47
+ ) {
48
+ return `<script type="module" data-inlined="${normalized}">\n${content}\n</script>`;
49
+ }
50
+ return _match;
51
+ };
52
+
53
+ const inlined = rawHtml.replace(
54
+ /<(?:link|script)[^>]*\b(href|src)\s*=\s*(["'])(\/[^"']+)\2[^>]*>(?:<\/script>)?/gi,
55
+ inlineAsset,
56
+ );
57
+
58
+ const bannerDoc = `<!-- Preview generated by reachy-mini-vibe-coder -->`;
59
+ return `${bannerDoc}\n${inlined}`;
60
+ }
61
+
62
+ export function PreviewFrame({ files, version }: PreviewFrameProps) {
63
+ const [docHtml, setDocHtml] = useState<string | null>(null);
64
+ const timerRef = useRef<number | null>(null);
65
+
66
+ const hasIndex = useMemo(
67
+ () => files.some((f) => f.path === "index.html"),
68
+ [files],
69
+ );
70
+
71
+ useEffect(() => {
72
+ if (timerRef.current) window.clearTimeout(timerRef.current);
73
+ timerRef.current = window.setTimeout(() => {
74
+ const doc = buildPreviewDoc(files);
75
+ setDocHtml(doc);
76
+ }, 300);
77
+ return () => {
78
+ if (timerRef.current) window.clearTimeout(timerRef.current);
79
+ };
80
+ }, [files, version]);
81
+
82
+ if (!hasIndex) {
83
+ return (
84
+ <div className="preview-empty">
85
+ <p>No <code>index.html</code> in the virtual FS yet.</p>
86
+ <p style={{ marginTop: 8 }}>Ask the agent to create one.</p>
87
+ </div>
88
+ );
89
+ }
90
+
91
+ if (docHtml === null) {
92
+ return <div className="preview-empty">Building preview…</div>;
93
+ }
94
+
95
+ return (
96
+ <iframe
97
+ key={version}
98
+ className="preview-frame"
99
+ title="App preview"
100
+ srcDoc={docHtml}
101
+ sandbox="allow-scripts allow-same-origin allow-forms"
102
+ />
103
+ );
104
+ }
frontend/src/components/PublishDialog.tsx ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useState } from "react";
2
+ import type { VirtualFile } from "../hooks/useVirtualFS";
3
+ import { usePublish } from "../hooks/usePublish";
4
+
5
+ interface PublishDialogProps {
6
+ open: boolean;
7
+ onClose: () => void;
8
+ files: VirtualFile[];
9
+ accessToken: string | null;
10
+ defaultUsername: string | null;
11
+ }
12
+
13
+ function suggestRepoName(username: string | null): string {
14
+ const stamp = new Date()
15
+ .toISOString()
16
+ .slice(0, 10)
17
+ .replace(/-/g, "");
18
+ return `reachy-voice-${stamp}`;
19
+ }
20
+
21
+ export function PublishDialog({
22
+ open,
23
+ onClose,
24
+ files,
25
+ accessToken,
26
+ defaultUsername,
27
+ }: PublishDialogProps) {
28
+ const { status, steps, result, error, errorCode, publish, reset } =
29
+ usePublish(accessToken);
30
+
31
+ const [repoName, setRepoName] = useState("");
32
+ const [isPrivate, setIsPrivate] = useState(false);
33
+ const [overwrite, setOverwrite] = useState(false);
34
+ const [commitMessage, setCommitMessage] = useState("");
35
+
36
+ useEffect(() => {
37
+ if (open) {
38
+ setRepoName((prev) => prev || suggestRepoName(defaultUsername));
39
+ if (status !== "idle") reset();
40
+ }
41
+ }, [open, defaultUsername, reset, status]);
42
+
43
+ if (!open) return null;
44
+
45
+ const handlePublish = () => {
46
+ if (status === "publishing") return;
47
+ publish({
48
+ repoName,
49
+ isPrivate,
50
+ overwrite,
51
+ commitMessage: commitMessage.trim() || undefined,
52
+ files,
53
+ });
54
+ };
55
+
56
+ const handleClose = () => {
57
+ if (status === "publishing") return;
58
+ reset();
59
+ onClose();
60
+ };
61
+
62
+ return (
63
+ <div className="dialog-backdrop" onClick={handleClose}>
64
+ <div className="dialog" onClick={(e) => e.stopPropagation()}>
65
+ <header className="dialog-header">
66
+ <span>Publish to Hugging Face Space</span>
67
+ <button
68
+ type="button"
69
+ className="btn-ghost"
70
+ onClick={handleClose}
71
+ disabled={status === "publishing"}
72
+ style={{ padding: "4px 8px" }}
73
+ >
74
+ ✕
75
+ </button>
76
+ </header>
77
+
78
+ <div className="dialog-body">
79
+ <div className="field">
80
+ <label>Space name</label>
81
+ <input
82
+ type="text"
83
+ value={repoName}
84
+ onChange={(e) => setRepoName(e.target.value)}
85
+ placeholder="my-reachy-app"
86
+ disabled={status === "publishing"}
87
+ />
88
+ {defaultUsername && (
89
+ <div className="field-hint">
90
+ Will be published as <code>{defaultUsername}/{repoName || "<name>"}</code>
91
+ </div>
92
+ )}
93
+ </div>
94
+
95
+ <div className="field">
96
+ <label>Commit message (optional)</label>
97
+ <input
98
+ type="text"
99
+ value={commitMessage}
100
+ onChange={(e) => setCommitMessage(e.target.value)}
101
+ placeholder="Initial commit from Reachy Vibe Coder"
102
+ disabled={status === "publishing"}
103
+ />
104
+ </div>
105
+
106
+ <div className="field-row">
107
+ <input
108
+ id="publish-private"
109
+ type="checkbox"
110
+ checked={isPrivate}
111
+ onChange={(e) => setIsPrivate(e.target.checked)}
112
+ disabled={status === "publishing"}
113
+ />
114
+ <label htmlFor="publish-private" style={{ cursor: "pointer" }}>
115
+ Make Space private
116
+ </label>
117
+ </div>
118
+
119
+ <div className="field-row">
120
+ <input
121
+ id="publish-overwrite"
122
+ type="checkbox"
123
+ checked={overwrite}
124
+ onChange={(e) => setOverwrite(e.target.checked)}
125
+ disabled={status === "publishing"}
126
+ />
127
+ <label htmlFor="publish-overwrite" style={{ cursor: "pointer" }}>
128
+ Overwrite if Space already exists
129
+ </label>
130
+ </div>
131
+
132
+ <div className="field">
133
+ <label>Files to upload ({files.length})</label>
134
+ <div className="publish-log">
135
+ {files.map((f) => (
136
+ <div key={f.path} className="publish-log-step">
137
+ {f.path} <span style={{ color: "var(--text-faint)" }}>· {f.content.length}B</span>
138
+ </div>
139
+ ))}
140
+ </div>
141
+ </div>
142
+
143
+ {steps.length > 0 && (
144
+ <div className="field">
145
+ <label>Progress</label>
146
+ <div className="publish-log">
147
+ {steps.map((s, i) => (
148
+ <div
149
+ key={i}
150
+ className={`publish-log-step ${s.step === "done" ? "done" : ""}`}
151
+ >
152
+ [{s.step}] {s.message}
153
+ </div>
154
+ ))}
155
+ </div>
156
+ </div>
157
+ )}
158
+
159
+ {status === "done" && result.spaceUrl && (
160
+ <div className="publish-success">
161
+ <div>✓ Published!</div>
162
+ <div>
163
+ <a href={result.spaceUrl} target="_blank" rel="noopener">
164
+ {result.spaceUrl}
165
+ </a>
166
+ </div>
167
+ </div>
168
+ )}
169
+
170
+ {status === "error" && error && (
171
+ <div className="publish-error">
172
+ {error}
173
+ {errorCode === "REPO_EXISTS" && (
174
+ <div style={{ marginTop: 6 }}>
175
+ Enable "Overwrite if Space already exists" and try again.
176
+ </div>
177
+ )}
178
+ </div>
179
+ )}
180
+ </div>
181
+
182
+ <footer className="dialog-footer">
183
+ <button
184
+ type="button"
185
+ className="btn btn-ghost"
186
+ onClick={handleClose}
187
+ disabled={status === "publishing"}
188
+ >
189
+ {status === "done" ? "Close" : "Cancel"}
190
+ </button>
191
+ {status !== "done" && (
192
+ <button
193
+ type="button"
194
+ className="btn btn-primary"
195
+ onClick={handlePublish}
196
+ disabled={
197
+ status === "publishing" ||
198
+ repoName.trim().length === 0 ||
199
+ files.length === 0
200
+ }
201
+ >
202
+ {status === "publishing" ? "Publishing…" : "Publish"}
203
+ </button>
204
+ )}
205
+ </footer>
206
+ </div>
207
+ </div>
208
+ );
209
+ }
frontend/src/components/SignInScreen.tsx ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { HfLogo } from "./HfLogo";
2
+
3
+ interface SignInScreenProps {
4
+ onSignIn: () => void;
5
+ isSigningIn: boolean;
6
+ error: string | null;
7
+ hasClientId: boolean;
8
+ }
9
+
10
+ export function SignInScreen({
11
+ onSignIn,
12
+ isSigningIn,
13
+ error,
14
+ hasClientId,
15
+ }: SignInScreenProps) {
16
+ return (
17
+ <div className="signin-screen">
18
+ <div className="signin-emoji">🤖</div>
19
+ <h1 className="signin-title">Reachy Mini · Vibe Coder</h1>
20
+ <p className="signin-subtitle">
21
+ Sign in with your Hugging Face account to start vibe-coding browser-based
22
+ voice apps for Reachy Mini. You'll be able to preview the app live and
23
+ publish it as a Hugging Face Space in one click.
24
+ </p>
25
+ <button
26
+ type="button"
27
+ className="signin-button"
28
+ onClick={onSignIn}
29
+ disabled={isSigningIn || !hasClientId}
30
+ >
31
+ <HfLogo size={20} />
32
+ {isSigningIn ? "Redirecting…" : "Sign in with Hugging Face"}
33
+ </button>
34
+
35
+ {!hasClientId && (
36
+ <p className="signin-hint">
37
+ No OAuth client ID detected. On a Hugging Face Space this is injected
38
+ automatically via the Docker entrypoint. For local dev, create an
39
+ OAuth app and set <code>VITE_HF_OAUTH_CLIENT_ID</code> in{" "}
40
+ <code>frontend/.env.local</code>.
41
+ </p>
42
+ )}
43
+
44
+ {error && <div className="signin-error">{error}</div>}
45
+ </div>
46
+ );
47
+ }
frontend/src/components/TopBar.tsx ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { OAuthResult } from "@huggingface/hub";
2
+ import { HfLogo } from "./HfLogo";
3
+
4
+ interface Model {
5
+ id: string;
6
+ label: string;
7
+ }
8
+
9
+ interface TopBarProps {
10
+ user: OAuthResult["userInfo"] | null;
11
+ models: Model[];
12
+ selectedModel: string | null;
13
+ onModelChange: (id: string) => void;
14
+ onPublish: () => void;
15
+ onSignOut: () => void;
16
+ onReset: () => void;
17
+ canPublish: boolean;
18
+ }
19
+
20
+ export function TopBar({
21
+ user,
22
+ models,
23
+ selectedModel,
24
+ onModelChange,
25
+ onPublish,
26
+ onSignOut,
27
+ onReset,
28
+ canPublish,
29
+ }: TopBarProps) {
30
+ return (
31
+ <header className="topbar">
32
+ <div className="topbar-brand">
33
+ <HfLogo size={26} />
34
+ <div className="topbar-title">
35
+ <span className="topbar-title-main">Reachy Mini · Vibe Coder</span>
36
+ <span className="topbar-title-sub">Powered by Hugging Face</span>
37
+ </div>
38
+ </div>
39
+
40
+ <div className="topbar-spacer" />
41
+
42
+ {models.length > 0 && (
43
+ <div className="topbar-model">
44
+ <span>Model</span>
45
+ <select
46
+ value={selectedModel ?? ""}
47
+ onChange={(e) => onModelChange(e.target.value)}
48
+ >
49
+ {models.map((m) => (
50
+ <option key={m.id} value={m.id}>
51
+ {m.label}
52
+ </option>
53
+ ))}
54
+ </select>
55
+ </div>
56
+ )}
57
+
58
+ <button type="button" className="btn btn-ghost" onClick={onReset} title="Reset virtual FS and chat">
59
+ Reset
60
+ </button>
61
+
62
+ <button
63
+ type="button"
64
+ className="btn btn-primary"
65
+ onClick={onPublish}
66
+ disabled={!canPublish}
67
+ title={canPublish ? "Publish to Hugging Face Space" : "Add files before publishing"}
68
+ >
69
+ Publish to HF Space
70
+ </button>
71
+
72
+ {user && (
73
+ <div className="topbar-user" title={user.preferred_username}>
74
+ {user.picture ? (
75
+ <img src={user.picture} alt={user.preferred_username} />
76
+ ) : (
77
+ <span className="avatar-fallback">
78
+ {user.preferred_username.slice(0, 1).toUpperCase()}
79
+ </span>
80
+ )}
81
+ <span className="topbar-user-name">{user.preferred_username}</span>
82
+ <button
83
+ type="button"
84
+ className="btn-ghost topbar-user-menu"
85
+ onClick={onSignOut}
86
+ title="Sign out"
87
+ >
88
+ ✕
89
+ </button>
90
+ </div>
91
+ )}
92
+ </header>
93
+ );
94
+ }
frontend/src/hooks/useAgentChat.ts ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useChat } from "@ai-sdk/react";
2
+ import {
3
+ DefaultChatTransport,
4
+ lastAssistantMessageIsCompleteWithToolCalls,
5
+ } from "ai";
6
+ import type { UIMessage } from "ai";
7
+ import { useCallback, useMemo, useRef, useState } from "react";
8
+ import type { VirtualFSHandle } from "./useVirtualFS";
9
+
10
+ const MAX_TOOL_ROUNDS = 12;
11
+
12
+ interface UseAgentChatOptions {
13
+ accessToken: string | null;
14
+ vfs: VirtualFSHandle;
15
+ modelRef: React.RefObject<string | null>;
16
+ }
17
+
18
+ interface ToolCall {
19
+ toolName: string;
20
+ args: unknown;
21
+ toolCallId: string;
22
+ dynamic?: boolean;
23
+ }
24
+
25
+ function executeEditorToolCall(
26
+ tc: ToolCall,
27
+ vfs: VirtualFSHandle,
28
+ ): string | { error: string } {
29
+ const { toolName, args } = tc;
30
+
31
+ switch (toolName) {
32
+ case "write_file": {
33
+ const { path, content } = args as { path: string; content: string };
34
+ if (!path) return { error: "Missing `path`" };
35
+ if (typeof content !== "string") return { error: "Missing `content`" };
36
+ vfs.writeFile(path, content);
37
+ return `Wrote ${content.length} chars to ${path}`;
38
+ }
39
+
40
+ case "edit_file": {
41
+ const { path, old_string, new_string } = args as {
42
+ path: string;
43
+ old_string: string;
44
+ new_string: string;
45
+ };
46
+ const res = vfs.editFile(path, old_string, new_string);
47
+ if (!res.ok) return { error: res.error ?? "Edit failed" };
48
+ return `Edited ${path}`;
49
+ }
50
+
51
+ case "delete_file": {
52
+ const { path } = args as { path: string };
53
+ const res = vfs.deleteFile(path);
54
+ if (!res.ok) return { error: res.error ?? "Delete failed" };
55
+ return `Deleted ${path}`;
56
+ }
57
+
58
+ case "read_file": {
59
+ const { path } = args as { path: string };
60
+ const file = vfs.readFile(path);
61
+ if (!file) return { error: `File "${path}" does not exist` };
62
+ return file.content;
63
+ }
64
+
65
+ case "list_files": {
66
+ const list = vfs.listFiles();
67
+ if (list.length === 0) return "(virtual FS is empty)";
68
+ return list
69
+ .map((f) => `- ${f.path} (${f.content.length} bytes)`)
70
+ .join("\n");
71
+ }
72
+
73
+ default:
74
+ return { error: `Unknown client-side tool: ${toolName}` };
75
+ }
76
+ }
77
+
78
+ export function useAgentChat({ accessToken, vfs, modelRef }: UseAgentChatOptions) {
79
+ const vfsRef = useRef(vfs);
80
+ vfsRef.current = vfs;
81
+
82
+ const [input, setInput] = useState("");
83
+
84
+ const transport = useMemo(
85
+ () =>
86
+ new DefaultChatTransport({
87
+ api: "/api/chat",
88
+ headers: (): Record<string, string> =>
89
+ accessToken ? { Authorization: `Bearer ${accessToken}` } : {},
90
+ }),
91
+ [accessToken],
92
+ );
93
+
94
+ const { addToolResult, ...chat } = useChat({
95
+ transport,
96
+ sendAutomaticallyWhen: ({ messages }) => {
97
+ let toolRounds = 0;
98
+ for (let i = messages.length - 1; i >= 0; i--) {
99
+ const m = messages[i];
100
+ if (m.role !== "assistant") break;
101
+ const hasToolPart = (m.parts ?? []).some(
102
+ (p) => typeof p.type === "string" && p.type.startsWith("tool-"),
103
+ );
104
+ if (!hasToolPart) break;
105
+ toolRounds += 1;
106
+ }
107
+ if (toolRounds >= MAX_TOOL_ROUNDS) return false;
108
+ return lastAssistantMessageIsCompleteWithToolCalls({ messages });
109
+ },
110
+
111
+ async onToolCall({ toolCall }) {
112
+ if (toolCall.dynamic) return;
113
+
114
+ // `read_skill_doc` is executed server-side (see backend tools.ts).
115
+ // Only the editor tools are handled here.
116
+ const clientSideTools = new Set([
117
+ "write_file",
118
+ "edit_file",
119
+ "delete_file",
120
+ "read_file",
121
+ "list_files",
122
+ ]);
123
+ if (!clientSideTools.has(toolCall.toolName as string)) return;
124
+
125
+ const result = executeEditorToolCall(
126
+ {
127
+ toolName: toolCall.toolName as string,
128
+ args: toolCall.input,
129
+ toolCallId: toolCall.toolCallId,
130
+ },
131
+ vfsRef.current,
132
+ );
133
+
134
+ const output = typeof result === "string" ? result : `ERROR: ${result.error}`;
135
+
136
+ (addToolResult as (args: {
137
+ tool: string;
138
+ toolCallId: string;
139
+ output: unknown;
140
+ }) => void)({
141
+ tool: toolCall.toolName as string,
142
+ toolCallId: toolCall.toolCallId,
143
+ output,
144
+ });
145
+ },
146
+
147
+ onError(error) {
148
+ console.error("[agent] chat error:", error);
149
+ },
150
+ });
151
+
152
+ const sendMessage = useCallback(
153
+ (text: string) => {
154
+ const trimmed = text.trim();
155
+ if (!trimmed) return;
156
+ const context = {
157
+ files: vfsRef.current.getAll().map((f) => ({
158
+ path: f.path,
159
+ size: f.content.length,
160
+ })),
161
+ };
162
+ chat.sendMessage(
163
+ { text: trimmed },
164
+ { body: { context, model: modelRef.current } },
165
+ );
166
+ setInput("");
167
+ },
168
+ [chat, modelRef],
169
+ );
170
+
171
+ const clearMessages = useCallback(() => {
172
+ chat.setMessages([]);
173
+ }, [chat]);
174
+
175
+ return {
176
+ messages: chat.messages as UIMessage[],
177
+ status: chat.status,
178
+ isLoading: chat.status === "streaming" || chat.status === "submitted",
179
+ error: chat.error,
180
+ stop: chat.stop,
181
+ sendMessage,
182
+ clearMessages,
183
+ input,
184
+ setInput,
185
+ };
186
+ }
frontend/src/hooks/useHfAuth.ts ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import {
3
+ oauthHandleRedirectIfPresent,
4
+ oauthLoginUrl,
5
+ type OAuthResult,
6
+ } from "@huggingface/hub";
7
+
8
+ /**
9
+ * HF OAuth flow, reused from `reachy_mini_minimal_conversation/src/main.ts`.
10
+ *
11
+ * - On a Space: the client ID is injected into `window.huggingface.variables`
12
+ * by `docker-entrypoint.d/10-inject-hf-vars.sh` at container start.
13
+ * - Locally: we read it from `VITE_HF_OAUTH_CLIENT_ID` (`.env.local`).
14
+ */
15
+
16
+ const STORAGE_KEY = "hf-oauth-result-v1";
17
+
18
+ function readClientId(): string | null {
19
+ const injected = window.huggingface?.variables?.OAUTH_CLIENT_ID;
20
+ if (injected) return injected;
21
+ return import.meta.env.VITE_HF_OAUTH_CLIENT_ID ?? null;
22
+ }
23
+
24
+ function readScopes(): string {
25
+ return (
26
+ window.huggingface?.variables?.OAUTH_SCOPES ??
27
+ "openid profile write-repos manage-repos"
28
+ );
29
+ }
30
+
31
+ function loadStoredResult(): OAuthResult | null {
32
+ try {
33
+ const raw = sessionStorage.getItem(STORAGE_KEY);
34
+ if (!raw) return null;
35
+ const parsed = JSON.parse(raw) as OAuthResult;
36
+ const expiresAt = parsed.accessTokenExpiresAt
37
+ ? new Date(parsed.accessTokenExpiresAt).getTime()
38
+ : 0;
39
+ if (expiresAt && expiresAt < Date.now()) {
40
+ sessionStorage.removeItem(STORAGE_KEY);
41
+ return null;
42
+ }
43
+ parsed.accessTokenExpiresAt = new Date(parsed.accessTokenExpiresAt);
44
+ return parsed;
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ function storeResult(result: OAuthResult): void {
51
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify(result));
52
+ }
53
+
54
+ export interface HfAuthState {
55
+ isReady: boolean;
56
+ isSignedIn: boolean;
57
+ isSigningIn: boolean;
58
+ user: OAuthResult["userInfo"] | null;
59
+ accessToken: string | null;
60
+ error: string | null;
61
+ clientId: string | null;
62
+ signIn: () => Promise<void>;
63
+ signOut: () => void;
64
+ }
65
+
66
+ export function useHfAuth(): HfAuthState {
67
+ const [result, setResult] = useState<OAuthResult | null>(null);
68
+ const [isReady, setIsReady] = useState(false);
69
+ const [isSigningIn, setIsSigningIn] = useState(false);
70
+ const [error, setError] = useState<string | null>(null);
71
+ const clientIdRef = useRef<string | null>(readClientId());
72
+
73
+ useEffect(() => {
74
+ let cancelled = false;
75
+ (async () => {
76
+ try {
77
+ const redirectResult = await oauthHandleRedirectIfPresent();
78
+ if (cancelled) return;
79
+ if (redirectResult) {
80
+ storeResult(redirectResult);
81
+ setResult(redirectResult);
82
+ const url = new URL(window.location.href);
83
+ url.searchParams.delete("code");
84
+ url.searchParams.delete("state");
85
+ window.history.replaceState({}, "", url.toString());
86
+ setIsReady(true);
87
+ return;
88
+ }
89
+ const stored = loadStoredResult();
90
+ if (stored) setResult(stored);
91
+ setIsReady(true);
92
+ } catch (e) {
93
+ if (cancelled) return;
94
+ setError(e instanceof Error ? e.message : "OAuth error");
95
+ setIsReady(true);
96
+ }
97
+ })();
98
+ return () => {
99
+ cancelled = true;
100
+ };
101
+ }, []);
102
+
103
+ const signIn = useCallback(async () => {
104
+ setError(null);
105
+ const clientId = clientIdRef.current;
106
+ if (!clientId) {
107
+ setError(
108
+ "No Hugging Face OAuth client ID configured. On a Space this should be auto-injected; locally set VITE_HF_OAUTH_CLIENT_ID in frontend/.env.local.",
109
+ );
110
+ return;
111
+ }
112
+ setIsSigningIn(true);
113
+ try {
114
+ const url = await oauthLoginUrl({
115
+ clientId,
116
+ scopes: readScopes(),
117
+ redirectUrl: window.location.origin + window.location.pathname,
118
+ });
119
+ window.location.href = url;
120
+ } catch (e) {
121
+ setIsSigningIn(false);
122
+ setError(e instanceof Error ? e.message : "Failed to start OAuth flow");
123
+ }
124
+ }, []);
125
+
126
+ const signOut = useCallback(() => {
127
+ sessionStorage.removeItem(STORAGE_KEY);
128
+ setResult(null);
129
+ }, []);
130
+
131
+ return {
132
+ isReady,
133
+ isSignedIn: !!result?.accessToken,
134
+ isSigningIn,
135
+ user: result?.userInfo ?? null,
136
+ accessToken: result?.accessToken ?? null,
137
+ error,
138
+ clientId: clientIdRef.current,
139
+ signIn,
140
+ signOut,
141
+ };
142
+ }
frontend/src/hooks/usePublish.ts ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useCallback, useRef, useState } from "react";
2
+ import type { VirtualFile } from "./useVirtualFS";
3
+
4
+ export type PublishStatus = "idle" | "publishing" | "done" | "error";
5
+
6
+ export interface PublishStep {
7
+ step: string;
8
+ message: string;
9
+ timestamp: number;
10
+ }
11
+
12
+ interface PublishArgs {
13
+ repoName: string;
14
+ isPrivate: boolean;
15
+ overwrite: boolean;
16
+ files: VirtualFile[];
17
+ commitMessage?: string;
18
+ }
19
+
20
+ interface PublishResult {
21
+ repoId?: string;
22
+ spaceUrl?: string;
23
+ }
24
+
25
+ export function usePublish(accessToken: string | null) {
26
+ const [status, setStatus] = useState<PublishStatus>("idle");
27
+ const [steps, setSteps] = useState<PublishStep[]>([]);
28
+ const [result, setResult] = useState<PublishResult>({});
29
+ const [error, setError] = useState<string | null>(null);
30
+ const [errorCode, setErrorCode] = useState<string | null>(null);
31
+ const abortRef = useRef<AbortController | null>(null);
32
+
33
+ const reset = useCallback(() => {
34
+ setStatus("idle");
35
+ setSteps([]);
36
+ setResult({});
37
+ setError(null);
38
+ setErrorCode(null);
39
+ }, []);
40
+
41
+ const publish = useCallback(
42
+ async (args: PublishArgs) => {
43
+ if (!accessToken) {
44
+ setError("You must sign in with Hugging Face before publishing.");
45
+ setStatus("error");
46
+ return;
47
+ }
48
+ reset();
49
+ setStatus("publishing");
50
+
51
+ const controller = new AbortController();
52
+ abortRef.current = controller;
53
+
54
+ const pushStep = (step: string, message: string) =>
55
+ setSteps((prev) => [
56
+ ...prev,
57
+ { step, message, timestamp: Date.now() },
58
+ ]);
59
+
60
+ try {
61
+ const response = await fetch("/api/publish", {
62
+ method: "POST",
63
+ headers: {
64
+ "Content-Type": "application/json",
65
+ Authorization: `Bearer ${accessToken}`,
66
+ },
67
+ body: JSON.stringify({
68
+ repoName: args.repoName,
69
+ private: args.isPrivate,
70
+ overwrite: args.overwrite,
71
+ commitMessage: args.commitMessage,
72
+ files: args.files.map((f) => ({ path: f.path, content: f.content })),
73
+ }),
74
+ signal: controller.signal,
75
+ });
76
+
77
+ if (!response.ok || !response.body) {
78
+ const text = await response.text().catch(() => "");
79
+ throw new Error(
80
+ text || `Publish request failed (${response.status})`,
81
+ );
82
+ }
83
+
84
+ const reader = response.body.getReader();
85
+ const decoder = new TextDecoder();
86
+ let buffer = "";
87
+
88
+ while (true) {
89
+ const { done, value } = await reader.read();
90
+ if (done) break;
91
+ buffer += decoder.decode(value, { stream: true });
92
+
93
+ let eventEnd;
94
+ while ((eventEnd = buffer.indexOf("\n\n")) !== -1) {
95
+ const raw = buffer.slice(0, eventEnd);
96
+ buffer = buffer.slice(eventEnd + 2);
97
+ const lines = raw.split("\n");
98
+ let eventName = "message";
99
+ let dataStr = "";
100
+ for (const line of lines) {
101
+ if (line.startsWith("event:")) eventName = line.slice(6).trim();
102
+ else if (line.startsWith("data:")) dataStr += line.slice(5).trim();
103
+ }
104
+ if (!dataStr) continue;
105
+ let data: Record<string, unknown>;
106
+ try {
107
+ data = JSON.parse(dataStr);
108
+ } catch {
109
+ continue;
110
+ }
111
+
112
+ if (eventName === "progress") {
113
+ pushStep(String(data.step ?? "?"), String(data.message ?? ""));
114
+ } else if (eventName === "done") {
115
+ pushStep("done", String(data.message ?? "Published"));
116
+ setResult({
117
+ repoId: data.repoId as string | undefined,
118
+ spaceUrl: data.spaceUrl as string | undefined,
119
+ });
120
+ setStatus("done");
121
+ } else if (eventName === "error") {
122
+ setError(String(data.message ?? "Publish failed"));
123
+ setErrorCode((data.code as string | undefined) ?? null);
124
+ setStatus("error");
125
+ }
126
+ }
127
+ }
128
+ } catch (e) {
129
+ if ((e as { name?: string })?.name === "AbortError") return;
130
+ setError(e instanceof Error ? e.message : "Unknown error");
131
+ setStatus("error");
132
+ } finally {
133
+ abortRef.current = null;
134
+ }
135
+ },
136
+ [accessToken, reset],
137
+ );
138
+
139
+ const cancel = useCallback(() => {
140
+ abortRef.current?.abort();
141
+ abortRef.current = null;
142
+ setStatus("idle");
143
+ }, []);
144
+
145
+ return {
146
+ status,
147
+ steps,
148
+ result,
149
+ error,
150
+ errorCode,
151
+ publish,
152
+ cancel,
153
+ reset,
154
+ };
155
+ }
frontend/src/hooks/useVirtualFS.ts ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useCallback, useRef, useState } from "react";
2
+
3
+ export interface VirtualFile {
4
+ path: string;
5
+ content: string;
6
+ updatedAt: number;
7
+ }
8
+
9
+ /**
10
+ * In-memory virtual file system for the app the agent is building.
11
+ *
12
+ * The agent mutates it through client-side tool execution (`onToolCall` in
13
+ * `useAgentChat`), the user can also edit files by hand in the Monaco pane.
14
+ * `version` is bumped on every mutation so the preview iframe and the
15
+ * publish flow can react to changes.
16
+ */
17
+ export function useVirtualFS(initial?: VirtualFile[]) {
18
+ const [files, setFiles] = useState<Map<string, VirtualFile>>(() => {
19
+ const map = new Map<string, VirtualFile>();
20
+ (initial ?? []).forEach((f) => map.set(f.path, f));
21
+ return map;
22
+ });
23
+ const [version, setVersion] = useState(0);
24
+ const filesRef = useRef(files);
25
+ filesRef.current = files;
26
+
27
+ const bump = () => setVersion((v) => v + 1);
28
+
29
+ const writeFile = useCallback((path: string, content: string) => {
30
+ setFiles((prev) => {
31
+ const next = new Map(prev);
32
+ next.set(path, { path, content, updatedAt: Date.now() });
33
+ return next;
34
+ });
35
+ bump();
36
+ }, []);
37
+
38
+ const editFile = useCallback(
39
+ (path: string, oldString: string, newString: string): { ok: boolean; error?: string } => {
40
+ const current = filesRef.current.get(path);
41
+ if (!current) {
42
+ return { ok: false, error: `File "${path}" does not exist (call write_file first).` };
43
+ }
44
+ const count = current.content.split(oldString).length - 1;
45
+ if (count === 0) {
46
+ return { ok: false, error: `old_string not found in "${path}".` };
47
+ }
48
+ if (count > 1) {
49
+ return {
50
+ ok: false,
51
+ error: `old_string is not unique in "${path}" (${count} occurrences). Add more surrounding context.`,
52
+ };
53
+ }
54
+ const nextContent = current.content.replace(oldString, newString);
55
+ setFiles((prev) => {
56
+ const next = new Map(prev);
57
+ next.set(path, { path, content: nextContent, updatedAt: Date.now() });
58
+ return next;
59
+ });
60
+ bump();
61
+ return { ok: true };
62
+ },
63
+ [],
64
+ );
65
+
66
+ const deleteFile = useCallback((path: string): { ok: boolean; error?: string } => {
67
+ if (!filesRef.current.has(path)) {
68
+ return { ok: false, error: `File "${path}" does not exist.` };
69
+ }
70
+ setFiles((prev) => {
71
+ const next = new Map(prev);
72
+ next.delete(path);
73
+ return next;
74
+ });
75
+ bump();
76
+ return { ok: true };
77
+ }, []);
78
+
79
+ const readFile = useCallback((path: string): VirtualFile | null => {
80
+ return filesRef.current.get(path) ?? null;
81
+ }, []);
82
+
83
+ const listFiles = useCallback((): VirtualFile[] => {
84
+ return Array.from(filesRef.current.values()).sort((a, b) =>
85
+ a.path.localeCompare(b.path),
86
+ );
87
+ }, []);
88
+
89
+ const clear = useCallback(() => {
90
+ setFiles(new Map());
91
+ bump();
92
+ }, []);
93
+
94
+ return {
95
+ files: Array.from(files.values()).sort((a, b) => a.path.localeCompare(b.path)),
96
+ filesMap: files,
97
+ version,
98
+ writeFile,
99
+ editFile,
100
+ deleteFile,
101
+ readFile,
102
+ listFiles,
103
+ clear,
104
+ getAll: () => Array.from(filesRef.current.values()),
105
+ };
106
+ }
107
+
108
+ export type VirtualFSHandle = ReturnType<typeof useVirtualFS>;
frontend/src/main.tsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import ReactDOM from "react-dom/client";
3
+ import { App } from "./App";
4
+ import "./styles.css";
5
+
6
+ ReactDOM.createRoot(document.getElementById("root")!).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>,
10
+ );
frontend/src/styles.css ADDED
@@ -0,0 +1,798 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --font-sans: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
3
+ --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
4
+
5
+ --bg: #0b0c10;
6
+ --bg-elev-1: #111218;
7
+ --bg-elev-2: #171922;
8
+ --bg-elev-3: #1e2130;
9
+ --border: #242838;
10
+ --border-strong: #30354a;
11
+
12
+ --text: #e7e9f1;
13
+ --text-dim: #a1a5b7;
14
+ --text-faint: #6a6f82;
15
+
16
+ --accent: #ffb020;
17
+ --accent-dim: #bf8418;
18
+ --accent-strong: #ffcd66;
19
+ --danger: #ff5c5c;
20
+ --success: #4ade80;
21
+
22
+ --user-bubble: #1e2130;
23
+ --agent-bubble: transparent;
24
+
25
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
26
+ --shadow-md: 0 4px 20px rgba(0, 0, 0, 0.4);
27
+
28
+ --radius-sm: 6px;
29
+ --radius-md: 10px;
30
+ --radius-lg: 14px;
31
+
32
+ color-scheme: dark;
33
+ }
34
+
35
+ * {
36
+ box-sizing: border-box;
37
+ }
38
+
39
+ html,
40
+ body,
41
+ #root {
42
+ height: 100%;
43
+ margin: 0;
44
+ padding: 0;
45
+ }
46
+
47
+ body {
48
+ background: var(--bg);
49
+ color: var(--text);
50
+ font-family: var(--font-sans);
51
+ font-size: 14px;
52
+ line-height: 1.5;
53
+ -webkit-font-smoothing: antialiased;
54
+ -moz-osx-font-smoothing: grayscale;
55
+ overflow: hidden;
56
+ }
57
+
58
+ button {
59
+ font-family: inherit;
60
+ font-size: inherit;
61
+ color: inherit;
62
+ background: none;
63
+ border: none;
64
+ cursor: pointer;
65
+ }
66
+
67
+ input,
68
+ textarea,
69
+ select {
70
+ font-family: inherit;
71
+ font-size: inherit;
72
+ color: inherit;
73
+ }
74
+
75
+ code,
76
+ pre,
77
+ .mono {
78
+ font-family: var(--font-mono);
79
+ }
80
+
81
+ /* ── App shell ─────────────────────────────────────────────────────────── */
82
+ .app-shell {
83
+ display: flex;
84
+ flex-direction: column;
85
+ height: 100vh;
86
+ min-height: 0;
87
+ }
88
+
89
+ .app-main {
90
+ display: grid;
91
+ grid-template-columns: minmax(360px, 420px) 1fr;
92
+ flex: 1;
93
+ min-height: 0;
94
+ }
95
+
96
+ /* ── Top bar ───────────────────────────────────────────────────────────── */
97
+ .topbar {
98
+ display: flex;
99
+ align-items: center;
100
+ gap: 12px;
101
+ padding: 0 16px;
102
+ height: 52px;
103
+ border-bottom: 1px solid var(--border);
104
+ background: var(--bg-elev-1);
105
+ }
106
+
107
+ .topbar-brand {
108
+ display: flex;
109
+ align-items: center;
110
+ gap: 10px;
111
+ font-weight: 600;
112
+ letter-spacing: 0.01em;
113
+ }
114
+
115
+ .topbar-logo {
116
+ display: inline-flex;
117
+ align-items: center;
118
+ justify-content: center;
119
+ width: 28px;
120
+ height: 28px;
121
+ border-radius: 6px;
122
+ background: var(--accent);
123
+ color: #111;
124
+ font-size: 18px;
125
+ font-weight: 700;
126
+ }
127
+
128
+ .topbar-title {
129
+ display: flex;
130
+ flex-direction: column;
131
+ line-height: 1.15;
132
+ }
133
+
134
+ .topbar-title-main {
135
+ font-weight: 600;
136
+ }
137
+
138
+ .topbar-title-sub {
139
+ font-size: 11px;
140
+ color: var(--text-faint);
141
+ letter-spacing: 0.02em;
142
+ text-transform: uppercase;
143
+ }
144
+
145
+ .topbar-spacer {
146
+ flex: 1;
147
+ }
148
+
149
+ .topbar-model {
150
+ display: flex;
151
+ align-items: center;
152
+ gap: 8px;
153
+ font-size: 12px;
154
+ color: var(--text-dim);
155
+ }
156
+
157
+ .topbar-model select {
158
+ background: var(--bg-elev-2);
159
+ border: 1px solid var(--border);
160
+ border-radius: var(--radius-sm);
161
+ padding: 4px 8px;
162
+ font-size: 12px;
163
+ cursor: pointer;
164
+ }
165
+
166
+ .topbar-user {
167
+ display: flex;
168
+ align-items: center;
169
+ gap: 8px;
170
+ padding: 4px 10px 4px 4px;
171
+ border: 1px solid var(--border);
172
+ border-radius: 999px;
173
+ background: var(--bg-elev-2);
174
+ }
175
+
176
+ .topbar-user img,
177
+ .topbar-user .avatar-fallback {
178
+ width: 24px;
179
+ height: 24px;
180
+ border-radius: 50%;
181
+ object-fit: cover;
182
+ background: var(--bg-elev-3);
183
+ display: inline-flex;
184
+ align-items: center;
185
+ justify-content: center;
186
+ font-size: 11px;
187
+ font-weight: 600;
188
+ }
189
+
190
+ .topbar-user-name {
191
+ font-size: 12px;
192
+ color: var(--text);
193
+ max-width: 140px;
194
+ overflow: hidden;
195
+ text-overflow: ellipsis;
196
+ white-space: nowrap;
197
+ }
198
+
199
+ .topbar-user-menu {
200
+ margin-left: 4px;
201
+ color: var(--text-faint);
202
+ font-size: 11px;
203
+ }
204
+
205
+ .btn {
206
+ display: inline-flex;
207
+ align-items: center;
208
+ gap: 6px;
209
+ padding: 6px 12px;
210
+ border-radius: var(--radius-sm);
211
+ background: var(--bg-elev-2);
212
+ border: 1px solid var(--border);
213
+ color: var(--text);
214
+ font-size: 12px;
215
+ font-weight: 500;
216
+ transition: all 0.15s ease;
217
+ }
218
+
219
+ .btn:hover {
220
+ background: var(--bg-elev-3);
221
+ border-color: var(--border-strong);
222
+ }
223
+
224
+ .btn:disabled {
225
+ opacity: 0.5;
226
+ cursor: not-allowed;
227
+ }
228
+
229
+ .btn-primary {
230
+ background: var(--accent);
231
+ border-color: var(--accent);
232
+ color: #111;
233
+ }
234
+
235
+ .btn-primary:hover {
236
+ background: var(--accent-strong);
237
+ border-color: var(--accent-strong);
238
+ }
239
+
240
+ .btn-ghost {
241
+ background: transparent;
242
+ border: 1px solid transparent;
243
+ color: var(--text-dim);
244
+ }
245
+
246
+ .btn-ghost:hover {
247
+ background: var(--bg-elev-2);
248
+ color: var(--text);
249
+ }
250
+
251
+ .btn-danger {
252
+ background: transparent;
253
+ border: 1px solid transparent;
254
+ color: var(--danger);
255
+ }
256
+
257
+ /* ── Sign-in screen ────────────────────────────────────────────────────── */
258
+ .signin-screen {
259
+ display: flex;
260
+ flex-direction: column;
261
+ align-items: center;
262
+ justify-content: center;
263
+ height: 100%;
264
+ text-align: center;
265
+ padding: 40px 24px;
266
+ }
267
+
268
+ .signin-emoji {
269
+ font-size: 56px;
270
+ margin-bottom: 16px;
271
+ }
272
+
273
+ .signin-title {
274
+ font-size: 28px;
275
+ font-weight: 700;
276
+ margin: 0 0 8px;
277
+ }
278
+
279
+ .signin-subtitle {
280
+ font-size: 14px;
281
+ color: var(--text-dim);
282
+ max-width: 480px;
283
+ margin: 0 0 28px;
284
+ }
285
+
286
+ .signin-button {
287
+ display: inline-flex;
288
+ align-items: center;
289
+ gap: 10px;
290
+ padding: 12px 22px;
291
+ border-radius: 999px;
292
+ background: var(--accent);
293
+ color: #111;
294
+ font-weight: 600;
295
+ border: none;
296
+ font-size: 14px;
297
+ transition: all 0.15s ease;
298
+ }
299
+
300
+ .signin-button:hover {
301
+ background: var(--accent-strong);
302
+ }
303
+
304
+ .signin-hint {
305
+ margin-top: 16px;
306
+ font-size: 12px;
307
+ color: var(--text-faint);
308
+ max-width: 480px;
309
+ }
310
+
311
+ .signin-error {
312
+ margin-top: 20px;
313
+ padding: 10px 14px;
314
+ background: rgba(255, 92, 92, 0.1);
315
+ border: 1px solid rgba(255, 92, 92, 0.4);
316
+ border-radius: var(--radius-sm);
317
+ color: var(--danger);
318
+ font-size: 12px;
319
+ max-width: 520px;
320
+ }
321
+
322
+ /* ── Chat panel ────────────────────────────────────────────────────────── */
323
+ .chat-panel {
324
+ display: flex;
325
+ flex-direction: column;
326
+ min-height: 0;
327
+ border-right: 1px solid var(--border);
328
+ background: var(--bg-elev-1);
329
+ }
330
+
331
+ .chat-messages {
332
+ flex: 1;
333
+ overflow-y: auto;
334
+ padding: 16px;
335
+ display: flex;
336
+ flex-direction: column;
337
+ gap: 14px;
338
+ }
339
+
340
+ .chat-empty {
341
+ padding: 20px;
342
+ color: var(--text-faint);
343
+ font-size: 13px;
344
+ line-height: 1.6;
345
+ }
346
+
347
+ .chat-empty-examples {
348
+ margin-top: 12px;
349
+ display: flex;
350
+ flex-direction: column;
351
+ gap: 6px;
352
+ }
353
+
354
+ .chat-empty-example {
355
+ text-align: left;
356
+ padding: 8px 12px;
357
+ background: var(--bg-elev-2);
358
+ border: 1px solid var(--border);
359
+ border-radius: var(--radius-sm);
360
+ color: var(--text-dim);
361
+ cursor: pointer;
362
+ font-size: 12px;
363
+ transition: all 0.15s ease;
364
+ }
365
+
366
+ .chat-empty-example:hover {
367
+ background: var(--bg-elev-3);
368
+ color: var(--text);
369
+ }
370
+
371
+ .bubble-user {
372
+ align-self: flex-end;
373
+ max-width: 85%;
374
+ padding: 10px 14px;
375
+ background: var(--user-bubble);
376
+ border: 1px solid var(--border);
377
+ border-radius: var(--radius-md);
378
+ white-space: pre-wrap;
379
+ word-wrap: break-word;
380
+ }
381
+
382
+ .bubble-agent {
383
+ align-self: stretch;
384
+ display: flex;
385
+ flex-direction: column;
386
+ gap: 8px;
387
+ color: var(--text);
388
+ }
389
+
390
+ .bubble-agent-text {
391
+ white-space: pre-wrap;
392
+ word-wrap: break-word;
393
+ }
394
+
395
+ .bubble-tool {
396
+ align-self: stretch;
397
+ padding: 6px 10px;
398
+ background: var(--bg-elev-2);
399
+ border: 1px solid var(--border);
400
+ border-radius: var(--radius-sm);
401
+ font-family: var(--font-mono);
402
+ font-size: 11px;
403
+ color: var(--text-dim);
404
+ }
405
+
406
+ .bubble-tool.status-done {
407
+ border-color: rgba(74, 222, 128, 0.3);
408
+ background: rgba(74, 222, 128, 0.05);
409
+ }
410
+
411
+ .bubble-tool.status-error {
412
+ border-color: rgba(255, 92, 92, 0.4);
413
+ background: rgba(255, 92, 92, 0.08);
414
+ color: var(--danger);
415
+ }
416
+
417
+ .bubble-tool-header {
418
+ display: flex;
419
+ align-items: center;
420
+ gap: 6px;
421
+ font-weight: 500;
422
+ }
423
+
424
+ .bubble-tool-detail {
425
+ margin-top: 4px;
426
+ color: var(--text-faint);
427
+ font-size: 10px;
428
+ white-space: pre-wrap;
429
+ word-break: break-all;
430
+ }
431
+
432
+ .chat-composer {
433
+ border-top: 1px solid var(--border);
434
+ padding: 12px 16px;
435
+ background: var(--bg-elev-1);
436
+ }
437
+
438
+ .chat-composer-field {
439
+ display: flex;
440
+ align-items: flex-end;
441
+ gap: 8px;
442
+ padding: 8px;
443
+ background: var(--bg-elev-2);
444
+ border: 1px solid var(--border);
445
+ border-radius: var(--radius-md);
446
+ transition: border-color 0.15s ease;
447
+ }
448
+
449
+ .chat-composer-field:focus-within {
450
+ border-color: var(--accent);
451
+ }
452
+
453
+ .chat-composer textarea {
454
+ flex: 1;
455
+ min-height: 20px;
456
+ max-height: 160px;
457
+ resize: none;
458
+ background: transparent;
459
+ border: none;
460
+ outline: none;
461
+ padding: 4px 6px;
462
+ line-height: 1.4;
463
+ color: var(--text);
464
+ }
465
+
466
+ .chat-composer textarea::placeholder {
467
+ color: var(--text-faint);
468
+ }
469
+
470
+ .chat-send {
471
+ display: inline-flex;
472
+ align-items: center;
473
+ justify-content: center;
474
+ width: 32px;
475
+ height: 32px;
476
+ border-radius: var(--radius-sm);
477
+ background: var(--accent);
478
+ color: #111;
479
+ border: none;
480
+ font-size: 16px;
481
+ transition: all 0.15s ease;
482
+ }
483
+
484
+ .chat-send:hover:not(:disabled) {
485
+ background: var(--accent-strong);
486
+ }
487
+
488
+ .chat-send:disabled {
489
+ opacity: 0.4;
490
+ cursor: not-allowed;
491
+ }
492
+
493
+ .chat-status {
494
+ margin-top: 6px;
495
+ font-size: 11px;
496
+ color: var(--text-faint);
497
+ display: flex;
498
+ align-items: center;
499
+ gap: 8px;
500
+ }
501
+
502
+ /* ── Right pane ────────────────────────────────────────────────────────── */
503
+ .right-pane {
504
+ display: flex;
505
+ flex-direction: column;
506
+ min-height: 0;
507
+ background: var(--bg);
508
+ }
509
+
510
+ .tab-bar {
511
+ display: flex;
512
+ align-items: center;
513
+ gap: 4px;
514
+ padding: 0 8px;
515
+ height: 40px;
516
+ border-bottom: 1px solid var(--border);
517
+ background: var(--bg-elev-1);
518
+ }
519
+
520
+ .tab {
521
+ padding: 6px 14px;
522
+ color: var(--text-dim);
523
+ font-size: 12px;
524
+ font-weight: 500;
525
+ border-radius: var(--radius-sm);
526
+ border: 1px solid transparent;
527
+ cursor: pointer;
528
+ }
529
+
530
+ .tab.active {
531
+ color: var(--text);
532
+ background: var(--bg-elev-2);
533
+ border-color: var(--border);
534
+ }
535
+
536
+ .tab-spacer {
537
+ flex: 1;
538
+ }
539
+
540
+ .tab-actions {
541
+ display: flex;
542
+ align-items: center;
543
+ gap: 6px;
544
+ }
545
+
546
+ .pane-content {
547
+ flex: 1;
548
+ min-height: 0;
549
+ display: flex;
550
+ flex-direction: column;
551
+ }
552
+
553
+ .pane-content.hidden {
554
+ display: none;
555
+ }
556
+
557
+ /* ── Code editor ───────────────────────────────────────────────────────── */
558
+ .code-layout {
559
+ display: grid;
560
+ grid-template-columns: 220px 1fr;
561
+ height: 100%;
562
+ min-height: 0;
563
+ }
564
+
565
+ .file-tree {
566
+ border-right: 1px solid var(--border);
567
+ background: var(--bg-elev-1);
568
+ overflow-y: auto;
569
+ padding: 8px 0;
570
+ }
571
+
572
+ .file-tree-empty {
573
+ padding: 16px;
574
+ color: var(--text-faint);
575
+ font-size: 12px;
576
+ }
577
+
578
+ .file-tree-item {
579
+ display: flex;
580
+ align-items: center;
581
+ gap: 6px;
582
+ padding: 5px 12px;
583
+ font-family: var(--font-mono);
584
+ font-size: 12px;
585
+ color: var(--text-dim);
586
+ cursor: pointer;
587
+ border-left: 2px solid transparent;
588
+ }
589
+
590
+ .file-tree-item:hover {
591
+ background: var(--bg-elev-2);
592
+ color: var(--text);
593
+ }
594
+
595
+ .file-tree-item.active {
596
+ background: var(--bg-elev-2);
597
+ color: var(--text);
598
+ border-left-color: var(--accent);
599
+ }
600
+
601
+ .file-tree-size {
602
+ margin-left: auto;
603
+ color: var(--text-faint);
604
+ font-size: 10px;
605
+ }
606
+
607
+ .editor-area {
608
+ min-height: 0;
609
+ display: flex;
610
+ flex-direction: column;
611
+ }
612
+
613
+ .editor-placeholder {
614
+ margin: auto;
615
+ color: var(--text-faint);
616
+ font-size: 13px;
617
+ text-align: center;
618
+ padding: 40px;
619
+ }
620
+
621
+ /* ── Preview ───────────────────────────────────────────────────────────── */
622
+ .preview-frame {
623
+ flex: 1;
624
+ min-height: 0;
625
+ background: white;
626
+ border: none;
627
+ }
628
+
629
+ .preview-empty {
630
+ margin: auto;
631
+ color: var(--text-faint);
632
+ font-size: 13px;
633
+ text-align: center;
634
+ padding: 40px;
635
+ }
636
+
637
+ /* ── Dialog ────────────────────────────────────────────────────────────── */
638
+ .dialog-backdrop {
639
+ position: fixed;
640
+ inset: 0;
641
+ background: rgba(0, 0, 0, 0.6);
642
+ display: flex;
643
+ align-items: center;
644
+ justify-content: center;
645
+ z-index: 100;
646
+ padding: 24px;
647
+ }
648
+
649
+ .dialog {
650
+ width: 100%;
651
+ max-width: 480px;
652
+ background: var(--bg-elev-1);
653
+ border: 1px solid var(--border);
654
+ border-radius: var(--radius-lg);
655
+ box-shadow: var(--shadow-md);
656
+ overflow: hidden;
657
+ }
658
+
659
+ .dialog-header {
660
+ display: flex;
661
+ align-items: center;
662
+ justify-content: space-between;
663
+ padding: 14px 18px;
664
+ border-bottom: 1px solid var(--border);
665
+ font-weight: 600;
666
+ }
667
+
668
+ .dialog-body {
669
+ padding: 18px;
670
+ display: flex;
671
+ flex-direction: column;
672
+ gap: 14px;
673
+ }
674
+
675
+ .dialog-footer {
676
+ display: flex;
677
+ justify-content: flex-end;
678
+ gap: 8px;
679
+ padding: 14px 18px;
680
+ border-top: 1px solid var(--border);
681
+ background: var(--bg-elev-2);
682
+ }
683
+
684
+ .field {
685
+ display: flex;
686
+ flex-direction: column;
687
+ gap: 6px;
688
+ }
689
+
690
+ .field label {
691
+ font-size: 12px;
692
+ color: var(--text-dim);
693
+ font-weight: 500;
694
+ }
695
+
696
+ .field input[type="text"],
697
+ .field input[type="password"],
698
+ .field textarea {
699
+ padding: 8px 10px;
700
+ background: var(--bg-elev-2);
701
+ border: 1px solid var(--border);
702
+ border-radius: var(--radius-sm);
703
+ color: var(--text);
704
+ }
705
+
706
+ .field input:focus,
707
+ .field textarea:focus {
708
+ outline: none;
709
+ border-color: var(--accent);
710
+ }
711
+
712
+ .field-hint {
713
+ font-size: 11px;
714
+ color: var(--text-faint);
715
+ }
716
+
717
+ .field-row {
718
+ display: flex;
719
+ align-items: center;
720
+ gap: 8px;
721
+ }
722
+
723
+ .publish-log {
724
+ display: flex;
725
+ flex-direction: column;
726
+ gap: 6px;
727
+ padding: 12px;
728
+ background: var(--bg-elev-2);
729
+ border: 1px solid var(--border);
730
+ border-radius: var(--radius-sm);
731
+ font-family: var(--font-mono);
732
+ font-size: 11px;
733
+ max-height: 180px;
734
+ overflow-y: auto;
735
+ }
736
+
737
+ .publish-log-step {
738
+ color: var(--text-dim);
739
+ }
740
+
741
+ .publish-log-step.done {
742
+ color: var(--success);
743
+ }
744
+
745
+ .publish-log-step.error {
746
+ color: var(--danger);
747
+ }
748
+
749
+ .publish-success {
750
+ padding: 10px 12px;
751
+ background: rgba(74, 222, 128, 0.08);
752
+ border: 1px solid rgba(74, 222, 128, 0.3);
753
+ border-radius: var(--radius-sm);
754
+ color: var(--success);
755
+ font-size: 12px;
756
+ display: flex;
757
+ flex-direction: column;
758
+ gap: 4px;
759
+ }
760
+
761
+ .publish-success a {
762
+ color: inherit;
763
+ font-weight: 600;
764
+ }
765
+
766
+ .publish-error {
767
+ padding: 10px 12px;
768
+ background: rgba(255, 92, 92, 0.1);
769
+ border: 1px solid rgba(255, 92, 92, 0.3);
770
+ border-radius: var(--radius-sm);
771
+ color: var(--danger);
772
+ font-size: 12px;
773
+ }
774
+
775
+ /* ── Utility ───────────────────────────────────────────────────────────── */
776
+ .dot {
777
+ display: inline-block;
778
+ width: 6px;
779
+ height: 6px;
780
+ border-radius: 50%;
781
+ background: currentColor;
782
+ }
783
+
784
+ .spinner {
785
+ display: inline-block;
786
+ width: 12px;
787
+ height: 12px;
788
+ border: 2px solid var(--text-faint);
789
+ border-top-color: var(--accent);
790
+ border-radius: 50%;
791
+ animation: spin 0.8s linear infinite;
792
+ }
793
+
794
+ @keyframes spin {
795
+ to {
796
+ transform: rotate(360deg);
797
+ }
798
+ }
frontend/src/vite-env.d.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /// <reference types="vite/client" />
2
+
3
+ interface ImportMetaEnv {
4
+ readonly VITE_HF_OAUTH_CLIENT_ID?: string;
5
+ }
6
+
7
+ interface ImportMeta {
8
+ readonly env: ImportMetaEnv;
9
+ }
10
+
11
+ interface Window {
12
+ huggingface?: {
13
+ variables?: {
14
+ OAUTH_CLIENT_ID?: string;
15
+ OAUTH_SCOPES?: string;
16
+ SPACE_HOST?: string;
17
+ SPACE_ID?: string;
18
+ };
19
+ };
20
+ }
frontend/tsconfig.app.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "bundler",
9
+ "allowImportingTsExtensions": false,
10
+ "isolatedModules": true,
11
+ "moduleDetection": "force",
12
+ "noEmit": true,
13
+ "jsx": "react-jsx",
14
+ "strict": true,
15
+ "noUnusedLocals": false,
16
+ "noUnusedParameters": false,
17
+ "noFallthroughCasesInSwitch": true,
18
+ "resolveJsonModule": true,
19
+ "types": ["vite/client"]
20
+ },
21
+ "include": ["src"]
22
+ }
frontend/tsconfig.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
frontend/tsconfig.node.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2023"],
5
+ "module": "ESNext",
6
+ "skipLibCheck": true,
7
+ "moduleResolution": "bundler",
8
+ "allowImportingTsExtensions": true,
9
+ "isolatedModules": true,
10
+ "moduleDetection": "force",
11
+ "noEmit": true,
12
+ "strict": true
13
+ },
14
+ "include": ["vite.config.ts"]
15
+ }
frontend/vite.config.ts ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+
4
+ /**
5
+ * Dev: Vite serves the React app on :5173 and proxies `/api/*` to the
6
+ * Hono backend on :7860. In production the same Hono server serves the
7
+ * static build from `frontend/dist` (see `backend/src/server.ts`).
8
+ */
9
+ export default defineConfig({
10
+ plugins: [react()],
11
+ server: {
12
+ port: 5173,
13
+ proxy: {
14
+ "/api": {
15
+ target: "http://localhost:7860",
16
+ changeOrigin: true,
17
+ },
18
+ },
19
+ },
20
+ build: {
21
+ outDir: "dist",
22
+ sourcemap: true,
23
+ },
24
+ });