Skip to content

Commit 9cca85d

Browse files
authored
Create-from-mention [CT-923] [CT-924] (commontoolsinc#1804)
* Fix enter-to-choose-completion * Create new note.tsx instances when mentioning a new title * Extend autolayout for side panels * Catch Cmd/Ctrl+S and prevent browser default * Format pass * Remove composed recipe * Fix type error * Format pass * Integrate layout + chat list * Combine all mentions * Format pass * Revert default-app.tsx
1 parent ff9dca7 commit 9cca85d

File tree

6 files changed

+656
-83
lines changed

6 files changed

+656
-83
lines changed

packages/patterns/chatbot-list-view.tsx

Lines changed: 104 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,18 @@ import {
1111
lift,
1212
NAME,
1313
navigateTo,
14+
OpaqueRef,
1415
recipe,
1516
UI,
1617
} from "commontools";
1718

18-
import Chat from "./chatbot.tsx";
19+
import Chat from "./chatbot-note-composed.tsx";
20+
21+
export type MentionableCharm = {
22+
[NAME]: string;
23+
content?: string;
24+
mentioned?: MentionableCharm[];
25+
};
1926

2027
type CharmEntry = {
2128
[ID]: string; // randomId is a string
@@ -26,9 +33,12 @@ type CharmEntry = {
2633
type Input = {
2734
selectedCharm: Default<{ charm: any }, { charm: undefined }>;
2835
charmsList: Default<CharmEntry[], []>;
36+
allCharms: Cell<any[]>;
2937
};
3038

31-
type Output = Input;
39+
type Output = {
40+
selectedCharm: Default<{ charm: any }, { charm: undefined }>;
41+
};
3242

3343
// this will be called whenever charm or selectedCharm changes
3444
// pass isInitialized to make sure we dont call this each time
@@ -64,7 +74,7 @@ const storeCharm = lift(
6474
if (!isInitialized.get()) {
6575
console.log(
6676
"storeCharm storing charm:",
67-
JSON.stringify(charm),
77+
charm,
6878
);
6979
selectedCharm.set({ charm });
7080

@@ -83,14 +93,24 @@ const storeCharm = lift(
8393

8494
const createChatRecipe = handler<
8595
unknown,
86-
{ selectedCharm: Cell<{ charm: any }>; charmsList: Cell<CharmEntry[]> }
96+
{
97+
selectedCharm: Cell<{ charm: any }>;
98+
charmsList: Cell<CharmEntry[]>;
99+
allCharms: Cell<any[]>;
100+
}
87101
>(
88-
(_, { selectedCharm, charmsList }) => {
102+
(_, { selectedCharm, charmsList, allCharms }) => {
89103
const isInitialized = cell(false);
90104

91105
const charm = Chat({
106+
title: "New Chat",
92107
messages: [],
93-
tools: undefined,
108+
expandChat: false,
109+
content: "",
110+
allCharms: [
111+
...allCharms.get(),
112+
...charmsList.get().map((i) => i.charm),
113+
] as unknown as OpaqueRef<Cell<MentionableCharm[]>>, // TODO(bf): types...
94114
});
95115
// store the charm ref in a cell (pass isInitialized to prevent recursive calls)
96116
return storeCharm({ charm, selectedCharm, charmsList, isInitialized });
@@ -132,42 +152,96 @@ const logCharmsList = lift(
132152
},
133153
);
134154

155+
const handleCharmLinkClicked = handler(
156+
(_: any, { charm }: { charm: Cell<MentionableCharm> }) => {
157+
return navigateTo(charm);
158+
},
159+
);
160+
135161
// create the named cell inside the recipe body, so we do it just once
136162
export default recipe<Input, Output>(
137163
"Launcher",
138-
({ selectedCharm, charmsList }) => {
164+
({ selectedCharm, charmsList, allCharms }) => {
139165
logCharmsList({ charmsList });
140166

141167
return {
142168
[NAME]: "Launcher",
143169
[UI]: (
144-
<div>
145-
<ct-button onClick={createChatRecipe({ selectedCharm, charmsList })}>
146-
Create New Chat
147-
</ct-button>
148-
149-
<div>
150-
<h3>Chat List</h3>
170+
<ct-screen>
171+
<div slot="header">
172+
<ct-button
173+
onClick={createChatRecipe({
174+
selectedCharm,
175+
charmsList,
176+
allCharms: allCharms as unknown as any,
177+
})}
178+
>
179+
Create New Chat
180+
</ct-button>
151181
</div>
152-
<div>
153-
{charmsList.map((charmEntry, i) => (
182+
<ct-autolayout tabNames={["Chat", "Tools"]}>
183+
{
184+
selectedCharm.charm.chat[UI] // workaround: CT-987
185+
}
186+
{
187+
selectedCharm.charm.note[UI] // workaround: CT-987
188+
}
189+
190+
<aside slot="left">
154191
<div>
155-
index={i} chat ID: {charmEntry.local_id}
156-
<ct-button
157-
onClick={selectCharm({
158-
selectedCharm: selectedCharm,
159-
charm: charmEntry.charm,
160-
})}
161-
>
162-
LOAD
163-
</ct-button>
192+
<h3>Chat List</h3>
164193
</div>
165-
))}
166-
</div>
194+
<div>
195+
{charmsList.map((charmEntry, i) => (
196+
<div>
197+
index={i} chat ID: {charmEntry.local_id}
198+
<ct-button
199+
onClick={selectCharm({
200+
selectedCharm: selectedCharm,
201+
charm: charmEntry.charm,
202+
})}
203+
>
204+
LOAD
205+
</ct-button>
206+
</div>
207+
))}
208+
</div>
209+
</aside>
167210

168-
<div>--- end chat list ---</div>
169-
<div>{selectedCharm.charm}</div>
170-
</div>
211+
<aside slot="right">
212+
{ifElse(
213+
selectedCharm.charm,
214+
<>
215+
<div>
216+
<label>Backlinks</label>
217+
<ct-vstack>
218+
{selectedCharm?.charm?.backlinks?.map((
219+
charm: MentionableCharm,
220+
) => (
221+
<ct-button onClick={handleCharmLinkClicked({ charm })}>
222+
{charm[NAME]}
223+
</ct-button>
224+
))}
225+
</ct-vstack>
226+
</div>
227+
<details>
228+
<summary>Mentioned Charms</summary>
229+
<ct-vstack>
230+
{selectedCharm?.charm?.mentioned?.map((
231+
charm: MentionableCharm,
232+
) => (
233+
<ct-button onClick={handleCharmLinkClicked({ charm })}>
234+
{charm[NAME]}
235+
</ct-button>
236+
))}
237+
</ct-vstack>
238+
</details>
239+
</>,
240+
null,
241+
)}
242+
</aside>
243+
</ct-autolayout>
244+
</ct-screen>
171245
),
172246
selectedCharm,
173247
charmsList,
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/// <cts-enable />
2+
import {
3+
BuiltInLLMMessage,
4+
Cell,
5+
cell,
6+
Default,
7+
derive,
8+
fetchData,
9+
getRecipeEnvironment,
10+
h,
11+
handler,
12+
ID,
13+
ifElse,
14+
JSONSchema,
15+
lift,
16+
llm,
17+
llmDialog,
18+
NAME,
19+
navigateTo,
20+
OpaqueRef,
21+
recipe,
22+
str,
23+
Stream,
24+
toSchema,
25+
UI,
26+
} from "commontools";
27+
28+
import Chat from "./chatbot.tsx";
29+
import Note from "./note.tsx";
30+
31+
export type MentionableCharm = {
32+
[NAME]: string;
33+
content?: string;
34+
mentioned?: MentionableCharm[];
35+
};
36+
37+
type NoteResult = {
38+
content: Default<string, "">;
39+
};
40+
41+
export type NoteInput = {
42+
content: Default<string, "">;
43+
allCharms: Cell<MentionableCharm[]>;
44+
};
45+
46+
const handleCharmLinkClick = handler<
47+
{
48+
detail: {
49+
charm: Cell<MentionableCharm>;
50+
};
51+
},
52+
Record<string, never>
53+
>(({ detail }, _) => {
54+
return navigateTo(detail.charm);
55+
});
56+
57+
const handleCharmLinkClicked = handler(
58+
(_: any, { charm }: { charm: Cell<MentionableCharm> }) => {
59+
return navigateTo(charm);
60+
},
61+
);
62+
63+
type LLMTestInput = {
64+
title: Default<string, "LLM Test">;
65+
messages: Default<Array<BuiltInLLMMessage>, []>;
66+
expandChat: Default<boolean, false>;
67+
content: Default<string, "">;
68+
allCharms: Cell<MentionableCharm[]>;
69+
};
70+
71+
type LLMTestResult = {
72+
messages: Default<Array<BuiltInLLMMessage>, []>;
73+
mentioned: Default<Array<MentionableCharm>, []>;
74+
backlinks: Default<Array<MentionableCharm>, []>;
75+
content: Default<string, "">;
76+
note: any;
77+
chat: any;
78+
};
79+
80+
// put a note at the end of the outline (by appending to root.children)
81+
const editNote = handler<
82+
{
83+
/** The text content of the note */
84+
body: string;
85+
/** A cell to store the result message indicating success or error */
86+
result: Cell<string>;
87+
},
88+
{ content: Cell<string> }
89+
>(
90+
(args, state) => {
91+
try {
92+
state.content.set(args.body);
93+
94+
args.result.set(
95+
`Updated note!`,
96+
);
97+
} catch (error) {
98+
args.result.set(`Error: ${(error as any)?.message || "<error>"}`);
99+
}
100+
},
101+
);
102+
103+
const readNote = handler<
104+
{
105+
/** A cell to store the result text */
106+
result: Cell<string>;
107+
},
108+
{ content: string }
109+
>(
110+
(args, state) => {
111+
try {
112+
args.result.set(state.content);
113+
} catch (error) {
114+
args.result.set(`Error: ${(error as any)?.message || "<error>"}`);
115+
}
116+
},
117+
);
118+
119+
export default recipe<LLMTestInput, LLMTestResult>(
120+
"Note",
121+
({ title, expandChat, messages, content, allCharms }) => {
122+
const tools = {
123+
editNote: {
124+
description: "Modify the shared note.",
125+
inputSchema: {
126+
type: "object",
127+
properties: {
128+
body: {
129+
type: "string",
130+
description: "The content of the note.",
131+
},
132+
},
133+
required: ["body"],
134+
} as JSONSchema,
135+
handler: editNote({ content }),
136+
},
137+
readNote: {
138+
description: "Read the shared note.",
139+
inputSchema: {
140+
type: "object",
141+
properties: {},
142+
required: [],
143+
} as JSONSchema,
144+
handler: readNote({ content }),
145+
},
146+
};
147+
148+
const chat = Chat({ messages, tools });
149+
const { addMessage, cancelGeneration, pending } = chat;
150+
151+
const note = Note({ title, content, allCharms });
152+
153+
return {
154+
[NAME]: title,
155+
chat,
156+
note,
157+
content,
158+
messages,
159+
mentioned: note.mentioned,
160+
backlinks: note.backlinks,
161+
};
162+
},
163+
);

0 commit comments

Comments
 (0)