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