///
import {
BuiltInLLMMessage,
Cell,
computed,
Default,
fetchData,
generateObject,
handler,
llmDialog,
NAME,
pattern,
patternTool,
Stream,
UI,
VNode,
wish,
} from "commontools";
import { type MentionableCharm } from "./system/backlinks-index.tsx";
const sendMessage = handler<
{
detail: {
text: string;
attachments: Array;
mentions: Array;
message: string; // Backward compatibility
};
},
{
addMessage: Stream;
}
>((event, { addMessage }) => {
const { text } = event.detail;
// Send the message as-is. Any markdown links like [name](/of:...)
// are just text that the LLM can parse and use with addAttachment() tool.
addMessage.send({
role: "user",
content: [{ type: "text" as const, text }],
});
});
const clearChat = handler(
(
_: never,
{ messages, pending }: {
messages: Cell>;
pending: Cell;
},
) => {
messages.set([]);
pending.set(false);
},
);
type ChatInput = {
messages?: Cell, []>>;
tools?: any;
theme?: any;
system?: string;
};
type PromptAttachment = {
id: string;
name: string;
type: "file" | "clipboard" | "mention";
data?: any; // File | Blob | string
charm?: any;
removable?: boolean; // Whether this attachment can be removed
};
type ChatOutput = {
messages: Array;
pending: boolean | undefined;
addMessage: Stream;
clearChat: Stream;
cancelGeneration: Stream;
title?: string;
pinnedCells: Array;
tools: any;
ui: {
chatLog: VNode;
promptInput: VNode;
attachmentsAndTools: VNode;
};
};
export const TitleGenerator = pattern<
{ model?: string; messages: Array }
>(({ model, messages }) => {
const previewMessage = computed(() => {
if (!messages || messages.length === 0) return "";
const firstMessage = messages[0];
if (!firstMessage) return "";
return JSON.stringify(firstMessage);
});
const { result } = generateObject({
system:
"Generate at most a 3-word title based on the following content, respond with NOTHING but the literal title text.",
prompt: previewMessage,
model,
schema: {
type: "object",
properties: {
title: {
type: "string",
description: "The title of the chat",
},
},
required: ["title"],
},
});
const title = computed(() => {
return result?.title || "Untitled Chat";
});
return title;
});
const listMentionable = pattern<
{ mentionable: Array },
{ result: Array<{ label: string; cell: Cell }> }
>(
({ mentionable }) => {
const result = mentionable.map((charm) => ({
label: charm[NAME]!,
cell: charm,
}));
return { result };
},
);
const listRecent = pattern<
{ recentCharms: Array },
{ result: Array<{ label: string; cell: Cell }> }
>(
({ recentCharms }) => {
const namesList = recentCharms.map((charm) => ({
label: charm[NAME]!,
cell: charm,
}));
return { result: namesList };
},
);
export default pattern(
({ messages, tools, theme, system }) => {
const model = Cell.of("anthropic:claude-sonnet-4-5");
const mentionable = wish("#mentionable");
const recentCharms = wish("#recent");
const assistantTools = {
listMentionable: patternTool(listMentionable, { mentionable }),
listRecent: patternTool(listRecent, { recentCharms }),
};
// Merge static and assistant tools
const mergedTools = computed(() => ({
...tools,
...assistantTools,
}));
const latest = computed(() => recentCharms[0]);
const latestName = computed(() => recentCharms[0]?.[NAME] ?? "latest");
const {
addMessage,
cancelGeneration,
pending,
flattenedTools,
pinnedCells,
} = llmDialog(
{
system: computed(() => {
return system ?? "You are a polite but efficient assistant.";
}),
messages,
tools: mergedTools,
model,
context: computed(() => ({
[latestName]: latest,
})),
},
);
const { result } = fetchData({
url: "/api/ai/llm/models",
mode: "json",
});
const items = computed(() => {
if (!result) return [];
const items = Object.keys(result as any).map((key) => ({
label: key,
value: key,
}));
return items;
});
const title = TitleGenerator({ model, messages });
const promptInput = (
);
const chatLog = (
);
const attachmentsAndTools = (
Clear
);
return {
[NAME]: title,
[UI]: (
{title}
{attachmentsAndTools}
{chatLog}
{promptInput}
),
messages,
pending,
addMessage,
clearChat: clearChat({
messages,
pending,
}),
cancelGeneration,
title,
pinnedCells,
tools: flattenedTools,
ui: {
chatLog,
promptInput,
attachmentsAndTools,
},
};
},
);