Skip to content
134 changes: 104 additions & 30 deletions packages/patterns/chatbot-list-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,18 @@ import {
lift,
NAME,
navigateTo,
OpaqueRef,
recipe,
UI,
} from "commontools";

import Chat from "./chatbot.tsx";
import Chat from "./chatbot-note-composed.tsx";

export type MentionableCharm = {
[NAME]: string;
content?: string;
mentioned?: MentionableCharm[];
};

type CharmEntry = {
[ID]: string; // randomId is a string
Expand All @@ -26,9 +33,12 @@ type CharmEntry = {
type Input = {
selectedCharm: Default<{ charm: any }, { charm: undefined }>;
charmsList: Default<CharmEntry[], []>;
allCharms: Cell<any[]>;
};

type Output = Input;
type Output = {
selectedCharm: Default<{ charm: any }, { charm: undefined }>;
};

// this will be called whenever charm or selectedCharm changes
// pass isInitialized to make sure we dont call this each time
Expand Down Expand Up @@ -64,7 +74,7 @@ const storeCharm = lift(
if (!isInitialized.get()) {
console.log(
"storeCharm storing charm:",
JSON.stringify(charm),
charm,
);
selectedCharm.set({ charm });

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

const createChatRecipe = handler<
unknown,
{ selectedCharm: Cell<{ charm: any }>; charmsList: Cell<CharmEntry[]> }
{
selectedCharm: Cell<{ charm: any }>;
charmsList: Cell<CharmEntry[]>;
allCharms: Cell<any[]>;
}
>(
(_, { selectedCharm, charmsList }) => {
(_, { selectedCharm, charmsList, allCharms }) => {
const isInitialized = cell(false);

const charm = Chat({
title: "New Chat",
messages: [],
tools: undefined,
expandChat: false,
content: "",
allCharms: [
...allCharms.get(),
...charmsList.get().map((i) => i.charm),
] as unknown as OpaqueRef<Cell<MentionableCharm[]>>, // TODO(bf): types...
});
// store the charm ref in a cell (pass isInitialized to prevent recursive calls)
return storeCharm({ charm, selectedCharm, charmsList, isInitialized });
Expand Down Expand Up @@ -132,42 +152,96 @@ const logCharmsList = lift(
},
);

const handleCharmLinkClicked = handler(
(_: any, { charm }: { charm: Cell<MentionableCharm> }) => {
return navigateTo(charm);
},
);

// create the named cell inside the recipe body, so we do it just once
export default recipe<Input, Output>(
"Launcher",
({ selectedCharm, charmsList }) => {
({ selectedCharm, charmsList, allCharms }) => {
logCharmsList({ charmsList });

return {
[NAME]: "Launcher",
[UI]: (
<div>
<ct-button onClick={createChatRecipe({ selectedCharm, charmsList })}>
Create New Chat
</ct-button>

<div>
<h3>Chat List</h3>
<ct-screen>
<div slot="header">
<ct-button
onClick={createChatRecipe({
selectedCharm,
charmsList,
allCharms: allCharms as unknown as any,
})}
>
Create New Chat
</ct-button>
</div>
<div>
{charmsList.map((charmEntry, i) => (
<ct-autolayout tabNames={["Chat", "Tools"]}>
{
selectedCharm.charm.chat[UI] // workaround: CT-987
}
{
selectedCharm.charm.note[UI] // workaround: CT-987
}

<aside slot="left">
<div>
index={i} chat ID: {charmEntry.local_id}
<ct-button
onClick={selectCharm({
selectedCharm: selectedCharm,
charm: charmEntry.charm,
})}
>
LOAD
</ct-button>
<h3>Chat List</h3>
</div>
))}
</div>
<div>
{charmsList.map((charmEntry, i) => (
<div>
index={i} chat ID: {charmEntry.local_id}
<ct-button
onClick={selectCharm({
selectedCharm: selectedCharm,
charm: charmEntry.charm,
})}
>
LOAD
</ct-button>
</div>
))}
</div>
</aside>

<div>--- end chat list ---</div>
<div>{selectedCharm.charm}</div>
</div>
<aside slot="right">
{ifElse(
selectedCharm.charm,
<>
<div>
<label>Backlinks</label>
<ct-vstack>
{selectedCharm?.charm?.backlinks?.map((
charm: MentionableCharm,
) => (
<ct-button onClick={handleCharmLinkClicked({ charm })}>
{charm[NAME]}
</ct-button>
))}
</ct-vstack>
</div>
<details>
<summary>Mentioned Charms</summary>
<ct-vstack>
{selectedCharm?.charm?.mentioned?.map((
charm: MentionableCharm,
) => (
<ct-button onClick={handleCharmLinkClicked({ charm })}>
{charm[NAME]}
</ct-button>
))}
</ct-vstack>
</details>
</>,
null,
)}
</aside>
</ct-autolayout>
</ct-screen>
),
selectedCharm,
charmsList,
Expand Down
163 changes: 163 additions & 0 deletions packages/patterns/chatbot-note-composed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/// <cts-enable />
import {
BuiltInLLMMessage,
Cell,
cell,
Default,
derive,
fetchData,
getRecipeEnvironment,
h,
handler,
ID,
ifElse,
JSONSchema,
lift,
llm,
llmDialog,
NAME,
navigateTo,
OpaqueRef,
recipe,
str,
Stream,
toSchema,
UI,
} from "commontools";

import Chat from "./chatbot.tsx";
import Note from "./note.tsx";

export type MentionableCharm = {
[NAME]: string;
content?: string;
mentioned?: MentionableCharm[];
};

type NoteResult = {
content: Default<string, "">;
};

export type NoteInput = {
content: Default<string, "">;
allCharms: Cell<MentionableCharm[]>;
};

const handleCharmLinkClick = handler<
{
detail: {
charm: Cell<MentionableCharm>;
};
},
Record<string, never>
>(({ detail }, _) => {
return navigateTo(detail.charm);
});

const handleCharmLinkClicked = handler(
(_: any, { charm }: { charm: Cell<MentionableCharm> }) => {
return navigateTo(charm);
},
);

type LLMTestInput = {
title: Default<string, "LLM Test">;
messages: Default<Array<BuiltInLLMMessage>, []>;
expandChat: Default<boolean, false>;
content: Default<string, "">;
allCharms: Cell<MentionableCharm[]>;
};

type LLMTestResult = {
messages: Default<Array<BuiltInLLMMessage>, []>;
mentioned: Default<Array<MentionableCharm>, []>;
backlinks: Default<Array<MentionableCharm>, []>;
content: Default<string, "">;
note: any;
chat: any;
};

// put a note at the end of the outline (by appending to root.children)
const editNote = handler<
{
/** The text content of the note */
body: string;
/** A cell to store the result message indicating success or error */
result: Cell<string>;
},
{ content: Cell<string> }
>(
(args, state) => {
try {
state.content.set(args.body);

args.result.set(
`Updated note!`,
);
} catch (error) {
args.result.set(`Error: ${(error as any)?.message || "<error>"}`);
}
},
);

const readNote = handler<
{
/** A cell to store the result text */
result: Cell<string>;
},
{ content: string }
>(
(args, state) => {
try {
args.result.set(state.content);
} catch (error) {
args.result.set(`Error: ${(error as any)?.message || "<error>"}`);
}
},
);

export default recipe<LLMTestInput, LLMTestResult>(
"Note",
({ title, expandChat, messages, content, allCharms }) => {
const tools = {
editNote: {
description: "Modify the shared note.",
inputSchema: {
type: "object",
properties: {
body: {
type: "string",
description: "The content of the note.",
},
},
required: ["body"],
} as JSONSchema,
handler: editNote({ content }),
},
readNote: {
description: "Read the shared note.",
inputSchema: {
type: "object",
properties: {},
required: [],
} as JSONSchema,
handler: readNote({ content }),
},
};

const chat = Chat({ messages, tools });
const { addMessage, cancelGeneration, pending } = chat;

const note = Note({ title, content, allCharms });

return {
[NAME]: title,
chat,
note,
content,
messages,
mentioned: note.mentioned,
backlinks: note.backlinks,
};
},
);
Loading