Export template source from 11bc3ae35b0034c113daca4c35d1b341aa31ef70:examples/local-first/packages/language-reader-presentation

This commit is contained in:
Quixos Template Publisher
2000-01-01 00:00:00 +00:00
commit 131148e054
61 changed files with 10297 additions and 0 deletions
+258
View File
@@ -0,0 +1,258 @@
import { usePrompt } from "./drafts.js";
import { useEffect, useState } from "react";
import { useCrdtText, useInterface, referenceOf, type ExactReference } from "@quixos/camino-react";
import { Icon, LocalChoice, TextArea, SegmentedControl } from "@quixos/camino-react/ui";
import { interfaces } from "./gen/client.js";
import { accepted, asInterface, errorMessage, type Root } from "./model.js";
type TextNode = Root["documents"]["entries"][number]["node"];
function TextRow({
node,
open,
selected,
changed,
}: {
node: TextNode;
open: (ref: ExactReference) => void;
selected?: string | undefined;
changed: (id: string, archived: boolean) => void;
}) {
const api = useInterface(
{ archived: { set: interfaces.ReadingDocument.archived.set } },
asInterface(referenceOf(node), "document"),
);
const [busy, setBusy] = useState(false),
[error, setError] = useState("");
const archive = async () => {
if (api.status !== "ready") return;
setBusy(true);
setError("");
try {
await accepted(api.value.archived.set(!node.archived));
changed(referenceOf(node).object, !node.archived);
} catch (e) {
setError(errorMessage(e));
} finally {
setBusy(false);
}
};
return (
<div className="lr-text-row">
<button
className="lr-text-open"
aria-current={selected === referenceOf(node).object ? "page" : undefined}
onClick={() => open(referenceOf(node))}
>
<span className="lr-text-icon">
<Icon name="pages" size={21} />
</span>
<span className="lr-text-title">
<strong>{node.data.title}</strong>
<small>
{node.data.source === "sample"
? "Sample text"
: node.data.source === "generated"
? "Generated text"
: node.data.source}
</small>
</span>
<Icon name="arrow-right" />
</button>
<button
className="lr-text-archive"
disabled={busy || api.status !== "ready"}
aria-label={`${node.archived ? "Restore" : "Archive"} ${node.data.title}`}
onClick={() => void archive()}
>
{node.archived ? "Restore" : <Icon name="archive" />}
</button>
{error && (
<p className="lr-error" role="alert">
{error}
</p>
)}
</div>
);
}
export function Texts({
root,
selected,
open,
add,
speech,
}: {
root: Root;
selected?: string | undefined;
open: (ref: ExactReference) => void;
add: () => void;
speech: () => void;
}) {
const [view, setView] = useState("active"),
[overrides, setOverrides] = useState<Record<string, boolean>>({});
useEffect(() => {
setOverrides((old) => {
const next = { ...old };
for (const { node } of root.documents.entries) {
const id = referenceOf(node).object;
if (next[id] === node.archived) delete next[id];
}
return Object.keys(next).length === Object.keys(old).length ? old : next;
});
}, [root.documents]);
const nodes = root.documents.entries
.map((e) => e.node)
.filter((n) => (overrides[referenceOf(n).object] ?? n.archived) === (view === "archived"));
return (
<div className="lr-library">
<header className="lr-library-head">
<div>
<h1>Texts</h1>
<p>
{nodes.length} {view === "archived" ? "archived " : ""}
{nodes.length === 1 ? "text" : "texts"}
</p>
</div>
<div className="lr-toolbar">
{selected && root.documents.entries.some((e) => referenceOf(e.node).object === selected) && (
<button
onClick={() =>
open(referenceOf(root.documents.entries.find((e) => referenceOf(e.node).object === selected)!.node))
}
>
Back to text
</button>
)}
<button onClick={speech}>Read-aloud settings</button>
<button className="lr-primary" onClick={add}>
<Icon name="plus" />
Add text
</button>
</div>
</header>
<div className="lr-library-filter">
<SegmentedControl
label="Text collection"
value={view}
options={[
{ value: "active", label: "Texts" },
{ value: "archived", label: "Archived" },
]}
onChange={setView}
/>
</div>
<nav className="lr-text-list" aria-label="Texts">
{nodes.map((node) => (
<TextRow
key={referenceOf(node).object}
node={node}
selected={selected}
open={open}
changed={(id, archived) => setOverrides((old) => ({ ...old, [id]: archived }))}
/>
))}
</nav>
{nodes.length === 0 && (
<div className="lr-empty">
<h2>{view === "archived" ? "No archived texts" : "No texts yet"}</h2>
{view === "active" && <button onClick={add}>Add text</button>}
</div>
)}
</div>
);
}
export function GenerateText({
library,
root,
initialReader,
navigation,
added,
}: {
library: ExactReference;
root: Root;
initialReader?: string | undefined;
navigation: ExactReference;
added: (ref: ExactReference) => void;
}) {
const draft = usePrompt(navigation, "generate");
const prompt = draft.text;
const api = useInterface(interfaces.LanguageReader, library),
[reader, setReader] = useState(initialReader ?? root.defaultReader?._camino.ref.object ?? ""),
[busy, setBusy] = useState(false),
[job, setJob] = useState(""),
[error, setError] = useState("");
const task = root.tasks.entries.map((e) => e.node).find((n) => referenceOf(n).object === job);
useEffect(() => {
if (task?.data.status === "ready" && task.document) added(referenceOf(task.document));
}, [task?.data.status, task?.document?._camino.ref.object]);
const failed = !!task && ["failed", "cancelled", "uncertain"].includes(task.data.status);
const generate = async () => {
if (api.status !== "ready" || !reader) return;
setBusy(true);
setError("");
try {
await draft.flush();
const ref = await accepted(
api.value.generateDocument.call({
prompt,
reader: {
...root.readers.entries.find((e) => referenceOf(e.node).object === reader)!.node._camino.ref,
expectation: { kind: "atom", id: "atom:reading:reader" },
},
createdAt: new Date().toISOString(),
}),
);
setJob(ref.object);
draft.clearSubmitted(prompt);
} catch (e) {
setError(errorMessage(e));
} finally {
setBusy(false);
}
};
return (
<div className="lr-generate-text">
<label className="lr-field">
Read with
<LocalChoice
aria-label="Reader for generated text"
value={reader}
onValueChange={setReader}
options={root.readers.entries.map(({ node }) => ({ value: referenceOf(node).object, label: node.data.name }))}
disabled={busy || (!!job && !failed)}
/>
</label>
<label className="lr-field">
What would you like to read?
<TextArea
binding={draft}
aria-label="Text request"
rows={6}
maxLength={4000}
placeholder="A short French story about missing a train, at A2 level…"
disabled={busy || (!!job && !failed)}
/>
</label>
<footer>
<small>Include a language, topic, and difficulty.</small>
<button
className="lr-primary"
disabled={!prompt.trim() || !reader || busy || (!!job && !failed)}
onClick={() => void generate()}
>
<Icon name="spark" />
{busy ? "Starting…" : job && !failed ? "Writing…" : failed ? "Try again" : "Generate text"}
</button>
</footer>
{job && !failed && (
<p role="status" className="lr-generation-status">
Writing your text. You can close this window; it will appear in Texts when ready.
</p>
)}
{(error || failed) && (
<p className="lr-error" role="alert">
{error || task?.data.error || "The text could not be generated."}
</p>
)}
</div>
);
}