@vibes.diy/prompts 14.3.79 → 14.3.81
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/access.md +120 -85
- package/llms/backend.js +2 -2
- package/llms/backend.js.map +1 -1
- package/llms/backend.md +51 -43
- package/llms/bluesky.md +16 -14
- package/llms/board-games.d.ts +2 -0
- package/llms/board-games.js +29 -0
- package/llms/board-games.js.map +1 -0
- package/llms/board-games.md +477 -0
- package/llms/calendar.js +19 -2
- package/llms/calendar.js.map +1 -1
- package/llms/calendar.md +119 -1
- package/llms/callai.md +91 -20
- package/llms/connections.md +5 -4
- package/llms/create-vibe.md +5 -4
- package/llms/fireproof.md +261 -109
- package/llms/image-gen.md +44 -27
- package/llms/index.d.ts +2 -1
- package/llms/index.js +3 -0
- package/llms/index.js.map +1 -1
- package/llms/p5.md +9 -11
- package/llms/spotify.md +5 -2
- package/llms/three-js.md +163 -166
- package/llms/use-vibe.md +30 -8
- package/llms/use-viewer.md +61 -45
- package/llms/voxel.md +32 -37
- package/llms/youtube.md +5 -2
- package/package.json +4 -4
- package/prompts.d.ts +4 -2
- package/prompts.js +47 -24
- package/prompts.js.map +1 -1
- package/system-prompt-initial-oneshot.md +15 -12
- package/system-prompt-initial.md +14 -11
- package/system-prompt.md +36 -54
- package/themes/colorsets.js +18 -2
- package/themes/colorsets.js.map +1 -1
- package/themes/looks/pouf/brief.md +65 -47
- package/themes/looks/pouf/catalog.generated.d.ts +1 -1
- package/themes/looks/pouf/catalog.generated.js +103 -10
- package/themes/looks/pouf/catalog.generated.js.map +1 -1
- package/themes/looks/pouf/variety.js +2 -2
- package/themes/looks/pouf/variety.js.map +1 -1
package/llms/fireproof.md
CHANGED
|
@@ -29,14 +29,17 @@ When the server later refuses a write the visitor can act on — a signed-in vis
|
|
|
29
29
|
function NoteForm({ text }) {
|
|
30
30
|
const { database, writeRejection, clearWriteRejection } = useFireproof("notes");
|
|
31
31
|
return (
|
|
32
|
-
<
|
|
33
|
-
<
|
|
32
|
+
<Stack gap={2}>
|
|
33
|
+
<Row>
|
|
34
|
+
<Button type="button" onClick={() => database.put({ type: "note", text })}>Save</Button>
|
|
35
|
+
</Row>
|
|
34
36
|
{writeRejection && (
|
|
35
|
-
<
|
|
36
|
-
{writeRejection.message}
|
|
37
|
-
|
|
37
|
+
<Row gap={2} align="center" wrap>
|
|
38
|
+
<p role="status">{writeRejection.message}</p>
|
|
39
|
+
<Button type="button" size="sm" variant="quiet" onClick={clearWriteRejection}>Dismiss</Button>
|
|
40
|
+
</Row>
|
|
38
41
|
)}
|
|
39
|
-
</
|
|
42
|
+
</Stack>
|
|
40
43
|
);
|
|
41
44
|
}
|
|
42
45
|
```
|
|
@@ -53,6 +56,7 @@ import React, { useState } from "react";
|
|
|
53
56
|
import { callAI } from "call-ai";
|
|
54
57
|
import { useFireproof } from "use-fireproof";
|
|
55
58
|
import { useVibe } from "use-vibes";
|
|
59
|
+
import { Button, Field, Heading, Input, Row, RowCard, Stack, Text, Textarea } from "@vibes.diy/pouf";
|
|
56
60
|
|
|
57
61
|
const taskSchema = {
|
|
58
62
|
type: "object",
|
|
@@ -100,19 +104,23 @@ function TaskCard({ task, database, can, onCorrect, onSaved, disabled }) {
|
|
|
100
104
|
const { useDocument } = useFireproof(database);
|
|
101
105
|
const { doc, merge, save } = useDocument(task);
|
|
102
106
|
const [editing, setEditing] = useState(false);
|
|
103
|
-
return <
|
|
104
|
-
<
|
|
107
|
+
return <RowCard><Stack gap={2}>
|
|
108
|
+
<Text>{task.title}</Text><Text muted>{task.details}</Text><Text muted>{task.dueDate}</Text>
|
|
105
109
|
{can.edit(task).ok && <>
|
|
106
|
-
<
|
|
107
|
-
|
|
110
|
+
<Row gap={2} wrap>
|
|
111
|
+
<Button type="button" size="sm" variant="quiet" disabled={disabled} onClick={() => onCorrect(task)}>Change by sentence</Button>
|
|
112
|
+
<Button type="button" size="sm" variant="quiet" disabled={disabled} onClick={() => setEditing(true)}>Edit details</Button>
|
|
113
|
+
</Row>
|
|
108
114
|
{editing && <form onSubmit={async (event) => { event.preventDefault(); if (disabled) return; await save(); onSaved(task); setEditing(false); }}>
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
115
|
+
<Stack gap={3}>
|
|
116
|
+
<Field label="Task">{(id, describedBy) => <Input id={id} describedBy={describedBy} disabled={disabled} value={doc.title} onChange={(title) => merge({ title })} />}</Field>
|
|
117
|
+
<Field label="Details">{(id, describedBy) => <Textarea id={id} describedBy={describedBy} disabled={disabled} value={doc.details} onChange={(details) => merge({ details })} />}</Field>
|
|
118
|
+
<Field label="Due date">{(id, describedBy) => <Input id={id} describedBy={describedBy} disabled={disabled} type="date" value={doc.dueDate} onChange={(dueDate) => merge({ dueDate })} />}</Field>
|
|
119
|
+
<Row><Button type="submit" size="sm" variant="quiet" disabled={disabled}>Save details</Button></Row>
|
|
120
|
+
</Stack>
|
|
113
121
|
</form>}
|
|
114
122
|
</>}
|
|
115
|
-
</
|
|
123
|
+
</Stack></RowCard>;
|
|
116
124
|
}
|
|
117
125
|
|
|
118
126
|
export default function App() {
|
|
@@ -188,21 +196,27 @@ export default function App() {
|
|
|
188
196
|
setMessage(error.message);
|
|
189
197
|
} finally { setBusy(false); }
|
|
190
198
|
}
|
|
191
|
-
return <main>
|
|
192
|
-
<
|
|
199
|
+
return <main className="min-h-screen bg-background text-foreground font-sans p-6"><Stack gap={5}>
|
|
200
|
+
<Heading level={1}>Tasks in your own words</Heading>
|
|
193
201
|
<form onSubmit={submitSentence}>
|
|
194
|
-
<
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
202
|
+
<Stack gap={3}>
|
|
203
|
+
<Field label="Describe tasks, a correction, or ask a question">{(id, describedBy) => <Textarea id={id} describedBy={describedBy} value={sentence} disabled={busy || pending !== undefined} onChange={setSentence} />}</Field>
|
|
204
|
+
{current !== undefined && <Row gap={2} align="center" wrap><Text muted>Changing: {current.title}</Text><Button type="button" size="sm" variant="quiet" disabled={busy || pending !== undefined} onClick={() => { setCurrent(undefined); setSentence(""); }}>Add a new task</Button></Row>}
|
|
205
|
+
<Row gap={2} wrap>
|
|
206
|
+
<Button type="submit" disabled={!ready || busy || sentence.trim().length === 0}>{busy ? "Adding your changes…" : pending !== undefined ? "Retry remaining tasks" : current === undefined ? "Add tasks" : "Apply correction"}</Button>
|
|
207
|
+
<Button type="button" variant="quiet" disabled={!ready || busy || pending !== undefined || sentence.trim().length === 0} onClick={askRecords}>Ask about tasks</Button>
|
|
208
|
+
</Row>
|
|
209
|
+
</Stack>
|
|
198
210
|
</form>
|
|
199
|
-
<
|
|
211
|
+
<Text>{answer}</Text>
|
|
200
212
|
<p role="status">{message}</p>
|
|
201
|
-
{writeRejection && <p role="status">{writeRejection.message}
|
|
202
|
-
|
|
203
|
-
|
|
213
|
+
{writeRejection && <Row gap={2} align="center" wrap><p role="status">{writeRejection.message}</p><Button type="button" size="sm" variant="quiet" onClick={clearWriteRejection}>Dismiss</Button></Row>}
|
|
214
|
+
<Row gap={2} wrap>
|
|
215
|
+
{lastWrite !== undefined && <Button type="button" size="sm" variant="quiet" disabled={busy} onClick={undoLast}>Undo</Button>}
|
|
216
|
+
{pending !== undefined && !busy && <Button type="button" size="sm" variant="quiet" tone="down" onClick={() => { setPending(undefined); setSentence(""); setCurrent(undefined); }}>Discard unsaved tasks</Button>}
|
|
217
|
+
</Row>
|
|
204
218
|
{docs.map((task) => <TaskCard key={task._id} task={task} database={database} can={can} disabled={busy || pending !== undefined} onSaved={(previous) => setLastWrite({ created: [], replaced: previous })} onCorrect={(doc) => { if (busy || pending !== undefined) return; setCurrent(doc); setSentence(""); }} />)}
|
|
205
|
-
</main>;
|
|
219
|
+
</Stack></main>;
|
|
206
220
|
}
|
|
207
221
|
```
|
|
208
222
|
|
|
@@ -337,9 +351,15 @@ To update an existing document from a click handler or callback, use `database.p
|
|
|
337
351
|
=======
|
|
338
352
|
<li key={doc._id}>
|
|
339
353
|
{doc.text}
|
|
340
|
-
<
|
|
341
|
-
|
|
342
|
-
|
|
354
|
+
<IconButton
|
|
355
|
+
size="sm"
|
|
356
|
+
variant={doc.favorite ? "solid" : "quiet"}
|
|
357
|
+
tone="warn"
|
|
358
|
+
aria-pressed={Boolean(doc.favorite)}
|
|
359
|
+
label={doc.favorite ? "Unfavorite" : "Favorite"}
|
|
360
|
+
icon={<Icon name="star" size="sm" />}
|
|
361
|
+
onClick={() => database.put({ ...doc, favorite: !doc.favorite })}
|
|
362
|
+
/>
|
|
343
363
|
</li>
|
|
344
364
|
>>>>>>> REPLACE
|
|
345
365
|
```
|
|
@@ -348,7 +368,17 @@ Never call hooks inside handlers — `const { doc, save } = useDocument({ _id: i
|
|
|
348
368
|
|
|
349
369
|
### Continuous Controls — `merge()` on every event, `save()` once on commit
|
|
350
370
|
|
|
351
|
-
A continuous control (slider, drag, color picker, live-typed text bound to one doc) fires many events in quick succession. **Never call `database.put()` or `save()` on every `onChange`** — that floods the server with rapid concurrent writes to the same `_id`, which collide on the per-document sequence and get rejected (`Failed to put document …`). Instead, `merge()`
|
|
371
|
+
A continuous control (slider, drag, color picker, live-typed text bound to one doc) fires many events in quick succession. **Never call `database.put()` or `save()` on every `onChange`** — that floods the server with rapid concurrent writes to the same `_id`, which collide on the per-document sequence and get rejected (`Failed to put document …`). Instead, `merge()` on each event to keep the UI live (it updates the local draft and, when the doc has an `_id`, broadcasts the merged snapshot to connected peers, but persists nothing), and `save()` once when the interaction commits (`onPointerUp`, `onBlur`, or a debounced trailing call; a `Slider`'s `onChange` fires on every step, so it is not a commit, and the wrapper around it hears the release). Editing a single doc with a slider:
|
|
372
|
+
|
|
373
|
+
### `App.jsx`
|
|
374
|
+
```jsx
|
|
375
|
+
<<<<<<< SEARCH
|
|
376
|
+
import { useFireproof } from "use-fireproof";
|
|
377
|
+
=======
|
|
378
|
+
import { useFireproof } from "use-fireproof";
|
|
379
|
+
import { Slider } from "@vibes.diy/pouf/interactive";
|
|
380
|
+
>>>>>>> REPLACE
|
|
381
|
+
```
|
|
352
382
|
|
|
353
383
|
### `App.jsx`
|
|
354
384
|
```jsx
|
|
@@ -359,6 +389,7 @@ A continuous control (slider, drag, color picker, live-typed text bound to one d
|
|
|
359
389
|
|
|
360
390
|
// One shared mix doc; slider updates the UI on every event, persists on commit
|
|
361
391
|
const { doc: mix, merge: mergeMix, save: saveMix } = useDocument({ _id: "mix:current", level: 50 });
|
|
392
|
+
const SLIDER_KEYS = new Set(["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End", "PageUp", "PageDown"]);
|
|
362
393
|
>>>>>>> REPLACE
|
|
363
394
|
```
|
|
364
395
|
|
|
@@ -367,14 +398,16 @@ A continuous control (slider, drag, color picker, live-typed text bound to one d
|
|
|
367
398
|
<<<<<<< SEARCH
|
|
368
399
|
<h3>Recent Documents</h3>
|
|
369
400
|
=======
|
|
370
|
-
<
|
|
371
|
-
type="range"
|
|
372
|
-
min="0"
|
|
373
|
-
max="100"
|
|
374
|
-
value={mix.level}
|
|
375
|
-
onChange={(e) => mergeMix({ level: Number(e.target.value) })} // live, no write
|
|
401
|
+
<div
|
|
376
402
|
onPointerUp={() => saveMix().catch((err) => console.error("save failed", err))} // one write on commit
|
|
377
|
-
|
|
403
|
+
onKeyUp={(e) => {
|
|
404
|
+
// only the keys that move the slider commit; Tab or focusing it writes nothing
|
|
405
|
+
if (SLIDER_KEYS.has(e.key)) saveMix().catch((err) => console.error("save failed", err));
|
|
406
|
+
}}
|
|
407
|
+
>
|
|
408
|
+
{/* live, no write */}
|
|
409
|
+
<Slider label="Level" min={0} max={100} value={[mix.level]} onChange={([level]) => mergeMix({ level })} />
|
|
410
|
+
</div>
|
|
378
411
|
|
|
379
412
|
<h3>Recent Documents</h3>
|
|
380
413
|
>>>>>>> REPLACE
|
|
@@ -382,6 +415,127 @@ A continuous control (slider, drag, color picker, live-typed text bound to one d
|
|
|
382
415
|
|
|
383
416
|
If you genuinely need to persist mid-drag, debounce the `save()` so at most one write is in flight, and still `.catch()` the rejection. Better yet, prefer one document per event (see the Counter Pattern) over hammering a single hot document.
|
|
384
417
|
|
|
418
|
+
### Strokes — `merge()` the gesture live, save its cells once on pointer-up
|
|
419
|
+
|
|
420
|
+
A drag that touches many documents (painting pixels, drawing on a shared board) splits the same way: **the stroke is live, the cells are durable.** Keep the in-progress stroke in a per-person draft (`_id: "stroke:" + handle`) and `merge()` each new cell into it on `onPointerMove`; peers see it as it is drawn, because `merge()` broadcasts the draft and their `useLiveQuery` returns it as a row. On `onPointerUp`, `put()` one document per touched cell, then `merge()` the stroke back to empty. Never `put()` from `onPointerMove`: one quick drag becomes dozens of writes in flight.
|
|
421
|
+
|
|
422
|
+
Keep each cell its own document with a deterministic `_id` (`pixel-<x>-<y>`). Never fold the canvas into one document every visitor saves: the server keeps whichever save arrives last, so one visitor's save erases everyone else's strokes. An unsaved draft drops out for peers about 12 seconds after it stops updating.
|
|
423
|
+
|
|
424
|
+
A draft reaches only the peers already subscribed to its channel, and a channel's grant comes from a saved document. Create a fixed-id room record on that channel (see Required starting records) so even the first stroke on an empty canvas is seen live.
|
|
425
|
+
|
|
426
|
+
### `App.jsx`
|
|
427
|
+
```jsx
|
|
428
|
+
import React, { useEffect, useRef } from "react";
|
|
429
|
+
import { useFireproof } from "use-fireproof";
|
|
430
|
+
import { useViewer, useVibe } from "use-vibes";
|
|
431
|
+
import { Button } from "@vibes.diy/pouf";
|
|
432
|
+
|
|
433
|
+
const SIZE = 32;
|
|
434
|
+
const CELL = 16;
|
|
435
|
+
const COLOR = "#3a86ff";
|
|
436
|
+
|
|
437
|
+
export default function App() {
|
|
438
|
+
const { viewer, isViewerPending, requestLogin } = useViewer();
|
|
439
|
+
if (isViewerPending) return null;
|
|
440
|
+
if (!viewer) return <Button type="button" onClick={requestLogin}>Sign in to paint</Button>;
|
|
441
|
+
return <Canvas key={viewer.userHandle} me={viewer.userHandle} />; // fixed draft _id per painter
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function Canvas({ me }) {
|
|
445
|
+
// Many people write the same pixel docs: fail fast rather than replay a stale queued write.
|
|
446
|
+
const { database, useLiveQuery, useDocument, writeRejection } = useFireproof("pixels", { offlineQueue: false });
|
|
447
|
+
const { ready, can } = useVibe("pixels");
|
|
448
|
+
const canPaint = ready && can.create({ _id: "pixel-0-0", type: "pixel", x: 0, y: 0, color: COLOR, paintedBy: me }).ok;
|
|
449
|
+
const { docs: pixels } = useLiveQuery("type", { key: "pixel" });
|
|
450
|
+
const { docs: strokes } = useLiveQuery("type", { key: "stroke" });
|
|
451
|
+
const { doc: stroke, merge: mergeStroke } = useDocument({ _id: "stroke:" + me, type: "stroke", userHandle: me, color: COLOR, cells: [] });
|
|
452
|
+
const cellsRef = useRef([]);
|
|
453
|
+
const canvasRef = useRef(null);
|
|
454
|
+
|
|
455
|
+
useEffect(() => {
|
|
456
|
+
// The room record grants the channel, so peers subscribe before the first stroke.
|
|
457
|
+
const room = { _id: "canvas:main", type: "canvas" };
|
|
458
|
+
if (ready && can.create(room).ok) database.put(room);
|
|
459
|
+
}, [ready, can, database]);
|
|
460
|
+
|
|
461
|
+
useEffect(() => {
|
|
462
|
+
const ctx = canvasRef.current.getContext("2d");
|
|
463
|
+
ctx.fillStyle = "#ffffff";
|
|
464
|
+
ctx.fillRect(0, 0, SIZE * CELL, SIZE * CELL);
|
|
465
|
+
const fill = (x, y, color) => {
|
|
466
|
+
ctx.fillStyle = color;
|
|
467
|
+
ctx.fillRect(x * CELL, y * CELL, CELL, CELL);
|
|
468
|
+
};
|
|
469
|
+
for (const p of pixels) fill(p.x, p.y, p.color);
|
|
470
|
+
for (const s of strokes) if (s.userHandle !== me) for (const [x, y] of s.cells ?? []) fill(x, y, s.color);
|
|
471
|
+
for (const [x, y] of stroke.cells) fill(x, y, stroke.color);
|
|
472
|
+
}, [pixels, strokes, stroke, me]);
|
|
473
|
+
|
|
474
|
+
function extendStroke(e) {
|
|
475
|
+
if (!canPaint) return;
|
|
476
|
+
const r = canvasRef.current.getBoundingClientRect();
|
|
477
|
+
const x = Math.floor(((e.clientX - r.left) / r.width) * SIZE);
|
|
478
|
+
const y = Math.floor(((e.clientY - r.top) / r.height) * SIZE);
|
|
479
|
+
if (x < 0 || y < 0 || x >= SIZE || y >= SIZE) return;
|
|
480
|
+
if (cellsRef.current.some((c) => c[0] === x && c[1] === y)) return;
|
|
481
|
+
cellsRef.current = [...cellsRef.current, [x, y]];
|
|
482
|
+
mergeStroke({ cells: cellsRef.current }); // live for everyone, no write
|
|
483
|
+
}
|
|
484
|
+
async function endStroke() {
|
|
485
|
+
const cells = cellsRef.current;
|
|
486
|
+
cellsRef.current = [];
|
|
487
|
+
const writes = [];
|
|
488
|
+
for (const [x, y] of cells) writes.push(database.put({ _id: `pixel-${x}-${y}`, type: "pixel", x, y, color: COLOR, paintedBy: me }));
|
|
489
|
+
await Promise.all(writes); // the durable commit: one write per touched cell
|
|
490
|
+
if (cellsRef.current.length === 0) mergeStroke({ cells: [] }); // unless a newer stroke began
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return (
|
|
494
|
+
<div>
|
|
495
|
+
{writeRejection && <p role="status">{writeRejection.message}</p>}
|
|
496
|
+
<canvas
|
|
497
|
+
ref={canvasRef}
|
|
498
|
+
width={SIZE * CELL}
|
|
499
|
+
height={SIZE * CELL}
|
|
500
|
+
className="w-full max-w-lg aspect-square touch-none"
|
|
501
|
+
onPointerDown={(e) => {
|
|
502
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
503
|
+
extendStroke(e);
|
|
504
|
+
}}
|
|
505
|
+
onPointerMove={(e) => e.buttons && extendStroke(e)}
|
|
506
|
+
onPointerUp={endStroke}
|
|
507
|
+
onPointerCancel={endStroke}
|
|
508
|
+
/>
|
|
509
|
+
</div>
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
The access rules see the broadcast draft too, so `stroke` needs its own branch that binds the `_id` to the sender:
|
|
515
|
+
|
|
516
|
+
### `access.js`
|
|
517
|
+
```js
|
|
518
|
+
export default function (doc, oldDoc, user, ctx) {
|
|
519
|
+
if (!user) throw { forbidden: "sign in to paint" };
|
|
520
|
+
const inGrid = (n) => Number.isInteger(n) && n >= 0 && n < 32;
|
|
521
|
+
if (doc.type === "canvas") {
|
|
522
|
+
if (doc._id !== "canvas:main") throw { forbidden: "one canvas" };
|
|
523
|
+
return { channels: ["canvas"], grant: { public: ["canvas"] } };
|
|
524
|
+
}
|
|
525
|
+
if (doc.type === "stroke") {
|
|
526
|
+
if (doc._id !== "stroke:" + user.userHandle || doc.userHandle !== user.userHandle) throw { forbidden: "not your stroke" };
|
|
527
|
+
if (!Array.isArray(doc.cells) || doc.cells.length > 1024 || !doc.cells.every((c) => Array.isArray(c) && c.length === 2 && inGrid(c[0]) && inGrid(c[1]))) throw { forbidden: "bad stroke" };
|
|
528
|
+
return { channels: ["canvas"], grant: { public: ["canvas"] } };
|
|
529
|
+
}
|
|
530
|
+
if (doc.type === "pixel") {
|
|
531
|
+
if (!inGrid(doc.x) || !inGrid(doc.y) || doc._id !== `pixel-${doc.x}-${doc.y}`) throw { forbidden: "pixel out of bounds" };
|
|
532
|
+
if (doc.paintedBy !== user.userHandle) throw { forbidden: "not your paint" };
|
|
533
|
+
return { channels: ["canvas"], grant: { public: ["canvas"] } };
|
|
534
|
+
}
|
|
535
|
+
throw { forbidden: "unknown document type" };
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
385
539
|
### Query Data
|
|
386
540
|
|
|
387
541
|
Data is queried by sorted indexes defined by the application. Sort by strings, numbers, or booleans, as well as arrays for grouping. Use numbers when possible for sorting continuous data. You can use the `_id` field for temporal sorting so you don't have to write code to get simple recent document lists, as in the basic example above.
|
|
@@ -620,7 +774,7 @@ Building an image uploader with `_files`:
|
|
|
620
774
|
```jsx
|
|
621
775
|
import React from "react";
|
|
622
776
|
import { useFireproof } from "use-fireproof";
|
|
623
|
-
import { Button, Card,
|
|
777
|
+
import { Button, Card, ErrorNote, Field, Figure, Grid, Heading, Input, Stack, Text } from "@vibes.diy/pouf";
|
|
624
778
|
|
|
625
779
|
export default function App() {
|
|
626
780
|
const { useDocument, useLiveQuery } = useFireproof("imageUploads");
|
|
@@ -654,46 +808,45 @@ export default function App() {
|
|
|
654
808
|
};
|
|
655
809
|
|
|
656
810
|
const c = {
|
|
657
|
-
page: "bg-background text-foreground font-sans",
|
|
658
|
-
meta: "text-muted-foreground",
|
|
811
|
+
page: "min-h-screen p-6 max-w-lg mx-auto bg-background text-foreground font-sans",
|
|
659
812
|
};
|
|
660
813
|
|
|
661
814
|
return (
|
|
662
|
-
<div className={
|
|
663
|
-
<
|
|
664
|
-
|
|
665
|
-
<
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
</
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
</
|
|
815
|
+
<div className={c.page}>
|
|
816
|
+
<Stack gap={5}>
|
|
817
|
+
<Heading level={1}>Image Uploader</Heading>
|
|
818
|
+
<form onSubmit={onSubmit}>
|
|
819
|
+
<Stack gap={3}>
|
|
820
|
+
{/* A file picker hands over files, not a value, so it stays the browser's own control inside the Field. */}
|
|
821
|
+
<Field label="Photo">{(id, describedBy) => <input id={id} aria-describedby={describedBy} type="file" accept="image/*" onChange={onPickFile} />}</Field>
|
|
822
|
+
<Field label="Caption">
|
|
823
|
+
{(id, describedBy) => <Input id={id} describedBy={describedBy} value={doc.caption} onChange={(caption) => merge({ caption })} />}
|
|
824
|
+
</Field>
|
|
825
|
+
<Button type="submit" size="lg">
|
|
826
|
+
Upload
|
|
827
|
+
</Button>
|
|
828
|
+
{saveError && <ErrorNote>{saveError}</ErrorNote>}
|
|
829
|
+
</Stack>
|
|
830
|
+
</form>
|
|
831
|
+
|
|
832
|
+
<Heading level={2}>Recent Uploads</Heading>
|
|
833
|
+
<Grid cols={2} gap={4}>
|
|
834
|
+
{docs.map((d) => (
|
|
835
|
+
<Card key={d._id}>
|
|
836
|
+
<Stack gap={2}>
|
|
837
|
+
{d._files?.photo?.url && <Figure src={d._files.photo.url} alt={d.caption || "upload"} width={400} height={300} />}
|
|
838
|
+
<Text muted>{d.caption || "No caption"}</Text>
|
|
839
|
+
</Stack>
|
|
840
|
+
</Card>
|
|
841
|
+
))}
|
|
842
|
+
</Grid>
|
|
843
|
+
</Stack>
|
|
691
844
|
</div>
|
|
692
845
|
);
|
|
693
846
|
}
|
|
694
847
|
```
|
|
695
848
|
|
|
696
|
-
For multi-file uploads (
|
|
849
|
+
For multi-file uploads (the file picker with `multiple`), build the `_files` map keyed by filename and iterate with `Object.entries(doc._files)` to render each entry.
|
|
697
850
|
|
|
698
851
|
Adding multi-file support:
|
|
699
852
|
|
|
@@ -756,7 +909,8 @@ Code listing for a bird sightings log App.jsx. Note the code ordering: hooks, th
|
|
|
756
909
|
```js
|
|
757
910
|
import React from "react";
|
|
758
911
|
import { useFireproof } from "use-fireproof";
|
|
759
|
-
import { Button,
|
|
912
|
+
import { Button, Field, Heading, Input, Row, RowCard, Spacer, Stack, Text } from "@vibes.diy/pouf";
|
|
913
|
+
import { Checkbox } from "@vibes.diy/pouf/interactive";
|
|
760
914
|
|
|
761
915
|
export default function App() {
|
|
762
916
|
// 1. Hooks and document shapes
|
|
@@ -779,8 +933,8 @@ export default function App() {
|
|
|
779
933
|
});
|
|
780
934
|
|
|
781
935
|
// 2. Event handlers
|
|
782
|
-
const handleInputChange = (
|
|
783
|
-
mergeNewSighting({ species
|
|
936
|
+
const handleInputChange = (species) => {
|
|
937
|
+
mergeNewSighting({ species });
|
|
784
938
|
};
|
|
785
939
|
|
|
786
940
|
const handleSubmit = (e) => {
|
|
@@ -788,48 +942,46 @@ export default function App() {
|
|
|
788
942
|
submitNewSighting();
|
|
789
943
|
};
|
|
790
944
|
|
|
791
|
-
// 3. ClassNames — right before JSX, for the plain elements only; the
|
|
945
|
+
// 3. ClassNames — right before JSX, for the plain elements only; the pouf
|
|
792
946
|
// components carry their own surface, ink and shape.
|
|
793
947
|
const c = {
|
|
794
|
-
page: "bg-background text-foreground font-sans",
|
|
795
|
-
meta: "text-muted-foreground",
|
|
948
|
+
page: "min-h-screen max-w-md mx-auto p-6 bg-background text-foreground font-sans",
|
|
796
949
|
};
|
|
797
950
|
|
|
798
951
|
// 4. JSX return
|
|
799
952
|
return (
|
|
800
|
-
<div className={
|
|
801
|
-
<
|
|
802
|
-
|
|
803
|
-
<
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
</ul>
|
|
953
|
+
<div className={c.page}>
|
|
954
|
+
<Stack gap={5}>
|
|
955
|
+
<Heading level={1}>Bird Sightings</Heading>
|
|
956
|
+
<form onSubmit={handleSubmit}>
|
|
957
|
+
<Row gap={2} align="top">
|
|
958
|
+
<Field label="Species">
|
|
959
|
+
{(id, describedBy) => <Input id={id} describedBy={describedBy} onChange={handleInputChange} value={newSighting.species} />}
|
|
960
|
+
</Field>
|
|
961
|
+
<Button type="submit">Add</Button>
|
|
962
|
+
</Row>
|
|
963
|
+
</form>
|
|
964
|
+
<Stack gap={3}>
|
|
965
|
+
{sightings.map((doc) => (
|
|
966
|
+
<RowCard key={doc._id}>
|
|
967
|
+
<Row gap={3} align="center">
|
|
968
|
+
<Checkbox
|
|
969
|
+
label={doc.species}
|
|
970
|
+
checked={doc.confirmed}
|
|
971
|
+
onChange={(confirmed) => database.put({ ...doc, confirmed: confirmed === true })}
|
|
972
|
+
/>
|
|
973
|
+
<Spacer />
|
|
974
|
+
<Button type="button" variant="quiet" tone="down" size="sm" onClick={() => database.del(doc._id)}>
|
|
975
|
+
Delete
|
|
976
|
+
</Button>
|
|
977
|
+
</Row>
|
|
978
|
+
<Text size="sm" muted>
|
|
979
|
+
{new Date(doc.createdAt).toISOString()}
|
|
980
|
+
</Text>
|
|
981
|
+
</RowCard>
|
|
982
|
+
))}
|
|
983
|
+
</Stack>
|
|
984
|
+
</Stack>
|
|
833
985
|
</div>
|
|
834
986
|
);
|
|
835
987
|
}
|
package/llms/image-gen.md
CHANGED
|
@@ -145,23 +145,26 @@ When each item's substance is its words — a generated joke, a short verse, a p
|
|
|
145
145
|
|
|
146
146
|
```jsx
|
|
147
147
|
import { ImgGen, useImgGen } from "use-vibes";
|
|
148
|
+
import { Button, RowCard, Text } from "@vibes.diy/pouf";
|
|
148
149
|
|
|
149
150
|
function PunCard({ pun, database }) {
|
|
150
151
|
const [wanted, setWanted] = React.useState(false);
|
|
151
152
|
const { document: saved } = useImgGen({ _id: pun._id, database });
|
|
152
153
|
const hasPicture = Object.keys(saved?._files ?? {}).length > 0;
|
|
153
154
|
return (
|
|
154
|
-
<
|
|
155
|
-
<
|
|
155
|
+
<RowCard>
|
|
156
|
+
<Text>{pun.text}</Text>
|
|
156
157
|
{wanted || hasPicture ? (
|
|
157
158
|
<figure>
|
|
158
159
|
<ImgGen _id={pun._id} database={database} prompt={`A cheerful cartoon of this pun: ${pun.text}`} />
|
|
159
160
|
{hasPicture ? null : <figcaption>Making a picture…</figcaption>}
|
|
160
161
|
</figure>
|
|
161
162
|
) : (
|
|
162
|
-
<button onClick={() => setWanted(true)}>
|
|
163
|
+
<Button type="button" size="sm" variant="quiet" onClick={() => setWanted(true)}>
|
|
164
|
+
Picture this
|
|
165
|
+
</Button>
|
|
163
166
|
)}
|
|
164
|
-
</
|
|
167
|
+
</RowCard>
|
|
165
168
|
);
|
|
166
169
|
}
|
|
167
170
|
```
|
|
@@ -209,37 +212,51 @@ public facts. Public viewers render its `_files` URL with `<img>`; keep generati
|
|
|
209
212
|
and revision controls on the creator's draft. Share the same `useImgGen` hook
|
|
210
213
|
result between preview, download and publication so they select the same version.
|
|
211
214
|
|
|
212
|
-
## Access Functions and
|
|
215
|
+
## Access Functions and pictures
|
|
213
216
|
|
|
214
|
-
|
|
217
|
+
Your `access.js` governs your app's own records — the story, the hat, the roast — and ImgGen keeps its pictures beside them in `media:<database>`, under the platform's media rules: every generation, a regenerate or an img2img edit included, is a new `version` record authored by whoever asked for it, and each person keeps their own `pointer` record for the picture they are viewing. So a type-keyed access function with one branch per record type already serves an image app, author checks and all; give each record the author and channel its own sharing calls for. When pictures should be more open or more closed than their record, write the access skill's media export, which checks that `doc.authorHandle` is the caller and admits both `version` and `pointer` records.
|
|
215
218
|
|
|
216
|
-
|
|
217
|
-
- **Standalone image** (no `_id`, or a brand-new doc): the doc is written with `type: "image"`. A type-keyed access function will **reject** this unless it explicitly permits `"image"`. Add an `"image"` branch (with the grant/channel you want image docs to live on) so the write is accepted — otherwise image generation fails silently with `Failed to put document:`.
|
|
219
|
+
## Choosing a Model
|
|
218
220
|
|
|
219
|
-
|
|
221
|
+
Every `<ImgGen>` draws with the app's default image model. `listModels({ kind: "image" })` (from `call-ai`, beside `callAI`) returns the image models this app can use, the app's own choice first, and each entry's `id` goes straight into the `model` prop: `<ImgGen prompt="An astronaut riding a horse" model={m.id} />`. The entry with `isDefault: true` is the one ImgGen uses when `model` is left out; with `images` present the default is the platform's image-edit model instead, so img2img leaves `model` out (see above). When the app's maker saved their own provider key, it covers the image model they chose in settings; other models run on the person's credits. The callAI skill's "Choosing a model" section lists every field.
|
|
220
222
|
|
|
221
|
-
|
|
223
|
+
To compare image models, call `listModels({ kind: "image" })` and render one `<ImgGen>` per entry with `model={m.id}`. Every picture shares one prompt, each card is labelled with `m.name`, and each `_id` pairs the prompt with the model so a reload shows the saved pictures:
|
|
222
224
|
|
|
223
|
-
|
|
225
|
+
### `App.jsx`
|
|
226
|
+
```jsx
|
|
227
|
+
import React, { useEffect, useState } from "react";
|
|
228
|
+
import { ImgGen } from "use-vibes";
|
|
229
|
+
import { listModels } from "call-ai";
|
|
230
|
+
import { Button, Card, Field, Grid, Input, Stack, Text } from "@vibes.diy/pouf";
|
|
224
231
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
+
export default function App() {
|
|
233
|
+
const [models, setModels] = useState([]);
|
|
234
|
+
const [message, setMessage] = useState("");
|
|
235
|
+
const [draft, setDraft] = useState("a penguin riding a bicycle");
|
|
236
|
+
const [prompt, setPrompt] = useState("a penguin riding a bicycle");
|
|
237
|
+
useEffect(() => {
|
|
238
|
+
listModels({ kind: "image" })
|
|
239
|
+
.then(setModels)
|
|
240
|
+
.catch((error) => setMessage(error.message));
|
|
241
|
+
}, []);
|
|
242
|
+
return (
|
|
243
|
+
<Stack>
|
|
244
|
+
<Field label="Picture">{(id) => <Input id={id} value={draft} onChange={setDraft} />}</Field>
|
|
245
|
+
<Button onClick={() => setPrompt(draft)} disabled={draft.trim().length === 0}>Draw it with every model</Button>
|
|
246
|
+
<Text>{message}</Text>
|
|
247
|
+
<Grid cols={3}>
|
|
248
|
+
{models.map((m) => (
|
|
249
|
+
<Card key={m.id}>
|
|
250
|
+
<ImgGen _id={`audition:${prompt}:${m.id}`} prompt={prompt} model={m.id} alt={`${prompt}, drawn by ${m.name}`} />
|
|
251
|
+
<Text>{m.isDefault ? `${m.name} · this app's default` : m.name}</Text>
|
|
252
|
+
</Card>
|
|
253
|
+
))}
|
|
254
|
+
</Grid>
|
|
255
|
+
</Stack>
|
|
256
|
+
);
|
|
232
257
|
}
|
|
233
258
|
```
|
|
234
259
|
|
|
235
|
-
The platform predicate `ctx.isImgGenVersionAppend` accepts exactly one legitimate ImgGen version append by a non-author — prior versions/files and all other fields must be unchanged, so a field you didn't anticipate stays author-protected by default; deletes stay author-only. Because `versions`/`currentVersion` are among the version fields it allows, a non-author can also advance the DISPLAYED version (the shared-generation feature); guard `currentVersion` specifically if you need author-only display control.
|
|
236
|
-
|
|
237
|
-
## Choosing a Model
|
|
238
|
-
|
|
239
|
-
Override the model per component: `<ImgGen prompt="An astronaut riding a horse" model="openai/gpt-5-image-mini" />`
|
|
240
|
-
|
|
241
|
-
Model ids follow the `provider/model-name` form from the platform's model catalog. Unknown ids surface as an error in the component's error UI.
|
|
242
|
-
|
|
243
260
|
#### Props
|
|
244
261
|
|
|
245
262
|
- `prompt`: text prompt (required unless `_id` is provided)
|
|
@@ -248,4 +265,4 @@ Model ids follow the `provider/model-name` form from the platform's model catalo
|
|
|
248
265
|
- `database`: Fireproof db name or instance (default `"ImgGen"`)
|
|
249
266
|
- `className`, `alt`, `style`: standard image styling
|
|
250
267
|
- `showControls`: toggle regenerate + version nav (default `true`)
|
|
251
|
-
- `model`:
|
|
268
|
+
- `model`: an `id` from `listModels({ kind: "image" })` to draw with instead of the app's default
|