Skip to content

Commit 6495921

Browse files
authored
Implement FAB + omnibox prototype (#1970)
* Working omnibox prototype * Fix mentions placement * Fix click to dismiss * Refine history toggle * Refine toast interaction * Format pass * Style refinements * Fine tuning styles * Integration model picker into FAB * Format pass * Remove spec * Integrate preview into FAB instead of using ct-toast * Remove unused ct-toast * Format FAB * Remove unused ct-omnibox * Fix lint * Add `type=button` * Fix type error * Remove sidebarUI from default-app * Action cubic review notes * Bind to Cmd+O * Review feedback * Format pass
1 parent feda69f commit 6495921

File tree

21 files changed

+2009
-1454
lines changed

21 files changed

+2009
-1454
lines changed

packages/patterns/chatbot-outliner.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,7 @@ const handleCharmLinkClick = handler<
5454

5555
function getMentionable() {
5656
return derive<MentionableCharm[], MentionableCharm[]>(
57-
wish<MentionableCharm[]>(
58-
"#mentionable",
59-
[],
60-
),
57+
wish<MentionableCharm[]>("#mentionable"),
6158
(i) => i,
6259
);
6360
}

packages/patterns/chatbot.tsx

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
llmDialog,
1212
NAME,
1313
navigateTo,
14-
Opaque,
1514
recipe,
1615
Stream,
1716
UI,
@@ -20,8 +19,8 @@ import {
2019
} from "commontools";
2120
import { type MentionableCharm } from "./backlinks-index.tsx";
2221

23-
function schemaifyWish<T>(path: string, def: Opaque<T>) {
24-
return derive<T, T>(wish<T>(path, def), (i) => i);
22+
function schemaifyWish<T>(path: string) {
23+
return derive<T, T>(wish<T>(path), (i) => i);
2524
}
2625

2726
const addAttachment = handler<
@@ -274,10 +273,7 @@ export default recipe<ChatInput, ChatOutput>(
274273
({ messages, tools, theme }) => {
275274
const model = cell<string>("anthropic:claude-sonnet-4-5");
276275
const allAttachments = cell<Array<PromptAttachment>>([]);
277-
const mentionable = schemaifyWish<MentionableCharm[]>(
278-
"#mentionable",
279-
[],
280-
);
276+
const mentionable = schemaifyWish<MentionableCharm[]>("#mentionable");
281277

282278
// Derive tools from attachments
283279
const dynamicTools = derive(allAttachments, (attachments) => {
@@ -358,25 +354,28 @@ export default recipe<ChatInput, ChatOutput>(
358354
const title = TitleGenerator({ model, messages });
359355

360356
const promptInput = (
361-
<div slot="footer">
362-
<ct-prompt-input
363-
placeholder="Ask the LLM a question..."
364-
pending={pending}
365-
$mentionable={mentionable}
366-
onct-send={sendMessage({ addMessage, allAttachments })}
367-
onct-stop={cancelGeneration}
368-
onct-attachment-add={addAttachment({ allAttachments })}
369-
onct-attachment-remove={removeAttachment({ allAttachments })}
370-
/>
371-
<ct-select
372-
items={items}
373-
$value={model}
374-
/>
375-
</div>
357+
<ct-prompt-input
358+
slot="footer"
359+
placeholder="Ask the LLM a question..."
360+
pending={pending}
361+
$mentionable={mentionable}
362+
modelItems={items}
363+
$model={model}
364+
onct-send={sendMessage({ addMessage, allAttachments })}
365+
onct-stop={cancelGeneration}
366+
onct-attachment-add={addAttachment({ allAttachments })}
367+
onct-attachment-remove={removeAttachment({ allAttachments })}
368+
/>
376369
);
377370

378371
const chatLog = (
379-
<ct-vscroll flex showScrollbar fadeEdges snapToBottom>
372+
<ct-vscroll
373+
style="padding: 1rem;"
374+
flex
375+
showScrollbar
376+
fadeEdges
377+
snapToBottom
378+
>
380379
<ct-chat
381380
theme={theme}
382381
$messages={messages}
@@ -387,14 +386,15 @@ export default recipe<ChatInput, ChatOutput>(
387386
);
388387

389388
const attachmentsAndTools = (
390-
<ct-hstack gap="normal">
389+
<ct-hstack align="center" gap="1">
391390
<ct-attachments-bar
392391
attachments={allAttachments}
393392
removable
394393
onct-remove={removeAttachment({ allAttachments })}
395394
/>
396395
<ct-tools-chip tools={flattenedTools} />
397-
<button
396+
<ct-button
397+
variant="pill"
398398
type="button"
399399
title="Clear chat"
400400
onClick={clearChat({
@@ -403,7 +403,7 @@ export default recipe<ChatInput, ChatOutput>(
403403
})}
404404
>
405405
Clear
406-
</button>
406+
</ct-button>
407407
</ct-hstack>
408408
);
409409

packages/patterns/default-app.tsx

Lines changed: 23 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
11
/// <cts-enable />
22
import {
3-
BuiltInLLMMessage,
43
Cell,
5-
cell,
64
derive,
75
handler,
8-
ifElse,
9-
lift,
106
NAME,
117
navigateTo,
12-
patternTool,
138
recipe,
149
str,
1510
UI,
@@ -21,7 +16,7 @@ import ChatbotOutliner from "./chatbot-outliner.tsx";
2116
import { default as Note } from "./note.tsx";
2217
import BacklinksIndex, { type MentionableCharm } from "./backlinks-index.tsx";
2318
import ChatList from "./chatbot-list-view.tsx";
24-
import { calculator, readWebpage, searchWeb } from "./common-tools.tsx";
19+
import OmniboxFAB from "./omnibox-fab.tsx";
2520

2621
type MinimalCharm = {
2722
[NAME]?: string;
@@ -61,10 +56,16 @@ const removeCharm = handler<
6156
console.log("charmListCopy before", charmListCopy.length);
6257
charmListCopy.splice(index, 1);
6358
console.log("charmListCopy after", charmListCopy.length);
64-
state.allCharms.resolveAsCell().set(charmListCopy);
59+
state.allCharms.set(charmListCopy);
6560
}
6661
});
6762

63+
const toggleFab = handler<any, { fabExpanded: Cell<boolean> }>(
64+
(_, { fabExpanded }) => {
65+
fabExpanded.set(!fabExpanded.get());
66+
},
67+
);
68+
6869
const spawnChatList = handler<void, void>((_, __) => {
6970
return navigateTo(ChatList({
7071
selectedCharm: { charm: undefined },
@@ -97,88 +98,17 @@ const spawnNote = handler<void, void>((_, __) => {
9798
}));
9899
});
99100

100-
const toggle = handler<any, { value: Cell<boolean> }>((_, { value }) => {
101-
value.set(!value.get());
102-
});
103-
104-
const messagesToNotifications = lift<
105-
{
106-
messages: BuiltInLLMMessage[];
107-
seen: Cell<number>;
108-
notifications: Cell<{ text: string; timestamp: number }[]>;
109-
}
110-
>(({ messages, seen, notifications }) => {
111-
if (messages.length > 0) {
112-
if (seen.get() >= messages.length) {
113-
// If messages length went backwards, reset seen counter
114-
if (seen.get() > messages.length) {
115-
seen.set(0);
116-
} else {
117-
return;
118-
}
119-
}
120-
121-
const latestMessage = messages[messages.length - 1];
122-
if (latestMessage.role === "assistant") {
123-
const contentText = typeof latestMessage.content === "string"
124-
? latestMessage.content
125-
: latestMessage.content.map((part) => {
126-
if (part.type === "text") {
127-
return part.text;
128-
} else if (part.type === "tool-call") {
129-
return `Tool call: ${part.toolName}`;
130-
} else if (part.type === "tool-result") {
131-
return part.output.type === "text"
132-
? part.output.value
133-
: JSON.stringify(part.output.value);
134-
} else if (part.type === "image") {
135-
return "[Image]";
136-
}
137-
return "";
138-
}).join("");
139-
140-
notifications.push({
141-
text: contentText,
142-
timestamp: Date.now(),
143-
});
144-
145-
seen.set(messages.length);
146-
}
147-
}
148-
});
149-
150101
export default recipe<CharmsListInput, CharmsListOutput>(
151102
"DefaultCharmList",
152103
(_) => {
153104
const allCharms = derive<MentionableCharm[], MentionableCharm[]>(
154-
wish<MentionableCharm[]>("#allCharms", []),
105+
wish<MentionableCharm[]>("#allCharms"),
155106
(c) => c,
156107
);
157108
const index = BacklinksIndex({ allCharms });
158-
const fabExpanded = cell(false);
159-
const notifications = cell<{ text: string; timestamp: number }[]>([]);
160-
const seen = cell<number>(0);
161-
162-
const omnibot = Chatbot({
163-
messages: [],
164-
tools: {
165-
searchWeb: {
166-
pattern: searchWeb,
167-
},
168-
readWebpage: {
169-
pattern: readWebpage,
170-
},
171-
// Example of using patternTool with an existing recipe and extra params
172-
calculator: patternTool(calculator, { base: 10 }),
173-
},
174-
});
175109

176-
messagesToNotifications({
177-
messages: omnibot.messages,
178-
seen: seen as unknown as Cell<number>,
179-
notifications: notifications as unknown as Cell<
180-
{ id: string; text: string; timestamp: number }[]
181-
>,
110+
const fab = OmniboxFAB({
111+
mentionable: index.mentionable as unknown as Cell<MentionableCharm[]>,
182112
});
183113

184114
return {
@@ -192,14 +122,20 @@ export default recipe<CharmsListInput, CharmsListOutput>(
192122
preventDefault
193123
onct-keybind={spawnChatList()}
194124
/>
195-
196125
<ct-keybind
197-
code="Escape"
126+
code="KeyO"
127+
meta
198128
preventDefault
199-
onct-keybind={toggle({ value: fabExpanded })}
129+
onct-keybind={toggleFab({ fabExpanded: fab.fabExpanded })}
130+
/>
131+
<ct-keybind
132+
code="KeyO"
133+
ctrl
134+
preventDefault
135+
onct-keybind={toggleFab({ fabExpanded: fab.fabExpanded })}
200136
/>
201137

202-
<ct-toolbar slot="header">
138+
<ct-toolbar slot="header" sticky>
203139
<div slot="start">
204140
<ct-button
205141
onClick={spawnChatList()}
@@ -264,28 +200,8 @@ export default recipe<CharmsListInput, CharmsListOutput>(
264200
</ct-vscroll>
265201
</ct-screen>
266202
),
267-
sidebarUI: (
268-
<div>
269-
{/* TODO(bf): Remove once we fix types to not require ReactNode */}
270-
{omnibot.ui.attachmentsAndTools as any}
271-
{omnibot.ui.chatLog as any}
272-
</div>
273-
),
274-
fabUI: (
275-
<>
276-
<ct-toast-stack
277-
$notifications={notifications}
278-
position="top-right"
279-
auto-dismiss={5000}
280-
max-toasts={5}
281-
/>
282-
{ifElse(
283-
fabExpanded,
284-
omnibot.ui.promptInput,
285-
<ct-button onClick={toggle({ value: fabExpanded })}></ct-button>,
286-
)}
287-
</>
288-
),
203+
sidebarUI: undefined,
204+
fabUI: fab[UI],
289205
};
290206
},
291207
);

0 commit comments

Comments
 (0)