@vibes.diy/prompts 14.1.61 → 14.2.0
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/callai.initial.md +176 -44
- package/llms/callai.md +176 -44
- package/llms/fireproof.initial.md +151 -38
- package/llms/fireproof.md +150 -37
- package/package.json +4 -4
- package/prompts.js +2 -2
- package/prompts.js.map +1 -1
- package/system-prompt-initial-oneshot.md +13 -4
- package/system-prompt-initial.md +13 -4
- package/system-prompt.md +28 -20
- package/themes/looks/house/brief.md +3 -1
package/llms/callai.initial.md
CHANGED
|
@@ -2,59 +2,191 @@
|
|
|
2
2
|
|
|
3
3
|
The `callAI` function asks an AI model and returns a string. Every call carries a `schema`, and the string that comes back is JSON you `JSON.parse()`. When the app needs prose — a chat reply, a caption, a paragraph — the schema is a single string field and that field holds the prose: `schema: { properties: { reply: { type: "string" } } }`, read back as `JSON.parse(response).reply`.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Text to records
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
import { callAI } from "call-ai";
|
|
7
|
+
Wire these visible actions in the first working app, using its own record names. One composer has Add and Ask actions. Add extracts and normalizes facts, saves each record immediately, and shows saved records with edit actions; one item and a pasted batch share this action. Each saved row has Change by sentence: selecting it changes Add to Apply correction, which saves onto that same record id. Ask sends the typed question with the relevant saved records and displays a read-only answer beside the composer. Edit opens the selected row’s populated fields. One parent-owned Undo restores the latest successful creation batch or the previous document from either a sentence correction or a field edit.
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
9
|
+
A record starts as a sentence. For structured intake, use one text box or the app's chat composer, extract the person's facts into the document's fields, normalize them, and save immediately in that same submit action. Show the saved record with editing controls. A pasted list becomes individual records through an array schema. Field-by-field controls belong to the edit view of a saved record. One new record and a pasted batch use the same composer. Opening a saved row’s Edit action reveals its populated field controls. A message whose text IS the record can save directly; extraction is useful when the person would otherwise fill several fields. Requested AI generation remains available for creative work.
|
|
10
|
+
|
|
11
|
+
The extraction schema names the same editable fields the app writes to its documents. Normalize the parsed output into that shape before saving or rendering: check strings, finite numbers, enum values and arrays (including their members), and project only the fields the app owns. Keep `_id`, authorship, parent ids and sharing fields under app control. Submitting the sentence commits the normalized records automatically. The saved card shows the understood facts and is the confirmation; editing follows persistence. For an edit by sentence, pass the current editable fields plus the correction through the same single-record schema, then write the normalized fields onto that saved record's trusted identity.
|
|
12
|
+
|
|
13
|
+
After each successful creation, sentence correction or field edit, the parent owns one Undo action. Keep the created ids or the previous document until the next successful write replaces them; a detail editor reports its previous document to that parent after its save succeeds. Undo deletes the created records or puts the previous document back. A value the person already knows — a number, a date, a time, a choice, an on/off state — is a control, never a schema field a sentence is asked to fill; the schema extracts what the person described and leaves what they can set.
|
|
14
|
+
|
|
15
|
+
This complete example covers **one sentence → one task**, **pasted list → many tasks**, **a selected task + correction → the same task**, **questions from the same box**, and **Undo after a save**. Every new submission uses the array schema, whether it names one task or several. The person sets a due date in the saved task’s date control. A malformed response leaves the typed sentence and saved records available; the waiting state ends when the request settles.
|
|
16
|
+
|
|
17
|
+
```jsx file=App.jsx
|
|
18
|
+
import React, { useState } from "react";
|
|
19
|
+
import { callAI } from "call-ai";
|
|
20
|
+
import { useFireproof } from "use-fireproof";
|
|
21
|
+
import { useVibe } from "use-vibes";
|
|
22
|
+
|
|
23
|
+
const taskSchema = {
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
title: { type: "string", description: "The task in the person's words" },
|
|
27
|
+
details: { type: "string", description: "Supporting details in the person’s words" },
|
|
19
28
|
},
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
required: ["title", "details"],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function normalizeTask(raw) {
|
|
33
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
34
|
+
const title = typeof raw.title === "string" ? raw.title.trim() : "";
|
|
35
|
+
const details = typeof raw.details === "string" ? raw.details.trim() : "";
|
|
36
|
+
return title.length > 0 ? { title, details } : undefined;
|
|
37
|
+
}
|
|
24
38
|
|
|
25
|
-
|
|
39
|
+
// Single-record extraction and edit-by-sentence share a schema and normalizer.
|
|
40
|
+
async function readTask(sentence, current) {
|
|
41
|
+
const facts = current === undefined ? {} : { title: current.title, details: current.details };
|
|
42
|
+
const response = await callAI(
|
|
43
|
+
`Extract one task’s title and supporting details from the person's words. Update only what the correction mentions when a current task is supplied. Today: ${new Date().toISOString().slice(0, 10)}. Current: ${JSON.stringify(facts)}. Person: ${sentence}`,
|
|
44
|
+
{ schema: taskSchema }
|
|
45
|
+
);
|
|
46
|
+
const task = normalizeTask(JSON.parse(response));
|
|
47
|
+
if (task === undefined) throw new Error("Add a task in your own words and try again.");
|
|
48
|
+
return task;
|
|
49
|
+
}
|
|
26
50
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
schema: {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
51
|
+
// A pasted list uses an array of the same flat document fields.
|
|
52
|
+
async function readTasks(sentence) {
|
|
53
|
+
const response = await callAI(
|
|
54
|
+
`Extract every task from these words as a separate item, including when the person names just one task. Keep their title and supporting details. Today: ${new Date().toISOString().slice(0, 10)}. List: ${sentence}`,
|
|
55
|
+
{ schema: { properties: { items: { type: "array", items: taskSchema } }, required: ["items"] } }
|
|
56
|
+
);
|
|
57
|
+
const raw = JSON.parse(response);
|
|
58
|
+
const items = Array.isArray(raw?.items) ? raw.items.map(normalizeTask) : [];
|
|
59
|
+
if (items.length === 0 || items.some((item) => item === undefined)) {
|
|
60
|
+
throw new Error("Describe the tasks you want to add and try again.");
|
|
61
|
+
}
|
|
62
|
+
return items;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function TaskCard({ task, database, can, onCorrect, onSaved, disabled }) {
|
|
66
|
+
const { useDocument } = useFireproof(database);
|
|
67
|
+
const { doc, merge, save } = useDocument(task);
|
|
68
|
+
const [editing, setEditing] = useState(false);
|
|
69
|
+
return <article>
|
|
70
|
+
<h2>{task.title}</h2><p>{task.details}</p><p>{task.dueDate}</p>
|
|
71
|
+
{can.edit(task).ok && <>
|
|
72
|
+
<button type="button" disabled={disabled} onClick={() => onCorrect(task)}>Change by sentence</button>
|
|
73
|
+
<button type="button" disabled={disabled} onClick={() => setEditing(true)}>Edit details</button>
|
|
74
|
+
{editing && <form onSubmit={async (event) => { event.preventDefault(); if (disabled) return; await save(); onSaved(task); setEditing(false); }}>
|
|
75
|
+
<input disabled={disabled} aria-label="Task" value={doc.title} onChange={(event) => merge({ title: event.target.value })} />
|
|
76
|
+
<textarea disabled={disabled} aria-label="Details" value={doc.details} onChange={(event) => merge({ details: event.target.value })} />
|
|
77
|
+
<input disabled={disabled} aria-label="Due date" type="date" value={doc.dueDate} onChange={(event) => merge({ dueDate: event.target.value })} />
|
|
78
|
+
<button type="submit" disabled={disabled}>Save details</button>
|
|
79
|
+
</form>}
|
|
80
|
+
</>}
|
|
81
|
+
</article>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export default function App() {
|
|
85
|
+
const { database, useLiveQuery } = useFireproof("tasks");
|
|
86
|
+
const { me, can, ready } = useVibe("tasks");
|
|
87
|
+
const { docs } = useLiveQuery("type", { key: "task" });
|
|
88
|
+
const [sentence, setSentence] = useState("");
|
|
89
|
+
const [current, setCurrent] = useState(undefined);
|
|
90
|
+
const [pending, setPending] = useState(undefined);
|
|
91
|
+
const [answer, setAnswer] = useState("");
|
|
92
|
+
const [busy, setBusy] = useState(false);
|
|
93
|
+
const [message, setMessage] = useState("");
|
|
94
|
+
// Creation, sentence correction and detail editing replace this one Undo receipt after a successful write.
|
|
95
|
+
const [lastWrite, setLastWrite] = useState(undefined);
|
|
96
|
+
async function submitSentence(event) {
|
|
97
|
+
event.preventDefault();
|
|
98
|
+
if (busy || !ready) return;
|
|
99
|
+
setBusy(true); setMessage("");
|
|
100
|
+
try {
|
|
101
|
+
// A failed local batch resumes its unsaved tail without extracting or duplicating earlier records.
|
|
102
|
+
let operation = pending;
|
|
103
|
+
if (operation === undefined) {
|
|
104
|
+
const target = current === undefined ? undefined : docs.find((doc) => doc._id === current._id);
|
|
105
|
+
if (current !== undefined && target === undefined) throw new Error("Choose a saved task to correct.");
|
|
106
|
+
const items = target === undefined ? await readTasks(sentence) : [await readTask(sentence, target)];
|
|
107
|
+
const remaining = items.map((item) => target === undefined
|
|
108
|
+
? { _id: crypto.randomUUID(), type: "task", title: item.title, details: item.details, dueDate: "", authorHandle: me?.userHandle }
|
|
109
|
+
: { ...target, title: item.title, details: item.details });
|
|
110
|
+
operation = { remaining, created: [], replaced: target };
|
|
111
|
+
}
|
|
112
|
+
if (operation.remaining.some((doc) => operation.replaced === undefined ? can.create(doc).ok === false : can.edit(doc).ok === false)) {
|
|
113
|
+
throw new Error("This task is not editable right now.");
|
|
114
|
+
}
|
|
115
|
+
setPending(operation);
|
|
116
|
+
while (operation.remaining.length > 0) {
|
|
117
|
+
const doc = operation.remaining[0];
|
|
118
|
+
await database.put(doc);
|
|
119
|
+
operation = { ...operation, remaining: operation.remaining.slice(1), created: operation.replaced === undefined ? [...operation.created, doc._id] : [] };
|
|
120
|
+
setPending(operation);
|
|
121
|
+
setLastWrite({ created: operation.created, replaced: operation.replaced });
|
|
122
|
+
}
|
|
123
|
+
setPending(undefined);
|
|
124
|
+
setSentence(""); setCurrent(undefined);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
// Preserve the words and any unsaved tail on local errors. Synced refusals converge through useLiveQuery.
|
|
127
|
+
setMessage(error.reason === "refusal" ? "Try describing the task another way." : error.message);
|
|
128
|
+
} finally { setBusy(false); }
|
|
47
129
|
}
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
|
|
130
|
+
async function askRecords() {
|
|
131
|
+
if (busy || !ready || pending !== undefined || sentence.trim().length === 0) return;
|
|
132
|
+
setBusy(true); setMessage("");
|
|
133
|
+
try {
|
|
134
|
+
const response = await callAI(
|
|
135
|
+
`Answer from these records. Today: ${new Date().toISOString().slice(0, 10)}. Records: ${JSON.stringify(docs.map(({ title, details, dueDate }) => ({ title, details, dueDate })))}. Question: ${sentence}`,
|
|
136
|
+
{ schema: { properties: { answer: { type: "string" } }, required: ["answer"] } }
|
|
137
|
+
);
|
|
138
|
+
const raw = JSON.parse(response);
|
|
139
|
+
if (typeof raw?.answer !== "string") throw new Error("Ask your question again.");
|
|
140
|
+
setAnswer(raw.answer); setSentence("");
|
|
141
|
+
} catch (error) {
|
|
142
|
+
setMessage(error.message);
|
|
143
|
+
} finally { setBusy(false); }
|
|
144
|
+
}
|
|
145
|
+
// A new record is one del away; a corrected one is one put of its previous version away.
|
|
146
|
+
async function undoLast() {
|
|
147
|
+
if (busy || lastWrite === undefined) return;
|
|
148
|
+
setBusy(true); setMessage("");
|
|
149
|
+
try {
|
|
150
|
+
for (const id of lastWrite.created) await database.del(id);
|
|
151
|
+
if (lastWrite.replaced !== undefined) await database.put(lastWrite.replaced);
|
|
152
|
+
setLastWrite(undefined); setPending(undefined); setCurrent(undefined);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
setMessage(error.message);
|
|
155
|
+
} finally { setBusy(false); }
|
|
156
|
+
}
|
|
157
|
+
return <main>
|
|
158
|
+
<h1>Tasks in your own words</h1>
|
|
159
|
+
<form onSubmit={submitSentence}>
|
|
160
|
+
<textarea aria-label="Describe tasks, a correction, or ask a question" value={sentence} disabled={busy || pending !== undefined} onChange={(event) => setSentence(event.target.value)} />
|
|
161
|
+
{current !== undefined && <p>Changing: {current.title} <button type="button" disabled={busy || pending !== undefined} onClick={() => { setCurrent(undefined); setSentence(""); }}>Add a new task</button></p>}
|
|
162
|
+
<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>
|
|
163
|
+
<button type="button" disabled={!ready || busy || pending !== undefined || sentence.trim().length === 0} onClick={askRecords}>Ask about tasks</button>
|
|
164
|
+
</form>
|
|
165
|
+
<p>{answer}</p>
|
|
166
|
+
<p role="status">{message}</p>
|
|
167
|
+
{lastWrite !== undefined && <button type="button" disabled={busy} onClick={undoLast}>Undo</button>}
|
|
168
|
+
{pending !== undefined && !busy && <button type="button" onClick={() => { setPending(undefined); setSentence(""); setCurrent(undefined); }}>Discard unsaved tasks</button>}
|
|
169
|
+
{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(""); }} />)}
|
|
170
|
+
</main>;
|
|
171
|
+
}
|
|
51
172
|
```
|
|
52
173
|
|
|
53
|
-
|
|
174
|
+
For nested document shapes, normalize every collection at its boundary: `Array.isArray(raw.days)` before mapping days, and the same check for each day's `stops`. Persist the normalized records in the submit handler, then render their saved cards. Extend the schema and normalizer together as the app's editable fields evolve.
|
|
175
|
+
|
|
176
|
+
## Ask about your records
|
|
177
|
+
|
|
178
|
+
The same box answers a question about the records the app holds: send the records the answer could need with the question, and ask for the answer in a schema with one string field. Bound what you send (the visible list, or the records a query selected), name today's date when the question could involve time, and render the answer as text beside the box, never as a saved record.
|
|
54
179
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
180
|
+
```jsx
|
|
181
|
+
const { docs } = useLiveQuery("type", { key: "task" });
|
|
182
|
+
async function askRecords(question) {
|
|
183
|
+
const response = await callAI(
|
|
184
|
+
`Answer the question from these records only; say so when they do not answer it. Today: ${new Date().toISOString().slice(0, 10)}. Records: ${JSON.stringify(docs.map(({ title, dueDate }) => ({ title, dueDate })))}. Question: ${question}`,
|
|
185
|
+
{ schema: { properties: { answer: { type: "string" } } } }
|
|
186
|
+
);
|
|
187
|
+
return JSON.parse(response).answer;
|
|
188
|
+
}
|
|
189
|
+
```
|
|
58
190
|
|
|
59
191
|
## Reading an Image
|
|
60
192
|
|
package/llms/callai.md
CHANGED
|
@@ -2,59 +2,191 @@
|
|
|
2
2
|
|
|
3
3
|
The `callAI` function asks an AI model and returns a string. Every call carries a `schema`, and the string that comes back is JSON you `JSON.parse()`. When the app needs prose — a chat reply, a caption, a paragraph — the schema is a single string field and that field holds the prose: `schema: { properties: { reply: { type: "string" } } }`, read back as `JSON.parse(response).reply`.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Text to records
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
import { callAI } from "call-ai";
|
|
7
|
+
Wire these visible actions in the first working app, using its own record names. One composer has Add and Ask actions. Add extracts and normalizes facts, saves each record immediately, and shows saved records with edit actions; one item and a pasted batch share this action. Each saved row has Change by sentence: selecting it changes Add to Apply correction, which saves onto that same record id. Ask sends the typed question with the relevant saved records and displays a read-only answer beside the composer. Edit opens the selected row’s populated fields. One parent-owned Undo restores the latest successful creation batch or the previous document from either a sentence correction or a field edit.
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
9
|
+
A record starts as a sentence. For structured intake, use one text box or the app's chat composer, extract the person's facts into the document's fields, normalize them, and save immediately in that same submit action. Show the saved record with editing controls. A pasted list becomes individual records through an array schema. Field-by-field controls belong to the edit view of a saved record. One new record and a pasted batch use the same composer. Opening a saved row’s Edit action reveals its populated field controls. A message whose text IS the record can save directly; extraction is useful when the person would otherwise fill several fields. Requested AI generation remains available for creative work.
|
|
10
|
+
|
|
11
|
+
The extraction schema names the same editable fields the app writes to its documents. Normalize the parsed output into that shape before saving or rendering: check strings, finite numbers, enum values and arrays (including their members), and project only the fields the app owns. Keep `_id`, authorship, parent ids and sharing fields under app control. Submitting the sentence commits the normalized records automatically. The saved card shows the understood facts and is the confirmation; editing follows persistence. For an edit by sentence, pass the current editable fields plus the correction through the same single-record schema, then write the normalized fields onto that saved record's trusted identity.
|
|
12
|
+
|
|
13
|
+
After each successful creation, sentence correction or field edit, the parent owns one Undo action. Keep the created ids or the previous document until the next successful write replaces them; a detail editor reports its previous document to that parent after its save succeeds. Undo deletes the created records or puts the previous document back. A value the person already knows — a number, a date, a time, a choice, an on/off state — is a control, never a schema field a sentence is asked to fill; the schema extracts what the person described and leaves what they can set.
|
|
14
|
+
|
|
15
|
+
This complete example covers **one sentence → one task**, **pasted list → many tasks**, **a selected task + correction → the same task**, **questions from the same box**, and **Undo after a save**. Every new submission uses the array schema, whether it names one task or several. The person sets a due date in the saved task’s date control. A malformed response leaves the typed sentence and saved records available; the waiting state ends when the request settles.
|
|
16
|
+
|
|
17
|
+
```jsx file=App.jsx
|
|
18
|
+
import React, { useState } from "react";
|
|
19
|
+
import { callAI } from "call-ai";
|
|
20
|
+
import { useFireproof } from "use-fireproof";
|
|
21
|
+
import { useVibe } from "use-vibes";
|
|
22
|
+
|
|
23
|
+
const taskSchema = {
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
title: { type: "string", description: "The task in the person's words" },
|
|
27
|
+
details: { type: "string", description: "Supporting details in the person’s words" },
|
|
19
28
|
},
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
required: ["title", "details"],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function normalizeTask(raw) {
|
|
33
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
34
|
+
const title = typeof raw.title === "string" ? raw.title.trim() : "";
|
|
35
|
+
const details = typeof raw.details === "string" ? raw.details.trim() : "";
|
|
36
|
+
return title.length > 0 ? { title, details } : undefined;
|
|
37
|
+
}
|
|
24
38
|
|
|
25
|
-
|
|
39
|
+
// Single-record extraction and edit-by-sentence share a schema and normalizer.
|
|
40
|
+
async function readTask(sentence, current) {
|
|
41
|
+
const facts = current === undefined ? {} : { title: current.title, details: current.details };
|
|
42
|
+
const response = await callAI(
|
|
43
|
+
`Extract one task’s title and supporting details from the person's words. Update only what the correction mentions when a current task is supplied. Today: ${new Date().toISOString().slice(0, 10)}. Current: ${JSON.stringify(facts)}. Person: ${sentence}`,
|
|
44
|
+
{ schema: taskSchema }
|
|
45
|
+
);
|
|
46
|
+
const task = normalizeTask(JSON.parse(response));
|
|
47
|
+
if (task === undefined) throw new Error("Add a task in your own words and try again.");
|
|
48
|
+
return task;
|
|
49
|
+
}
|
|
26
50
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
schema: {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
51
|
+
// A pasted list uses an array of the same flat document fields.
|
|
52
|
+
async function readTasks(sentence) {
|
|
53
|
+
const response = await callAI(
|
|
54
|
+
`Extract every task from these words as a separate item, including when the person names just one task. Keep their title and supporting details. Today: ${new Date().toISOString().slice(0, 10)}. List: ${sentence}`,
|
|
55
|
+
{ schema: { properties: { items: { type: "array", items: taskSchema } }, required: ["items"] } }
|
|
56
|
+
);
|
|
57
|
+
const raw = JSON.parse(response);
|
|
58
|
+
const items = Array.isArray(raw?.items) ? raw.items.map(normalizeTask) : [];
|
|
59
|
+
if (items.length === 0 || items.some((item) => item === undefined)) {
|
|
60
|
+
throw new Error("Describe the tasks you want to add and try again.");
|
|
61
|
+
}
|
|
62
|
+
return items;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function TaskCard({ task, database, can, onCorrect, onSaved, disabled }) {
|
|
66
|
+
const { useDocument } = useFireproof(database);
|
|
67
|
+
const { doc, merge, save } = useDocument(task);
|
|
68
|
+
const [editing, setEditing] = useState(false);
|
|
69
|
+
return <article>
|
|
70
|
+
<h2>{task.title}</h2><p>{task.details}</p><p>{task.dueDate}</p>
|
|
71
|
+
{can.edit(task).ok && <>
|
|
72
|
+
<button type="button" disabled={disabled} onClick={() => onCorrect(task)}>Change by sentence</button>
|
|
73
|
+
<button type="button" disabled={disabled} onClick={() => setEditing(true)}>Edit details</button>
|
|
74
|
+
{editing && <form onSubmit={async (event) => { event.preventDefault(); if (disabled) return; await save(); onSaved(task); setEditing(false); }}>
|
|
75
|
+
<input disabled={disabled} aria-label="Task" value={doc.title} onChange={(event) => merge({ title: event.target.value })} />
|
|
76
|
+
<textarea disabled={disabled} aria-label="Details" value={doc.details} onChange={(event) => merge({ details: event.target.value })} />
|
|
77
|
+
<input disabled={disabled} aria-label="Due date" type="date" value={doc.dueDate} onChange={(event) => merge({ dueDate: event.target.value })} />
|
|
78
|
+
<button type="submit" disabled={disabled}>Save details</button>
|
|
79
|
+
</form>}
|
|
80
|
+
</>}
|
|
81
|
+
</article>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export default function App() {
|
|
85
|
+
const { database, useLiveQuery } = useFireproof("tasks");
|
|
86
|
+
const { me, can, ready } = useVibe("tasks");
|
|
87
|
+
const { docs } = useLiveQuery("type", { key: "task" });
|
|
88
|
+
const [sentence, setSentence] = useState("");
|
|
89
|
+
const [current, setCurrent] = useState(undefined);
|
|
90
|
+
const [pending, setPending] = useState(undefined);
|
|
91
|
+
const [answer, setAnswer] = useState("");
|
|
92
|
+
const [busy, setBusy] = useState(false);
|
|
93
|
+
const [message, setMessage] = useState("");
|
|
94
|
+
// Creation, sentence correction and detail editing replace this one Undo receipt after a successful write.
|
|
95
|
+
const [lastWrite, setLastWrite] = useState(undefined);
|
|
96
|
+
async function submitSentence(event) {
|
|
97
|
+
event.preventDefault();
|
|
98
|
+
if (busy || !ready) return;
|
|
99
|
+
setBusy(true); setMessage("");
|
|
100
|
+
try {
|
|
101
|
+
// A failed local batch resumes its unsaved tail without extracting or duplicating earlier records.
|
|
102
|
+
let operation = pending;
|
|
103
|
+
if (operation === undefined) {
|
|
104
|
+
const target = current === undefined ? undefined : docs.find((doc) => doc._id === current._id);
|
|
105
|
+
if (current !== undefined && target === undefined) throw new Error("Choose a saved task to correct.");
|
|
106
|
+
const items = target === undefined ? await readTasks(sentence) : [await readTask(sentence, target)];
|
|
107
|
+
const remaining = items.map((item) => target === undefined
|
|
108
|
+
? { _id: crypto.randomUUID(), type: "task", title: item.title, details: item.details, dueDate: "", authorHandle: me?.userHandle }
|
|
109
|
+
: { ...target, title: item.title, details: item.details });
|
|
110
|
+
operation = { remaining, created: [], replaced: target };
|
|
111
|
+
}
|
|
112
|
+
if (operation.remaining.some((doc) => operation.replaced === undefined ? can.create(doc).ok === false : can.edit(doc).ok === false)) {
|
|
113
|
+
throw new Error("This task is not editable right now.");
|
|
114
|
+
}
|
|
115
|
+
setPending(operation);
|
|
116
|
+
while (operation.remaining.length > 0) {
|
|
117
|
+
const doc = operation.remaining[0];
|
|
118
|
+
await database.put(doc);
|
|
119
|
+
operation = { ...operation, remaining: operation.remaining.slice(1), created: operation.replaced === undefined ? [...operation.created, doc._id] : [] };
|
|
120
|
+
setPending(operation);
|
|
121
|
+
setLastWrite({ created: operation.created, replaced: operation.replaced });
|
|
122
|
+
}
|
|
123
|
+
setPending(undefined);
|
|
124
|
+
setSentence(""); setCurrent(undefined);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
// Preserve the words and any unsaved tail on local errors. Synced refusals converge through useLiveQuery.
|
|
127
|
+
setMessage(error.reason === "refusal" ? "Try describing the task another way." : error.message);
|
|
128
|
+
} finally { setBusy(false); }
|
|
47
129
|
}
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
|
|
130
|
+
async function askRecords() {
|
|
131
|
+
if (busy || !ready || pending !== undefined || sentence.trim().length === 0) return;
|
|
132
|
+
setBusy(true); setMessage("");
|
|
133
|
+
try {
|
|
134
|
+
const response = await callAI(
|
|
135
|
+
`Answer from these records. Today: ${new Date().toISOString().slice(0, 10)}. Records: ${JSON.stringify(docs.map(({ title, details, dueDate }) => ({ title, details, dueDate })))}. Question: ${sentence}`,
|
|
136
|
+
{ schema: { properties: { answer: { type: "string" } }, required: ["answer"] } }
|
|
137
|
+
);
|
|
138
|
+
const raw = JSON.parse(response);
|
|
139
|
+
if (typeof raw?.answer !== "string") throw new Error("Ask your question again.");
|
|
140
|
+
setAnswer(raw.answer); setSentence("");
|
|
141
|
+
} catch (error) {
|
|
142
|
+
setMessage(error.message);
|
|
143
|
+
} finally { setBusy(false); }
|
|
144
|
+
}
|
|
145
|
+
// A new record is one del away; a corrected one is one put of its previous version away.
|
|
146
|
+
async function undoLast() {
|
|
147
|
+
if (busy || lastWrite === undefined) return;
|
|
148
|
+
setBusy(true); setMessage("");
|
|
149
|
+
try {
|
|
150
|
+
for (const id of lastWrite.created) await database.del(id);
|
|
151
|
+
if (lastWrite.replaced !== undefined) await database.put(lastWrite.replaced);
|
|
152
|
+
setLastWrite(undefined); setPending(undefined); setCurrent(undefined);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
setMessage(error.message);
|
|
155
|
+
} finally { setBusy(false); }
|
|
156
|
+
}
|
|
157
|
+
return <main>
|
|
158
|
+
<h1>Tasks in your own words</h1>
|
|
159
|
+
<form onSubmit={submitSentence}>
|
|
160
|
+
<textarea aria-label="Describe tasks, a correction, or ask a question" value={sentence} disabled={busy || pending !== undefined} onChange={(event) => setSentence(event.target.value)} />
|
|
161
|
+
{current !== undefined && <p>Changing: {current.title} <button type="button" disabled={busy || pending !== undefined} onClick={() => { setCurrent(undefined); setSentence(""); }}>Add a new task</button></p>}
|
|
162
|
+
<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>
|
|
163
|
+
<button type="button" disabled={!ready || busy || pending !== undefined || sentence.trim().length === 0} onClick={askRecords}>Ask about tasks</button>
|
|
164
|
+
</form>
|
|
165
|
+
<p>{answer}</p>
|
|
166
|
+
<p role="status">{message}</p>
|
|
167
|
+
{lastWrite !== undefined && <button type="button" disabled={busy} onClick={undoLast}>Undo</button>}
|
|
168
|
+
{pending !== undefined && !busy && <button type="button" onClick={() => { setPending(undefined); setSentence(""); setCurrent(undefined); }}>Discard unsaved tasks</button>}
|
|
169
|
+
{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(""); }} />)}
|
|
170
|
+
</main>;
|
|
171
|
+
}
|
|
51
172
|
```
|
|
52
173
|
|
|
53
|
-
|
|
174
|
+
For nested document shapes, normalize every collection at its boundary: `Array.isArray(raw.days)` before mapping days, and the same check for each day's `stops`. Persist the normalized records in the submit handler, then render their saved cards. Extend the schema and normalizer together as the app's editable fields evolve.
|
|
175
|
+
|
|
176
|
+
## Ask about your records
|
|
177
|
+
|
|
178
|
+
The same box answers a question about the records the app holds: send the records the answer could need with the question, and ask for the answer in a schema with one string field. Bound what you send (the visible list, or the records a query selected), name today's date when the question could involve time, and render the answer as text beside the box, never as a saved record.
|
|
54
179
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
180
|
+
```jsx
|
|
181
|
+
const { docs } = useLiveQuery("type", { key: "task" });
|
|
182
|
+
async function askRecords(question) {
|
|
183
|
+
const response = await callAI(
|
|
184
|
+
`Answer the question from these records only; say so when they do not answer it. Today: ${new Date().toISOString().slice(0, 10)}. Records: ${JSON.stringify(docs.map(({ title, dueDate }) => ({ title, dueDate })))}. Question: ${question}`,
|
|
185
|
+
{ schema: { properties: { answer: { type: "string" } } } }
|
|
186
|
+
);
|
|
187
|
+
return JSON.parse(response).answer;
|
|
188
|
+
}
|
|
189
|
+
```
|
|
58
190
|
|
|
59
191
|
## Reading an Image
|
|
60
192
|
|