Spaces:
Runtime error
Runtime error
File size: 3,977 Bytes
b74c6ae | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | import { useEffect, useState, type FormEvent } from "react";
import { memoryApi } from "../api/endpoints";
import type { MemoryNote } from "../api/types";
import { ConfirmButton, errorMessage } from "./ui";
const EXAMPLES = [
{ key: "audience", value: "" },
{ key: "tone", value: "" },
{ key: "avoid", value: "" },
];
/** The writer's standing notes: what the research chat always knows, in every notebook. The chat also adds
* notes on its own when the writer says something lasting, and every note can be edited or removed here. */
export function MemoryNotes() {
const [notes, setNotes] = useState<MemoryNote[] | null>(null);
const [limit, setLimit] = useState(20);
const [key, setKey] = useState("");
const [value, setValue] = useState("");
const [error, setError] = useState<string | null>(null);
const load = () =>
memoryApi.list().then(
(d) => {
setNotes(d.items);
setLimit(d.limit);
},
() => setError("Could not load your notes."),
);
useEffect(() => {
void load();
}, []);
const add = async (e: FormEvent) => {
e.preventDefault();
setError(null);
try {
await memoryApi.put(key, value);
setKey("");
setValue("");
await load();
} catch (err) {
setError(errorMessage(err));
}
};
const edit = async (note: MemoryNote, next: string) => {
if (next.trim() === note.value) return;
setError(null);
try {
await memoryApi.put(note.key, next);
await load();
} catch (err) {
setError(errorMessage(err));
await load();
}
};
const remove = async (note: MemoryNote) => {
await memoryApi.remove(note.key);
await load();
};
return (
<>
<p className="muted">
Notes the assistant reads before every answer, in every notebook. It saves new ones when you tell it something
lasting, like who you write for. Your posts stay the only source it cites.
</p>
{notes && notes.length === 0 && <p className="mono muted">No notes yet.</p>}
{notes?.map((n) => (
<div key={n.key} className="stack">
<div className="row between">
<span className="mono">{n.key}</span>
<span className="row">
<span className="mono muted small">{n.source === "auto" ? "saved from chat" : "written by you"}</span>
<ConfirmButton onConfirm={() => remove(n)}>Delete</ConfirmButton>
</span>
</div>
<input
className="input input-sm"
defaultValue={n.value}
maxLength={500}
aria-label={`Note ${n.key}`}
onBlur={(e) => edit(n, e.target.value)}
/>
</div>
))}
<form className="stack" onSubmit={add}>
<div className="row">
{EXAMPLES.map((x) => (
<button key={x.key} type="button" className="btn btn-small" onClick={() => setKey(x.key)}>
{x.key}
</button>
))}
</div>
<label className="field">
<span>Name</span>
<input className="input input-sm" placeholder="audience" maxLength={60} value={key} onChange={(e) => setKey(e.target.value)} />
</label>
<label className="field">
<span>Note</span>
<input
className="input input-sm"
placeholder="I write for indie founders"
maxLength={500}
value={value}
onChange={(e) => setValue(e.target.value)}
/>
</label>
<div className="row between">
<span className="mono muted small">
{notes?.length ?? 0} of {limit} notes
</span>
<button className="btn btn-small btn-primary" type="submit" disabled={!key.trim() || !value.trim() || (notes?.length ?? 0) >= limit}>
Add note
</button>
</div>
</form>
{error && <p className="error-text">{error}</p>}
</>
);
}
|