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>}
    </>
  );
}