///
import {
Cell,
computed,
Default,
handler,
ID,
ifElse,
lift,
NAME,
navigateTo,
OpaqueRef,
pattern,
toSchema,
UI,
wish,
} from "commontools";
import Chat from "./chatbot-note-composed.tsx";
import { ListItem } from "../system/common-tools.tsx";
import { type MentionableCharm } from "../system/backlinks-index.tsx";
type CharmEntry = {
[ID]: string; // randomId is a string
local_id: string; // same as ID but easier to access
charm: any;
};
type Input = {
selectedCharm: Default<{ charm: any }, { charm: undefined }>;
charmsList: Default;
theme?: {
accentColor: Cell>;
fontFace: Cell>;
borderRadius: Cell>;
};
};
type Output = {
selectedCharm: Default<{ charm: any }, { charm: undefined }>;
// Expose a mentionable list aggregated from local chat entries
// Returned as an opaque ref to an array (not a Cell), suitable for
// upstream aggregators that read exported mentionables.
mentionable?: MentionableCharm[];
};
const removeChat = handler<
unknown,
{
charmsList: Cell;
id: string;
selectedCharm: Cell>;
}
>(
(
_,
{ charmsList, id, selectedCharm },
) => {
const list = charmsList.get();
const index = list.findIndex((entry) => entry.local_id === id);
if (index === -1) return;
const removed = list[index];
const next = [...list];
next.splice(index, 1);
charmsList.set(next);
// If we removed the currently selected charm, choose a new selection.
const current = selectedCharm.get();
if (current?.charm === removed.charm) {
const replacement = next[index] ?? next[index - 1];
if (replacement) {
selectedCharm.set({ charm: replacement.charm });
} else {
selectedCharm.set({ charm: undefined as unknown as any });
}
}
},
);
// this will be called whenever charm or selectedCharm changes
// pass isInitialized to make sure we dont call this each time
// we change selectedCharm, otherwise creates a loop
const storeCharm = lift(
toSchema<{
charm: any;
selectedCharm: Cell>;
charmsList: Cell;
allCharms: Cell;
theme?: {
accentColor: Default;
fontFace: Default;
borderRadius: Default;
};
isInitialized: Cell;
}>(),
undefined,
({ charm, selectedCharm, charmsList, isInitialized, allCharms: _ }) => { // Not including `allCharms` is a compile error...
if (!isInitialized.get()) {
console.log(
"storeCharm storing charm:",
charm,
);
selectedCharm.set({ charm });
// create the chat charm with a custom name including a random suffix
const randomId = Math.random().toString(36).substring(2, 10); // Random 8-char string
charmsList.push({ [ID]: randomId, local_id: randomId, charm });
isInitialized.set(true);
return charm;
} else {
console.log("storeCharm: already initialized");
}
return undefined;
},
);
const populateChatList = lift(
toSchema<{
charmsList: CharmEntry[];
allCharms: Cell;
selectedCharm: Cell<{ charm: any }>;
}>(),
undefined,
(
{ charmsList, allCharms, selectedCharm },
) => {
if (charmsList.length === 0) {
const isInitialized = Cell.of(false);
return storeCharm({
charm: Chat({
title: "New Chat",
messages: [],
}),
selectedCharm,
charmsList,
allCharms,
isInitialized: isInitialized as unknown as Cell,
});
}
return charmsList;
},
);
const createChatRecipe = handler<
unknown,
{
selectedCharm: Cell<{ charm: any }>;
charmsList: Cell;
allCharms: Cell;
}
>(
(_, { selectedCharm, charmsList, allCharms }) => {
const isInitialized = Cell.of(false);
const charm = Chat({
title: "New Chat",
messages: [],
});
// store the charm ref in a cell (pass isInitialized to prevent recursive calls)
return storeCharm({
charm,
selectedCharm,
charmsList: charmsList as unknown as OpaqueRef,
allCharms,
isInitialized: isInitialized as unknown as Cell,
});
},
);
const selectCharm = handler<
unknown,
{ selectedCharm: Cell<{ charm: any }>; charm: any }
>(
(_, { selectedCharm, charm }) => {
console.log("selectCharm: updating selectedCharm to ", charm);
selectedCharm.set({ charm });
return selectedCharm;
},
);
const logCharmsList = lift<
{ charmsList: Cell },
Cell
>(
({ charmsList }) => {
console.log("logCharmsList: ", charmsList.get());
return charmsList;
},
);
const _handleCharmLinkClicked = handler(
(_: any, { charm }: { charm: Cell }) => {
return navigateTo(charm);
},
);
const _merge = lift(
(
{ allCharms, charmsList }: { allCharms: any[]; charmsList: CharmEntry[] },
) => {
return [...charmsList.map((c) => c.charm), ...allCharms];
},
);
const getSelectedCharm = lift<
{ entry: { charm: any | undefined } },
{
chat: unknown;
list: ListItem[];
} | undefined
>(
({ entry }) => {
return entry?.charm;
},
);
const getCharmName = lift(({ charm }: { charm: any }) => {
return charm?.[NAME] || "Unknown";
});
const extractLocalMentionable = lift<
{ list: CharmEntry[] },
MentionableCharm[]
>(({ list }) => {
const out: MentionableCharm[] = [];
for (const entry of list) {
const c = entry.charm;
out.push(c.chat);
}
return out;
});
// create the named cell inside the pattern body, so we do it just once
export default pattern(
({ selectedCharm, charmsList, theme }) => {
const wishedCharms = wish("#allCharms");
const allCharms = computed(() => wishedCharms ?? []);
logCharmsList({ charmsList: charmsList as unknown as Cell });
populateChatList({
selectedCharm: selectedCharm as unknown as Cell<
Pick
>,
charmsList,
allCharms,
});
const selected = getSelectedCharm({ entry: selectedCharm });
// Aggregate mentionables from the local charms list so that this
// container exposes its child chat charms as mention targets.
const localMentionable = extractLocalMentionable({ list: charmsList });
const localTheme = theme ?? {
accentColor: Cell.of("#3b82f6"),
fontFace: Cell.of("system-ui, -apple-system, sans-serif"),
borderRadius: Cell.of("0.5rem"),
};
return {
[NAME]: "Launcher",
[UI]: (
Create New Chat
alt+N
{/* Keyboard shortcuts */}
{/* workaround: this seems to correctly start the sub-recipes on a refresh while directly rendering does not */}
{/* this should be fixed after the builder-refactor (DX1) */}
),
selectedCharm,
charmsList,
// Expose the aggregated mentionables for parent-level indexing.
mentionable: localMentionable,
};
},
);