@vibes.diy/prompts 14.3.35 → 14.3.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,159 @@
1
+ # Babylon.js WebXR — the first draft: a lit scene with a door into VR
2
+
3
+ A WebXR app's first draft is the page around the scene (title, copy, a card
4
+ holding the canvas) with one real Babylon.js scene running inside it: a lit
5
+ box turning above a ground plane. The VR half is shown honestly: an "Enter
6
+ VR" Button that lights up when the browser reports an immersive headset and
7
+ opens the session through `scene.createDefaultXRExperienceAsync`, and reads
8
+ "VR needs a headset browser" everywhere else. Hit testing, anchors, hand
9
+ tracking, particles and shaders are the next pass. What the draft owes that
10
+ pass is the skeleton: the engine and scene are built once in one effect with a
11
+ full cleanup, the ground mesh the XR floor needs already exists, and the scene
12
+ lives in a ref so the next pass adds features to this file rather than
13
+ rewriting it.
14
+
15
+ ### `App.jsx`
16
+ ```jsx
17
+ import React, { useEffect, useRef, useState } from "react";
18
+ import { useFireproof } from "use-fireproof";
19
+ import * as BABYLON from "@babylonjs/core";
20
+ import { Card, CardHeader, CardTitle, CardContent, Button, Badge } from "@vibes.diy/look";
21
+
22
+ export default function App() {
23
+ const { useLiveQuery } = useFireproof("floatingRoom");
24
+ const { docs: rooms } = useLiveQuery("type", { key: "room" });
25
+ const room = rooms[0];
26
+ const canvasRef = useRef(null);
27
+ const sceneRef = useRef(null);
28
+ const [vrReady, setVrReady] = useState(false);
29
+ const [inVr, setInVr] = useState(false);
30
+
31
+ // The engine, built once: scene, camera, lights, ground, one box, render loop, resize, cleanup.
32
+ useEffect(() => {
33
+ const canvas = canvasRef.current;
34
+ const engine = new BABYLON.Engine(canvas, true, { preserveDrawingBuffer: true, stencil: true });
35
+ const scene = new BABYLON.Scene(engine);
36
+ scene.clearColor = new BABYLON.Color4(0.06, 0.08, 0.14, 1);
37
+
38
+ const camera = new BABYLON.FreeCamera("cam", new BABYLON.Vector3(0, 1.7, -5), scene);
39
+ camera.setTarget(new BABYLON.Vector3(0, 1, 0));
40
+ camera.attachControl(canvas, true);
41
+
42
+ const ambient = new BABYLON.HemisphericLight("ambient", new BABYLON.Vector3(0, 1, 0), scene);
43
+ ambient.intensity = 0.5;
44
+ const sun = new BABYLON.DirectionalLight("sun", new BABYLON.Vector3(-1, -2, -1), scene);
45
+ sun.position = new BABYLON.Vector3(5, 10, 5);
46
+ sun.intensity = 0.8;
47
+
48
+ const ground = BABYLON.MeshBuilder.CreateGround("ground", { width: 12, height: 12 }, scene);
49
+ const groundMat = new BABYLON.StandardMaterial("groundMat", scene);
50
+ groundMat.diffuseColor = new BABYLON.Color3(0.22, 0.3, 0.26);
51
+ ground.material = groundMat;
52
+
53
+ const box = BABYLON.MeshBuilder.CreateBox("box", { size: 1 }, scene);
54
+ box.position.y = 1.2;
55
+ const boxMat = new BABYLON.StandardMaterial("boxMat", scene);
56
+ boxMat.diffuseColor = new BABYLON.Color3(0.95, 0.55, 0.25);
57
+ box.material = boxMat;
58
+
59
+ scene.onBeforeRenderObservable.add(() => {
60
+ const dt = engine.getDeltaTime() / 1000;
61
+ box.rotation.y += dt * 0.8;
62
+ box.rotation.x += dt * 0.3;
63
+ });
64
+
65
+ sceneRef.current = { scene, ground };
66
+ engine.runRenderLoop(() => scene.render());
67
+ const onResize = () => engine.resize();
68
+ window.addEventListener("resize", onResize);
69
+
70
+ // Ask the browser once whether an immersive headset session is on offer.
71
+ let alive = true;
72
+ if (navigator.xr) {
73
+ navigator.xr
74
+ .isSessionSupported("immersive-vr")
75
+ .then((ok) => { if (alive) setVrReady(ok); })
76
+ .catch(() => { if (alive) setVrReady(false); });
77
+ }
78
+
79
+ return () => {
80
+ alive = false;
81
+ sceneRef.current = null;
82
+ window.removeEventListener("resize", onResize);
83
+ engine.stopRenderLoop();
84
+ scene.dispose();
85
+ engine.dispose();
86
+ };
87
+ }, []);
88
+
89
+ const enterVr = async () => {
90
+ const current = sceneRef.current;
91
+ if (!current) return;
92
+ const xr = await current.scene.createDefaultXRExperienceAsync({ floorMeshes: [current.ground], disableDefaultUI: true });
93
+ xr.baseExperience.onStateChangedObservable.add((state) => setInVr(state === BABYLON.WebXRState.IN_XR));
94
+ await xr.baseExperience.enterXRAsync("immersive-vr", "local-floor");
95
+ };
96
+
97
+ return (
98
+ <main className="min-h-screen bg-background text-foreground font-sans p-6">
99
+ <h1 className="text-3xl font-bold">{room?.name ?? "Floating Room"}</h1>
100
+ <p className="mt-1 text-muted-foreground">{room?.tagline ?? "A quiet room with one bright box. Drag to look around, or step inside with a headset."}</p>
101
+
102
+ <Card className="mt-6">
103
+ <CardHeader>
104
+ <CardTitle>The room</CardTitle>
105
+ </CardHeader>
106
+ <CardContent>
107
+ <canvas ref={canvasRef} className="block aspect-video w-full rounded-lg outline-none touch-none" />
108
+ {/* TODO(next pass): hit testing, anchors, hand tracking, particles and shaders inside this same scene */}
109
+ <div className="mt-4 flex flex-wrap items-center gap-3">
110
+ {vrReady ? (
111
+ <Button onClick={enterVr}>Enter VR</Button>
112
+ ) : (
113
+ <Button disabled variant="outline">VR needs a headset browser</Button>
114
+ )}
115
+ <Badge variant="secondary">{inVr ? "In VR" : "On screen"}</Badge>
116
+ </div>
117
+ </CardContent>
118
+ </Card>
119
+ </main>
120
+ );
121
+ }
122
+ ```
123
+
124
+ ### `seed.json`
125
+ ```json
126
+ {
127
+ "floatingRoom": [
128
+ { "key": "room", "type": "room", "name": "Floating Room", "tagline": "A quiet room with one bright box. Drag to look around, or step inside with a headset." }
129
+ ]
130
+ }
131
+ ```
132
+
133
+ Draft note: the next pass adds hit testing, anchors, hand tracking, particles and shaders to the same scene.
134
+
135
+ ## What stays the same in a WebXR draft
136
+
137
+ - **The import is `import * as BABYLON from "@babylonjs/core"`**, never the
138
+ legacy `babylonjs` package, and every engine class is reached through the
139
+ `BABYLON.` namespace.
140
+ - **The engine is built once** in `useEffect(() => {...}, [])`: `Engine`,
141
+ `Scene`, camera, lights, ground, mesh and `engine.runRenderLoop` all inside
142
+ it, per-frame motion in `scene.onBeforeRenderObservable`, and the scene kept
143
+ in a ref for the few calls React makes in.
144
+ - **The cleanup is complete**: it removes the resize listener, stops the render
145
+ loop, disposes the scene and disposes the engine.
146
+ - **Sizing**: the canvas is `block aspect-video w-full` inside a Card, and
147
+ `window` resize calls `engine.resize()`.
148
+ - **The hello is one lit box on a ground plane** under a hemispheric light and
149
+ a directional light, turning on the engine's delta time. The ground doubles
150
+ as the XR floor mesh.
151
+ - **VR is shown, never hidden**: `navigator.xr.isSessionSupported("immersive-vr")`
152
+ is checked in the effect and stored in state; a supported browser gets an
153
+ "Enter VR" Button that awaits `createDefaultXRExperienceAsync` with
154
+ `disableDefaultUI` and then `enterXRAsync("immersive-vr", "local-floor")`,
155
+ and every other browser sees a disabled Button reading "VR needs a headset
156
+ browser".
157
+ - **What waits for the next pass** is named in the `TODO(next pass)` comment
158
+ and the draft note: hit testing, anchors, hand tracking, particles, shaders.
159
+ The draft draws none of it.
package/llms/webxr.js CHANGED
@@ -19,6 +19,7 @@ export const webxrConfig = {
19
19
  "quest",
20
20
  "vision pro",
21
21
  ],
22
+ draftVariant: true,
22
23
  importModule: "@babylonjs/core",
23
24
  importName: "BABYLON",
24
25
  importType: "namespace",
package/llms/webxr.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"webxr.js","sourceRoot":"","sources":["../../jsr/llms/webxr.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,WAAW,GAAc;IACpC,IAAI,EAAE,OAAO;IACb,KAAK,EAAE,kBAAkB;IACzB,WAAW,EACT,gFAAgF;QAChF,4FAA4F;QAC5F,kFAAkF;QAClF,sFAAsF;QACtF,uDAAuD;IACzD,IAAI,EAAE;QACJ,OAAO;QACP,IAAI;QACJ,IAAI;QACJ,iBAAiB;QACjB,mBAAmB;QACnB,SAAS;QACT,WAAW;QACX,eAAe;QACf,SAAS;QACT,OAAO;QACP,YAAY;KACb;IACD,YAAY,EAAE,iBAAiB;IAC/B,UAAU,EAAE,SAAS;IACrB,UAAU,EAAE,WAAW;CACxB,CAAC"}
1
+ {"version":3,"file":"webxr.js","sourceRoot":"","sources":["../../jsr/llms/webxr.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,WAAW,GAAc;IACpC,IAAI,EAAE,OAAO;IACb,KAAK,EAAE,kBAAkB;IACzB,WAAW,EACT,gFAAgF;QAChF,4FAA4F;QAC5F,kFAAkF;QAClF,sFAAsF;QACtF,uDAAuD;IACzD,IAAI,EAAE;QACJ,OAAO;QACP,IAAI;QACJ,IAAI;QACJ,iBAAiB;QACjB,mBAAmB;QACnB,SAAS;QACT,WAAW;QACX,eAAe;QACf,SAAS;QACT,OAAO;QACP,YAAY;KACb;IAGD,YAAY,EAAE,IAAI;IAClB,YAAY,EAAE,iBAAiB;IAC/B,UAAU,EAAE,SAAS;IACrB,UAAU,EAAE,WAAW;CACxB,CAAC"}
@@ -0,0 +1,183 @@
1
+ # YouTube channel stats — the first draft: the whole dashboard on sample rows
2
+
3
+ A YouTube dashboard's first draft is the finished page, painted from seeded
4
+ documents shaped exactly like the rows the real backend returns: the channel
5
+ header with its name and handle, three stat tiles (subscribers, views, videos),
6
+ the recent uploads with their published date and view count, and the setup card
7
+ that walks the owner through getting a YouTube Data API v3 key. A Badge on the
8
+ header reads "sample data until the key is set", so the capability is on screen
9
+ and honestly labeled from the first paint.
10
+
11
+ The next pass writes `backend.js`: it reads `YOUTUBE_API_KEY` from the vibe's
12
+ secrets, resolves the channel from its handle, reads the uploads playlist, and
13
+ saves a daily snapshot. What the draft owes that pass is the row shape: the
14
+ `channel` document carries the same fields `loadChannel` returns (`channelId`,
15
+ `title`, `handle`, `subscribers`, `subscribersHidden`, `views`, `videoCount`)
16
+ and each `video` document carries the same fields `loadVideos` returns (`id`,
17
+ `title`, `published`, `views`, `likes`, `comments`), so the next pass swaps the
18
+ source and keeps every component. The draft writes no `backend.js`, makes no
19
+ network call, reads no secret, and never asks a visitor for a token: the key is
20
+ the owner's, set once in the vibe's settings.
21
+
22
+ ### `App.jsx`
23
+ ```jsx
24
+ import React, { useState } from "react";
25
+ import { useFireproof } from "use-fireproof";
26
+ import { Button, Card, CardHeader, CardTitle, CardContent, Badge } from "@vibes.diy/look";
27
+
28
+ // The owner's one-time trip to Google, carried in the app because the owner is the one who reads it.
29
+ const SETUP_STEPS = [
30
+ "Open the Google Cloud console and create or select a project.",
31
+ "In APIs & Services, open Library and enable YouTube Data API v3.",
32
+ "In Credentials, choose Create credentials, then API key.",
33
+ "Restrict the key to YouTube Data API v3.",
34
+ "Set it on this vibe as YOUTUBE_API_KEY in the vibe's settings page.",
35
+ ];
36
+ const SECRET_NAME = "YOUTUBE_API_KEY";
37
+
38
+ const formatCount = (n) => (n == null ? "hidden" : Number(n).toLocaleString());
39
+ const formatDay = (iso) => new Date(iso).toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" });
40
+
41
+ function StatCard({ label, value, caption }) {
42
+ return (
43
+ <Card>
44
+ <CardHeader>
45
+ <p className="text-muted-foreground">{label}</p>
46
+ </CardHeader>
47
+ <CardContent>
48
+ <p className="text-3xl font-bold">{value}</p>
49
+ {caption && <p className="mt-1 text-muted-foreground">{caption}</p>}
50
+ </CardContent>
51
+ </Card>
52
+ );
53
+ }
54
+
55
+ function VideoCard({ video }) {
56
+ return (
57
+ <Card>
58
+ <CardContent>
59
+ <p className="font-semibold">{video.title}</p>
60
+ <div className="mt-2 flex flex-wrap items-center gap-3 text-muted-foreground">
61
+ <span>{formatDay(video.published)}</span>
62
+ <span className="font-medium text-foreground">{formatCount(video.views)} views</span>
63
+ <span>{formatCount(video.likes)} likes</span>
64
+ <span>{formatCount(video.comments)} comments</span>
65
+ </div>
66
+ </CardContent>
67
+ </Card>
68
+ );
69
+ }
70
+
71
+ export default function App() {
72
+ const { useLiveQuery } = useFireproof("youtubeDashboard");
73
+ const { docs: channels } = useLiveQuery("type", { key: "channel" });
74
+ const { docs: videos } = useLiveQuery("type", { key: "video" });
75
+ const [sortBy, setSortBy] = useState("newest");
76
+ const channel = channels[0];
77
+
78
+ const sorted = videos
79
+ .slice()
80
+ .sort((a, b) => (sortBy === "views" ? b.views - a.views : b.published.localeCompare(a.published)));
81
+
82
+ return (
83
+ <main className="min-h-screen bg-background text-foreground font-sans p-6">
84
+ <div className="flex flex-wrap items-center gap-3">
85
+ <h1 className="text-3xl font-bold">{channel?.title ?? "Your channel"}</h1>
86
+ <Badge variant="secondary">sample data until the key is set</Badge>
87
+ </div>
88
+ <p className="mt-1 text-muted-foreground">{channel?.handle} · how the channel is doing, at a glance</p>
89
+
90
+ <div className="mt-6 grid gap-4 sm:grid-cols-3">
91
+ <StatCard
92
+ label="Subscribers"
93
+ value={channel?.subscribersHidden ? "hidden by the channel" : formatCount(channel?.subscribers)}
94
+ caption="YouTube rounds public subscriber counts."
95
+ />
96
+ <StatCard label="Views" value={formatCount(channel?.views)} />
97
+ <StatCard label="Videos" value={formatCount(channel?.videoCount)} />
98
+ </div>
99
+
100
+ <div className="mt-8 flex flex-wrap items-center justify-between gap-3">
101
+ <h2 className="text-xl font-semibold">Recent uploads</h2>
102
+ <div className="flex gap-2">
103
+ <Button size="sm" variant={sortBy === "newest" ? "default" : "outline"} onClick={() => setSortBy("newest")}>Newest</Button>
104
+ <Button size="sm" variant={sortBy === "views" ? "default" : "outline"} onClick={() => setSortBy("views")}>Most viewed</Button>
105
+ </div>
106
+ </div>
107
+ <div className="mt-4 grid gap-3">
108
+ {sorted.map((video) => (
109
+ <VideoCard key={video.id} video={video} />
110
+ ))}
111
+ </div>
112
+ {/* TODO(next pass): the server handler reads YOUTUBE_API_KEY, resolves the channel from its handle, reads the uploads playlist, and saves a daily snapshot these same components render */}
113
+
114
+ <Card className="mt-8">
115
+ <CardHeader>
116
+ <CardTitle>Show your live numbers</CardTitle>
117
+ </CardHeader>
118
+ <CardContent>
119
+ <p className="text-muted-foreground">Five minutes in the Google Cloud console, once, and this page fills with your channel.</p>
120
+ <ol className="mt-3 grid list-decimal gap-2 pl-5">
121
+ {SETUP_STEPS.map((step) => (
122
+ <li key={step}>{step}</li>
123
+ ))}
124
+ </ol>
125
+ <p className="mt-3">
126
+ The secret is named <code className="rounded bg-muted px-1">{SECRET_NAME}</code> and stays on the server.
127
+ </p>
128
+ </CardContent>
129
+ </Card>
130
+
131
+ <Card className="mt-4">
132
+ <CardHeader>
133
+ <CardTitle>From YouTube Analytics</CardTitle>
134
+ </CardHeader>
135
+ <CardContent>
136
+ <p className="text-muted-foreground">
137
+ Watch time, revenue, impressions, traffic sources and demographics live in YouTube Analytics with your own
138
+ sign-in. Not available with an API key.
139
+ </p>
140
+ </CardContent>
141
+ </Card>
142
+ </main>
143
+ );
144
+ }
145
+ ```
146
+
147
+ ### `seed.json`
148
+ ```json
149
+ {
150
+ "youtubeDashboard": [
151
+ { "key": "channel", "type": "channel", "channelId": "UCq8x2Tn4Lm0bVfKe7RdPw3A", "title": "Tidepool Kitchen", "handle": "@tidepoolkitchen", "subscribers": 48200, "subscribersHidden": false, "views": 3915422, "videoCount": 214 },
152
+ { "key": "v1", "type": "video", "id": "k3Jd9sLq2Xw", "title": "Sourdough on a camp stove (it worked!)", "published": "2026-09-24T15:00:00Z", "views": 18430, "likes": 1204, "comments": 187 },
153
+ { "key": "v2", "type": "video", "id": "Pz7tR1mVb8c", "title": "Five-minute miso butter, three ways", "published": "2026-09-17T15:00:00Z", "views": 42911, "likes": 3310, "comments": 402 },
154
+ { "key": "v3", "type": "video", "id": "aQ4nW6yHe0s", "title": "Foraging sea beans at low tide", "published": "2026-09-10T15:00:00Z", "views": 27604, "likes": 2188, "comments": null },
155
+ { "key": "v4", "type": "video", "id": "Lm2cX8oTf5u", "title": "The pantry tour everyone asked for", "published": "2026-09-03T15:00:00Z", "views": 61378, "likes": null, "comments": 955 }
156
+ ]
157
+ }
158
+ ```
159
+
160
+ Draft note: the next pass adds backend.js, which reads `YOUTUBE_API_KEY`, resolves the channel from its handle, reads the uploads playlist and saves a daily snapshot, so the same tiles and cards show live numbers and a chart of change over time.
161
+
162
+ ## What stays the same in a YouTube draft
163
+
164
+ - **The whole dashboard paints on the first draft**: header, the three stat
165
+ tiles, the upload list, the setup card and the Analytics label. Nothing
166
+ waits behind a loading state or an empty section.
167
+ - **Seeded rows mirror the backend's rows field for field**: the `channel`
168
+ document is `loadChannel`'s shape and each `video` document is `loadVideos`'
169
+ shape, counts as numbers, `published` as an ISO string, so the next pass
170
+ changes the source and keeps every component.
171
+ - **The capability is labeled, never hidden**: a Badge on the header reads
172
+ "sample data until the key is set", and the setup card names YouTube Data API
173
+ v3 and `YOUTUBE_API_KEY` in so many words.
174
+ - **The key is the owner's**: the setup steps are for the owner, once. No input
175
+ asks a visitor for a key or a token, and no visitor is sent to a console. An
176
+ app where each visitor sees their own channel is a connection, not this.
177
+ - **Honest numbers**: a hidden subscriber count reads "hidden by the channel",
178
+ a missing like or comment count reads "hidden", and the subscriber tile
179
+ carries the rounding caption. Nothing unknown renders as zero.
180
+ - **What waits for the next pass** is named in the `TODO(next pass)` comment and
181
+ the draft note: `backend.js`, reading the secret, resolving the handle, the
182
+ uploads playlist, daily snapshots and the chart. The draft writes no
183
+ `backend.js`, makes no network call and reads no secret.
package/llms/youtube.js CHANGED
@@ -2,6 +2,7 @@ export const youtubeConfig = {
2
2
  name: "youtube",
3
3
  label: "YouTube channel stats",
4
4
  description: "YouTube channel dashboards on the owner's own YouTube Data API v3 key: the setup steps the app shows when the key is missing, keeping the key in backend.js, resolving a channel from a handle or URL, reading videos through the uploads playlist instead of the 100-unit search endpoint, daily snapshots to chart change over time, and labeling the numbers an API key cannot see",
5
+ draftVariant: true,
5
6
  cues: [
6
7
  "youtube",
7
8
  "my channel",
@@ -1 +1 @@
1
- {"version":3,"file":"youtube.js","sourceRoot":"","sources":["../../jsr/llms/youtube.ts"],"names":[],"mappings":"AAKA,MAAM,CAAC,MAAM,aAAa,GAAc;IACtC,IAAI,EAAE,SAAS;IACf,KAAK,EAAE,uBAAuB;IAC9B,WAAW,EACT,uXAAuX;IACzX,IAAI,EAAE;QACJ,SAAS;QACT,YAAY;QACZ,kBAAkB;QAClB,aAAa;QACb,aAAa;QACb,eAAe;QACf,mBAAmB;KACpB;CACF,CAAC"}
1
+ {"version":3,"file":"youtube.js","sourceRoot":"","sources":["../../jsr/llms/youtube.ts"],"names":[],"mappings":"AAKA,MAAM,CAAC,MAAM,aAAa,GAAc;IACtC,IAAI,EAAE,SAAS;IACf,KAAK,EAAE,uBAAuB;IAC9B,WAAW,EACT,uXAAuX;IAGzX,YAAY,EAAE,IAAI;IAClB,IAAI,EAAE;QACJ,SAAS;QACT,YAAY;QACZ,kBAAkB;QAClB,aAAa;QACb,aAAa;QACb,eAAe;QACf,mBAAmB;KACpB;CACF,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibes.diy/prompts",
3
- "version": "14.3.35",
3
+ "version": "14.3.37",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "exports": {
@@ -37,9 +37,9 @@
37
37
  "license": "Apache-2.0",
38
38
  "dependencies": {
39
39
  "@adviser/cement": "~0.5.34",
40
- "@vibes.diy/call-ai-v2": "14.3.35",
41
- "@vibes.diy/identity": "14.3.35",
42
- "@vibes.diy/use-vibes-types": "14.3.35",
40
+ "@vibes.diy/call-ai-v2": "14.3.37",
41
+ "@vibes.diy/identity": "14.3.37",
42
+ "@vibes.diy/use-vibes-types": "14.3.37",
43
43
  "arktype": "~2.2.3",
44
44
  "json-schema-faker": "~0.6.3"
45
45
  },