@vibes.diy/prompts 9.4.2 → 9.4.3
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.
- package/llms/backend.md +59 -46
- package/llms/fireproof.md +7 -17
- package/package.json +4 -4
- package/style-prompts.js +1 -1
- package/style-prompts.js.map +1 -1
- package/system-prompt-initial-oneshot.md +3 -2
- package/system-prompt-initial.md +3 -2
- package/system-prompt.md +3 -2
package/llms/backend.md
CHANGED
|
@@ -182,70 +182,73 @@ async function ensureSnapshot(ctx) {
|
|
|
182
182
|
for (const doc of SNAPSHOT) await ctx.db.put(doc, { db: "nodeReleases" });
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
// True only when every tracked field of the desired doc already matches what's
|
|
186
|
+
// stored — extra stored keys (like `_rev`) are ignored. This is what lets a
|
|
187
|
+
// tick that changes nothing write nothing.
|
|
188
|
+
function unchanged(existing, next) {
|
|
189
|
+
if (!existing) return false;
|
|
190
|
+
return Object.keys(next).every((k) => existing[k] === next[k]);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function putIfChanged(ctx, byId, next) {
|
|
194
|
+
if (unchanged(byId.get(next._id), next)) return;
|
|
195
|
+
await ctx.db.put(next, { db: "nodeReleases" });
|
|
196
|
+
}
|
|
197
|
+
|
|
185
198
|
export async function scheduled(event, ctx) {
|
|
186
199
|
await ensureSnapshot(ctx);
|
|
187
200
|
|
|
201
|
+
// Read current docs once so each write can diff against what's already stored.
|
|
202
|
+
const current = await ctx.db.query({ db: "nodeReleases" });
|
|
203
|
+
const byId = new Map(current.map((doc) => [doc._id, doc]));
|
|
204
|
+
|
|
188
205
|
// One request, no retry storm; ctx.fetch enforces 15s/10MB and egress caps.
|
|
189
206
|
const response = await ctx.fetch("https://api.github.com/repos/nodejs/node/releases?per_page=20", {
|
|
190
207
|
headers: { accept: "application/vnd.github+json" },
|
|
191
208
|
});
|
|
192
209
|
|
|
193
210
|
if (response.status === 403) {
|
|
194
|
-
const denied = await response
|
|
211
|
+
const denied = await response
|
|
212
|
+
.clone()
|
|
213
|
+
.json()
|
|
214
|
+
.catch(() => null);
|
|
195
215
|
if (denied?.vibesEgressDenied === true) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
{ db: "nodeReleases" }
|
|
205
|
-
);
|
|
216
|
+
// A real state change — putIfChanged writes once on entering denial, then
|
|
217
|
+
// nothing while it persists. No per-tick timestamp, so no churn.
|
|
218
|
+
await putIfChanged(ctx, byId, {
|
|
219
|
+
_id: "refresh:status",
|
|
220
|
+
type: "refreshStatus",
|
|
221
|
+
state: "egress-denied",
|
|
222
|
+
message: "showing saved data — live refresh unavailable",
|
|
223
|
+
});
|
|
206
224
|
return; // preserve the seeded/last-good release docs
|
|
207
225
|
}
|
|
208
226
|
}
|
|
209
227
|
|
|
210
|
-
if (!response.ok)
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
type: "refreshStatus",
|
|
215
|
-
state: "failed",
|
|
216
|
-
message: "showing saved data — refresh failed",
|
|
217
|
-
checkedAt: event.scheduledTime,
|
|
218
|
-
},
|
|
219
|
-
{ db: "nodeReleases" }
|
|
220
|
-
);
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
228
|
+
if (!response.ok) return; // transient failure: keep last-good rows, write nothing
|
|
229
|
+
|
|
230
|
+
const rows = await response.json().catch(() => null);
|
|
231
|
+
if (!Array.isArray(rows) || rows.length === 0) return; // empty upstream: abort, no writes
|
|
223
232
|
|
|
224
|
-
const rows = await response.json();
|
|
225
233
|
for (const row of rows.slice(0, 20)) {
|
|
226
234
|
if (!row?.id || !row?.html_url || !row?.published_at) continue;
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
{ db: "nodeReleases" }
|
|
237
|
-
);
|
|
235
|
+
// Stable per-item `_id` + diff: a row whose content is identical is skipped.
|
|
236
|
+
await putIfChanged(ctx, byId, {
|
|
237
|
+
_id: `release:github-${row.id}`,
|
|
238
|
+
type: "release",
|
|
239
|
+
title: String(row.name || row.tag_name || "Node.js release"),
|
|
240
|
+
url: String(row.html_url),
|
|
241
|
+
publishedAt: String(row.published_at),
|
|
242
|
+
source: "github-live",
|
|
243
|
+
});
|
|
238
244
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
},
|
|
247
|
-
{ db: "nodeReleases" }
|
|
248
|
-
);
|
|
245
|
+
|
|
246
|
+
await putIfChanged(ctx, byId, {
|
|
247
|
+
_id: "refresh:status",
|
|
248
|
+
type: "refreshStatus",
|
|
249
|
+
state: "fresh",
|
|
250
|
+
message: "Live feed refreshed",
|
|
251
|
+
});
|
|
249
252
|
}
|
|
250
253
|
```
|
|
251
254
|
|
|
@@ -290,6 +293,16 @@ down. Catch that exact 403 shape and show the saved snapshot with honest copy
|
|
|
290
293
|
**"showing saved data — live refresh unavailable"**. Never clear cached rows,
|
|
291
294
|
render a broken screen, or silently label saved data as fresh.
|
|
292
295
|
|
|
296
|
+
**Zero-churn discipline for scheduled ticks.** A cron runs forever, so it must
|
|
297
|
+
not write forever. Read the current docs first (`ctx.db.query`), give every item
|
|
298
|
+
a stable `_id`, and `put` **only** the rows whose content actually changed — the
|
|
299
|
+
`putIfChanged` diff above. Never stamp a per-tick `checkedAt`/`updatedAt`/run
|
|
300
|
+
counter onto a doc: it makes every put look new, and each spurious put mints a
|
|
301
|
+
fresh revision and a sync-feed row that re-tickles every replica on every tick.
|
|
302
|
+
A failed or empty upstream fetch aborts with no writes, preserving the last-good
|
|
303
|
+
rows. Only a genuine content change — a new or updated row, a real state
|
|
304
|
+
transition — earns a write.
|
|
305
|
+
|
|
293
306
|
## fetch — the app's HTTP endpoint
|
|
294
307
|
|
|
295
308
|
Runs for requests to the app's `/_api` route. The request path is rooted after
|
package/llms/fireproof.md
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# Fireproof Database API Guide
|
|
2
2
|
|
|
3
|
-
Fireproof is a document database with live sync, designed to make browser apps easy. On vibes.diy it runs against Firefly
|
|
3
|
+
Fireproof is a document database with live sync, designed to make browser apps easy. On vibes.diy it runs against Firefly: each app holds a local replica (IndexedDB) that IS the database, so writes succeed locally and instantly and then sync in the background. The Firefly server validates every synced write with your `access.js` on ingest and can still reject it (access denied, conflict); accepted writes stream live to every viewer. Use it in any JavaScript environment with a unified API that works both in React (with hooks) and as a standalone core API.
|
|
4
4
|
|
|
5
5
|
## Key Features
|
|
6
6
|
|
|
7
7
|
- **Apps run anywhere:** Bundle UI, data, and logic together.
|
|
8
|
-
- **Real-Time,
|
|
8
|
+
- **Real-Time, local-first:** Writes land in the local replica instantly and sync in the background; the server validates each on ingest and streams the accepted ones live to every viewer. `useLiveQuery` keeps the UI in sync as data arrives, so you render empty states rather than loading spinners — but a synced write can still be rejected on ingest (access denied, conflicts), so handle write rejections.
|
|
9
9
|
- **Unified API:** TypeScript works with Deno, Bun, Node.js, and the browser.
|
|
10
10
|
- **React Hooks:** Leverage `useLiveQuery` and `useDocument` for live collaboration. Note: these are NOT top-level exports — they are returned by the `useFireproof()` hook. Always destructure from `const { useLiveQuery, useDocument, database } = useFireproof("dbName")`.
|
|
11
11
|
|
|
12
12
|
**File structure:** A vibe's source is one or more files. `/App.jsx` is the entry point (React component). `/access.js` is optional — include it when the app needs per-document write validation or channel-based read isolation. Both files are pushed together and the server discovers `/access.js` automatically.
|
|
13
13
|
|
|
14
|
-
Fireproof enforces cryptographic causal consistency and ledger integrity using hash history, providing git-like versioning with lightweight blockchain-style verification. On vibes.diy, the Firefly server is the authority
|
|
14
|
+
Fireproof enforces cryptographic causal consistency and ledger integrity using hash history, providing git-like versioning with lightweight blockchain-style verification. On vibes.diy, a write commits to the local replica immediately and syncs to the Firefly server in the background; the server is the authority on acceptance: it stores each document in a per-document append-only sequence, runs your `access.js` to validate and route it on ingest, and then syncs it to viewers. Because every synced write is validated server-side on ingest, it is subject to access rules and can be rejected.
|
|
15
15
|
|
|
16
16
|
## Installation
|
|
17
17
|
|
|
@@ -388,21 +388,11 @@ const { useLiveQuery } = useFireproof("todos");
|
|
|
388
388
|
const { useLiveQuery } = useFireproof("shared-board", { offlineQueue: false });
|
|
389
389
|
```
|
|
390
390
|
|
|
391
|
-
## Anonymous local
|
|
391
|
+
## Anonymous writes (local-first by default)
|
|
392
392
|
|
|
393
|
-
|
|
393
|
+
Letting a logged-out visitor try the app and build a little state before signing in needs **no opt-in** — it is the universal default on every vibe. While logged out, `put`/`del`/`useLiveQuery`/`useDocument` run against the local replica with the identical API — no auth branching in your code — so favorites, a draft, or a scratch list just work. On first sign-in that local state drains automatically into the user's cloud database and then syncs live; nothing is lost if a drain is interrupted. The sign-in and drain-confirmation toast ("saved to your account") is platform-provided chrome — do not build your own "saved"/"synced" notice for it.
|
|
394
394
|
|
|
395
|
-
|
|
396
|
-
const { useLiveQuery, database } = useFireproof("favorites", {
|
|
397
|
-
anonymousLocal: true,
|
|
398
|
-
// Optional: reshape/stamp each local doc for its new owner; return falsy to drop it.
|
|
399
|
-
// Preserve `_id` (spread `...doc`) — migration retries are only idempotent when
|
|
400
|
-
// the id is kept, otherwise a retry after a partial failure creates duplicates.
|
|
401
|
-
migrate: (doc, userHandle) => ({ ...doc, owner: userHandle }),
|
|
402
|
-
});
|
|
403
|
-
```
|
|
404
|
-
|
|
405
|
-
Notes: use it only where anonymous-first state makes sense (favorites, drafts, a scratch list). A returning signed-out visitor is automatically steered to sign in rather than starting a fresh throwaway local session. Migration keeps local data intact if it fails, so nothing is lost.
|
|
395
|
+
The old `{ anonymousLocal: true }` option (and its `migrate` hook) is **deprecated and ignored** — its behavior is now the default, so the runtime warns once and drains any legacy data on its own. Do not emit the flag in new code.
|
|
406
396
|
|
|
407
397
|
## Reading Resolved Grants and worked access examples
|
|
408
398
|
|
|
@@ -622,7 +612,7 @@ Other common `oldDoc` patterns: `if (oldDoc === null) { /* create-only logic */
|
|
|
622
612
|
|
|
623
613
|
## Architecture: Where's My Data?
|
|
624
614
|
|
|
625
|
-
Data lives
|
|
615
|
+
Data lives in a local replica (IndexedDB) that the app reads and writes instantly; that replica syncs to the Firefly server in the background. The server is the authority on acceptance — each synced write is validated by your `access.js` on ingest, persisted, and then synced to all users who have read access. A write that fails validation or hits a conflict is rejected on ingest (the server's copy wins) — handle those rejections rather than assuming the write always lands.
|
|
626
616
|
|
|
627
617
|
## Using Fireproof in JavaScript
|
|
628
618
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibes.diy/prompts",
|
|
3
|
-
"version": "9.4.
|
|
3
|
+
"version": "9.4.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"description": "",
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
"license": "Apache-2.0",
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@adviser/cement": "~0.5.34",
|
|
27
|
-
"@vibes.diy/call-ai-v2": "^9.4.
|
|
28
|
-
"@vibes.diy/identity": "^9.4.
|
|
29
|
-
"@vibes.diy/use-vibes-types": "^9.4.
|
|
27
|
+
"@vibes.diy/call-ai-v2": "^9.4.3",
|
|
28
|
+
"@vibes.diy/identity": "^9.4.3",
|
|
29
|
+
"@vibes.diy/use-vibes-types": "^9.4.3",
|
|
30
30
|
"arktype": "~2.2.3",
|
|
31
31
|
"json-schema-faker": "~0.6.2"
|
|
32
32
|
},
|
package/style-prompts.js
CHANGED
|
@@ -54,7 +54,7 @@ export const stylePrompts = [
|
|
|
54
54
|
`ACCESSIBILITY: Body text ≥0.82rem, section labels ≥0.6rem. Strong contrast (text on primary → var(--on-primary, #ffffff); on secondary → var(--on-secondary, var(--text-primary)); on success → var(--on-success, var(--text-primary)); on accent → var(--on-info, #ffffff)). The on-* names are palette extras — the fallbacks keep colored blocks readable in both light and dark mode. aria-hidden the decorative .hero-text-shadow.`,
|
|
55
55
|
`RESPONSIVENESS: ≤700px: stat row collapses to 2 columns, form grid to 1 column. ≤500px: nav stacks vertically, stat row to single column.`,
|
|
56
56
|
`FONTS: Use Google Fonts for Space Grotesk (400,500,600,700) and JetBrains Mono (400,500,700). Use display=swap (so text renders immediately in a fallback and upgrades to the real font when it lands, instead of staying invisible) and add <link rel="preconnect" href="https://fonts.googleapis.com"> + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> right before the stylesheet so the connection is warm.`,
|
|
57
|
-
`LOADING STATES:
|
|
57
|
+
`LOADING STATES: Only a genuinely-network async operation (callAI, fetch) shows a loading state. A database save is local-first — it resolves against the local replica instantly, so it gets NO spinner (apply the change optimistically and let useLiveQuery reconcile; roll back only if the write is rejected on sync). A database read is not a network op either — the first render already has the data, so render an inviting empty-state when there's nothing yet, never a loading spinner. For the network calls that do warrant it, use a useState boolean \`isLoading\`. While the call is in flight: disable the button with \`disabled={isLoading}\`, replace its label with a spinning SVG (a 16x16 circle with 3px stroke, top quarter transparent, CSS animation rotate 0.8s linear infinite), and optionally add a short text like "Loading..." nearby. Pattern: \`setIsLoading(true); try { await callAI(...); } finally { setIsLoading(false); }\`. The spinner should match the theme: 3px stroke in var(--border) color, no blur, sharp edges. Never leave a network-triggering button clickable with no feedback during the call.`,
|
|
58
58
|
`THEME TOKENS — provided by the platform at runtime. The platform injects these canonical color + structural tokens into the served page, so do NOT restate a :root block — just build the UI on the CSS variables via Tailwind bracket notation (bg-[var(--background)], text-[var(--text-primary)], border-[var(--border)], bg-[var(--primary)]). The block below is the REFERENCE for the token names and values, not something to paste in. They are the swap contract: the user can later swap themes and the app restyles instantly — but only for values routed through these variables. Two hard rules: (1) no literal colors for core surfaces/text/actions/borders — never bg-[#hex]/text-[#hex]/border-[#hex]; a swap can't reach a baked-in literal (a genuinely one-off decorative value may be inlined on the element); (2) don't invent bespoke color tokens — a name a future theme won't define can't be swapped. Plain Tailwind spacing/size utilities (p-4, gap-2, text-lg, rounded-md) are fine — the runtime remaps them onto the structural tokens automatically. Never write a bare --spacing variable — the runtime reserves that name; the spacing token is --vibes-spacing (when editing older code that references var(--spacing), rewrite it to var(--vibes-spacing) or plain spacing utilities). Token names are code-only vocabulary — never render a token name (--background, --primary, …) in the app's visible UI copy, headings, or labels.
|
|
59
59
|
|
|
60
60
|
<style>
|
package/style-prompts.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"style-prompts.js","sourceRoot":"","sources":["../jsr/style-prompts.ts"],"names":[],"mappings":"AAYA,MAAM,CAAC,MAAM,kBAAkB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAuChC,CAAC;AAEH,MAAM,CAAC,MAAM,YAAY,GAAkB;IAIzC;QACE,IAAI,EAAE,eAAe;QACrB,MAAM,EAAE;YACN,yXAAyX;YACzX,04BAA04B;YAC14B,2SAA2S;YAC3S,wiBAAwiB;YACxiB,quBAAquB;YACruB,0JAA0J;YAC1J,i0BAAi0B;YACj0B,u/DAAu/D;YACv/D,mcAAmc;YACnc,yaAAya;YACza,2IAA2I;YAC3I,waAAwa;YACxa,
|
|
1
|
+
{"version":3,"file":"style-prompts.js","sourceRoot":"","sources":["../jsr/style-prompts.ts"],"names":[],"mappings":"AAYA,MAAM,CAAC,MAAM,kBAAkB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAuChC,CAAC;AAEH,MAAM,CAAC,MAAM,YAAY,GAAkB;IAIzC;QACE,IAAI,EAAE,eAAe;QACrB,MAAM,EAAE;YACN,yXAAyX;YACzX,04BAA04B;YAC14B,2SAA2S;YAC3S,wiBAAwiB;YACxiB,quBAAquB;YACruB,0JAA0J;YAC1J,i0BAAi0B;YACj0B,u/DAAu/D;YACv/D,mcAAmc;YACnc,yaAAya;YACza,2IAA2I;YAC3I,waAAwa;YACxa,ylCAAylC;YAKzlC;;;EAGJ,kBAAkB;SACX;YACH,oDAAoD;SACrD,CAAC,IAAI,CAAC,MAAM,CAAC;KACf;IACD;QACE,IAAI,EAAE,SAAS;QACf,MAAM,EACJ,+8BAA+8B;KACl9B;IACD;QACE,IAAI,EAAE,WAAW;QACjB,MAAM,EAAE,uBAAuB;KAChC;IACD;QACE,IAAI,EAAE,YAAY;QAClB,MAAM,EAAE,sBAAsB;KAC/B;IACD;QACE,IAAI,EAAE,YAAY;QAClB,MAAM,EAAE,mBAAmB;KAC5B;IACD;QACE,IAAI,EAAE,cAAc;QACpB,MAAM,EAAE,mBAAmB;KAC5B;IACD;QACE,IAAI,EAAE,aAAa;QACnB,MAAM,EAAE,kBAAkB;KAC3B;IACD;QACE,IAAI,EAAE,SAAS;QACf,MAAM,EAAE,qBAAqB;KAC9B;IACD;QACE,IAAI,EAAE,YAAY;QAClB,MAAM,EAAE,0BAA0B;KACnC;IACD;QACE,IAAI,EAAE,cAAc;QACpB,MAAM,EAAE,kBAAkB;KAC3B;IACD;QACE,IAAI,EAAE,UAAU;QAChB,MAAM,EACJ,orCAAorC;KACvrC;CACF,CAAC;AAGF,MAAM,CAAC,MAAM,kBAAkB,GAAG,eAAwB,CAAC;AAG3D,MAAM,WAAW,GAAG,IAAI,GAAG,EAAuB,CAAC;AACnD,KAAK,MAAM,CAAC,IAAI,YAAY,EAAE,CAAC;IAC7B,IAAI,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC,IAAI,gCAAgC,CAAC,CAAC;IAC7F,CAAC;IACD,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;AAC7B,CAAC;AAGD,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,GAAG,EAAE;IACtC,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;IAClD,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5D,MAAM,IAAI,KAAK,CACb,uBAAuB,kBAAkB,iDAAiD,SAAS,gDAAgD,CACpJ,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC,MAAM,CAAC;AACtB,CAAC,CAAC,EAAE,CAAC"}
|
|
@@ -29,8 +29,9 @@ You are an AI assistant tasked with creating React components. You should create
|
|
|
29
29
|
- Structure your component code in this order: (1) hooks and document shapes, (2) event handlers, (3) classNames object, (4) JSX return. ClassNames go right before JSX so they are close to where they are used.
|
|
30
30
|
- Use Fireproof for data persistence
|
|
31
31
|
- Use `callAI` to fetch AI, use schema like this: `JSON.parse(await callAI(prompt, { schema: { properties: { todos: { type: 'array', items: { type: 'string' } } } } }))` and save final responses as individual Fireproof documents.
|
|
32
|
-
- Always show loading states during
|
|
33
|
-
-
|
|
32
|
+
- Always show loading states during genuinely-network async operations (callAI, fetch): use a useState boolean (e.g. `isLoading`), set it true before the call and false in .finally(). While loading: (1) disable the trigger button with `disabled={isLoading}`, (2) replace the button text with a spinning SVG icon using CSS animation `animate-spin` (a simple circle with a gap), (3) optionally show a short status text like 'Loading...' near the button. Never leave the user clicking a button with no visual feedback. Pattern: `setIsLoading(true); try { await callAI(...); } finally { setIsLoading(false); }`
|
|
33
|
+
- Database reads are not a network operation — never show a loading state or spinner for them. `useLiveQuery` reads a hydrated local replica, so the first render already contains the data; an empty result means the database is genuinely empty. Render an inviting empty-state instead (friendly copy that prompts the first action), never a loading state.
|
|
34
|
+
- Give instant feedback for Fireproof writes too, not just for callAI/fetch. A `database.put` (toggling a checkbox, marking done, inline edits, reorder, like/vote counters) resolves against the local replica instantly — there is no visible in-flight window to spinner over — but the UI must still react the moment the user acts. Apply the change optimistically: flip the visible value immediately and let `useLiveQuery` reconcile. On failure (a rejected write syncing back from the server), revert the optimistic value and surface a brief error (inline or toast) with a way to retry — never let the optimistic UI silently lie when a `put` is rejected. Never let a checkbox tap, toggle, or saved inline edit sit with no visible response.
|
|
34
35
|
- For file uploads use drag and drop and store using the `doc._files` API; for AI image generation use `<ImgGen prompt="..." />`
|
|
35
36
|
- Access control is decided by the runtime, not your code. Gate every write surface — forms, submit/edit/delete buttons, any mutating action — on `useVibe(dbName).can`. `const { me, can, ready } = useVibe("comments")` from `"use-vibes"`, passing the Fireproof database name you write to. Show the editor when `can.create(draft).ok` (or `can.edit(doc)` / `can.delete(doc)`); while `ready` is false show a neutral skeleton/disabled state; when denied, render `can.create(draft).reason` as the fallback copy (the sign-in or join prompt). `can.*` asks the server's permission rules — the same rules the server enforces — so NEVER derive write permission from `viewer`, `access.hasRole()`/`access.hasChannel()`, or document fields. `useViewer()` is identity/display only: `const { ViewerTag } = useViewer()` renders **other** people (`<ViewerTag userHandle={...} />` for comment authors, rosters, "added by" labels). The current viewer's own pill and the sign-in button are system chrome in the Vibes Switch (the panel the logo opens) — don't add a header pill or login button for the current user. Owner-only management UI is gated on `can.*` too (the runtime knows the owner). This applies to every app — the runtime decides sharing, not the prompt. Writes can still be rejected server-side even when `can.*` allows, so keep the optimistic-write + rollback handling. See use-vibe docs.
|
|
36
37
|
- Don't try to generate png or base64 data, use placeholder image APIs instead, like https://picsum.photos/400 where 400 is the square size
|
package/system-prompt-initial.md
CHANGED
|
@@ -29,8 +29,9 @@ You are an AI assistant tasked with creating React components. You should create
|
|
|
29
29
|
- Structure your component code in this order: (1) hooks and document shapes, (2) event handlers, (3) classNames object, (4) JSX return. ClassNames go right before JSX so they are close to where they are used.
|
|
30
30
|
- Use Fireproof for data persistence
|
|
31
31
|
- Use `callAI` to fetch AI, use schema like this: `JSON.parse(await callAI(prompt, { schema: { properties: { todos: { type: 'array', items: { type: 'string' } } } } }))` and save final responses as individual Fireproof documents.
|
|
32
|
-
- Always show loading states during
|
|
33
|
-
-
|
|
32
|
+
- Always show loading states during genuinely-network async operations (callAI, fetch): use a useState boolean (e.g. `isLoading`), set it true before the call and false in .finally(). While loading: (1) disable the trigger button with `disabled={isLoading}`, (2) replace the button text with a spinning SVG icon using CSS animation `animate-spin` (a simple circle with a gap), (3) optionally show a short status text like 'Loading...' near the button. Never leave the user clicking a button with no visual feedback. Pattern: `setIsLoading(true); try { await callAI(...); } finally { setIsLoading(false); }`
|
|
33
|
+
- Database reads are not a network operation — never show a loading state or spinner for them. `useLiveQuery` reads a hydrated local replica, so the first render already contains the data; an empty result means the database is genuinely empty. Render an inviting empty-state instead (friendly copy that prompts the first action), never a loading state.
|
|
34
|
+
- Give instant feedback for Fireproof writes too, not just for callAI/fetch. A `database.put` (toggling a checkbox, marking done, inline edits, reorder, like/vote counters) resolves against the local replica instantly — there is no visible in-flight window to spinner over — but the UI must still react the moment the user acts. Apply the change optimistically: flip the visible value immediately and let `useLiveQuery` reconcile. On failure (a rejected write syncing back from the server), revert the optimistic value and surface a brief error (inline or toast) with a way to retry — never let the optimistic UI silently lie when a `put` is rejected. Never let a checkbox tap, toggle, or saved inline edit sit with no visible response.
|
|
34
35
|
- For file uploads use drag and drop and store using the `doc._files` API; for AI image generation use `<ImgGen prompt="..." />`
|
|
35
36
|
- Access control is decided by the runtime, not your code. Gate every write surface — forms, submit/edit/delete buttons, any mutating action — on `useVibe(dbName).can`. `const { me, can, ready } = useVibe("comments")` from `"use-vibes"`, passing the Fireproof database name you write to. Show the editor when `can.create(draft).ok` (or `can.edit(doc)` / `can.delete(doc)`); while `ready` is false show a neutral skeleton/disabled state; when denied, render `can.create(draft).reason` as the fallback copy (the sign-in or join prompt). `can.*` asks the server's permission rules — the same rules the server enforces — so NEVER derive write permission from `viewer`, `access.hasRole()`/`access.hasChannel()`, or document fields. `useViewer()` is identity/display only: `const { ViewerTag } = useViewer()` renders **other** people (`<ViewerTag userHandle={...} />` for comment authors, rosters, "added by" labels). The current viewer's own pill and the sign-in button are system chrome in the Vibes Switch (the panel the logo opens) — don't add a header pill or login button for the current user. Owner-only management UI is gated on `can.*` too (the runtime knows the owner). This applies to every app — the runtime decides sharing, not the prompt. Writes can still be rejected server-side even when `can.*` allows, so keep the optimistic-write + rollback handling. See use-vibe docs.
|
|
36
37
|
- Don't try to generate png or base64 data, use placeholder image APIs instead, like https://picsum.photos/400 where 400 is the square size
|
package/system-prompt.md
CHANGED
|
@@ -29,8 +29,9 @@ You are an AI assistant tasked with creating React components. You should create
|
|
|
29
29
|
- Structure your component code in this order: (1) hooks and document shapes, (2) event handlers, (3) classNames object, (4) JSX return. ClassNames go right before JSX so they are close to where they are used. Never define components (functions that return JSX) inside `App` or any other component — always define them at module scope and pass data as props. Components defined inside other components are recreated on every render, causing React to unmount and remount them, which breaks form focus and input state.
|
|
30
30
|
- Use Fireproof for data persistence
|
|
31
31
|
- Use `callAI` to fetch AI, use schema like this: `JSON.parse(await callAI(prompt, { schema: { properties: { todos: { type: 'array', items: { type: 'string' } } } } }))` and save final responses as individual Fireproof documents.
|
|
32
|
-
- Always show loading states during
|
|
33
|
-
-
|
|
32
|
+
- Always show loading states during genuinely-network async operations (callAI, fetch): use a useState boolean (e.g. `isLoading`), set it true before the call and false in .finally(). While loading: (1) disable the trigger button with `disabled={isLoading}`, (2) replace the button text with a spinning SVG icon using CSS animation `animate-spin` (a simple circle with a gap), (3) optionally show a short status text like 'Loading...' near the button. Never leave the user clicking a button with no visual feedback. Pattern: `setIsLoading(true); try { await callAI(...); } finally { setIsLoading(false); }`
|
|
33
|
+
- Database reads are not a network operation — never show a loading state or spinner for them. `useLiveQuery` reads a hydrated local replica, so the first render already contains the data; an empty result means the database is genuinely empty. Render an inviting empty-state instead (friendly copy that prompts the first action), never a loading state.
|
|
34
|
+
- Give instant feedback for Fireproof writes too, not just for callAI/fetch. A `database.put` (toggling a checkbox, marking done, inline edits, reorder, like/vote counters) resolves against the local replica instantly — there is no visible in-flight window to spinner over — but the UI must still react the moment the user acts. Apply the change optimistically: flip the visible value immediately and let `useLiveQuery` reconcile. On failure (a rejected write syncing back from the server), revert the optimistic value and surface a brief error (inline or toast) with a way to retry — never let the optimistic UI silently lie when a `put` is rejected. Never let a checkbox tap, toggle, or saved inline edit sit with no visible response.
|
|
34
35
|
- For file uploads use drag and drop and store using the `doc._files` API; for AI image generation use `<ImgGen prompt="..." />`
|
|
35
36
|
- Access control is decided by the runtime, not your code. Gate every write surface — forms, submit/edit/delete buttons, any mutating action — on `useVibe(dbName).can`. `const { me, can, ready } = useVibe("comments")` from `"use-vibes"`, passing the Fireproof database name you write to. Show the editor when `can.create(draft).ok` (or `can.edit(doc)` / `can.delete(doc)`); while `ready` is false show a neutral skeleton/disabled state; when denied, render `can.create(draft).reason` as the fallback copy (the sign-in or join prompt). `can.*` runs the app's own `access.js` — the same function the server enforces — so NEVER derive write permission from `viewer`, `access.hasRole()`/`access.hasChannel()`, or document fields. `useViewer()` is identity/display only: `const { ViewerTag } = useViewer()` renders **other** people (`<ViewerTag userHandle={...} />` for comment authors, rosters, "added by" labels). The current viewer's own pill and the sign-in button are system chrome in the Vibes Switch (the panel the logo opens) — don't add a header pill or login button for the current user. Owner-only management UI is gated on `can.*` too (the access.js encodes the owner rule). This applies to every app — the runtime decides sharing, not the prompt. Writes can still be rejected server-side even when `can.*` allows, so keep the optimistic-write + rollback handling. See use-vibe docs.
|
|
36
37
|
- Don't try to generate png or base64 data, use placeholder image APIs instead, like https://picsum.photos/400 where 400 is the square size
|