Skip to content

Commit f3ef2ff

Browse files
committed
feat: Implement a new tool integration system with dedicated UI components and server-side utilities, replacing the previous assistant tool and model selection mechanisms.
1 parent a3bc385 commit f3ef2ff

32 files changed

+301
-915
lines changed

apps/mobile/components/ai-chat/thread.tsx

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { FC, useCallback, type ReactNode } from "react";
2-
import { View } from "react-native";
2+
import { Pressable, View } from "react-native";
33

44
import { Icon, IconName } from "../ui/icon";
55
import { Text } from "../ui/text";
@@ -299,6 +299,37 @@ const ModelSelector: React.FC = () => {
299299
);
300300
};
301301

302+
const ToolsSelector: React.FC = () => {
303+
return (
304+
<>
305+
<ComposerPrimitive.SelectTools
306+
header={
307+
<>
308+
<Divider />
309+
<Text className="font-bold">Tools</Text>
310+
</>
311+
}
312+
toolUI={({ toolName, toggle, isEnabled }) => (
313+
<Pressable className="flex-1 active:opacity-50" onPress={toggle}>
314+
<View className="flex-1 p-2">
315+
<View className="mb-1 flex flex-row gap-2 text-lg">
316+
<Text className={cn("font-bold", isEnabled ? "text-cyan-500" : "")}>{toolName.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase())}</Text>
317+
<Icon name="Brain" size={20} />
318+
</View>
319+
<Text className={cn("text-muted-foreground text-sm", isEnabled ? "text-cyan-500" : "")}>
320+
Groundbreaking advancements in reasoning
321+
</Text>
322+
</View>
323+
{isEnabled && (
324+
<Icon name="Check" size={20} color={isEnabled ? "#00b8db" : undefined} />
325+
)}
326+
327+
</Pressable>
328+
)} />
329+
</>
330+
);
331+
};
332+
302333
const ComposerAction: FC = () => {
303334
const textMutedForeground = useCSSVariable("--color-muted-foreground");
304335
const borderColor = useCSSVariable("--color-border");
@@ -322,11 +353,8 @@ const ComposerAction: FC = () => {
322353
<ComposerAddAttachmentSheet />
323354
<Divider />
324355
<ModelSelector />
356+
<ToolsSelector />
325357
</View>
326-
{/* <View className="h-40 px-4">
327-
<ComposerAddAttachmentFile />
328-
<ComposerAddAttachmentImage />
329-
</View> */}
330358
</ActionSheetContent>
331359
</ActionSheet>
332360
);

apps/mobile/components/tools/weather-tool-ui.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { FC, useMemo } from "react";
22
import { View } from "react-native";
3-
import { useAssistantToolUI } from "@creatorem/ai-chat";
3+
import { useToolUI } from "@creatorem/ai-chat";
44
import type { ToolCallMessagePartProps } from "@creatorem/ai-chat/types/message-part-component-types";
55
import { Text } from "../ui/text";
66
import { cn } from "~/lib/cn";
7+
import { IconName } from "../ui/icon";
78

89
const HOUR_LABELS = ["1PM", "2PM", "3PM", "4PM", "5PM"] as const;
910

@@ -121,8 +122,13 @@ const WeatherToolCard: FC<ToolCallMessagePartProps> = ({
121122
};
122123

123124
export const WeatherToolRegistration: FC = () => {
124-
useAssistantToolUI({
125+
useToolUI({
125126
toolName: "weather",
127+
display: {
128+
icon: 'CloudSun' as IconName,
129+
title: 'Weather',
130+
description: 'Get the weather forecast for a location',
131+
},
126132
render: WeatherToolCard,
127133
});
128134
return null;

apps/nextjs-example/app/api/chat/route.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createOpenAI } from "@ai-sdk/openai";
22
import { streamText, convertToModelMessages, type UIMessage, smoothStream } from "ai";
33
import { weatherTool } from "../../../lib/tools/weather-tool";
4-
import { getModel, getDisabledToolsFiter } from "@creatorem/ai-chat/server";
4+
import { getServerUtils } from "@creatorem/ai-chat/server";
55
import { DEFAULT_AI_MODEL } from "@/lib/ai-constants";
66

77
const groq = createOpenAI({
@@ -11,14 +11,15 @@ const groq = createOpenAI({
1111

1212
export async function POST(req: Request) {
1313
const { messages, ...body }: { messages: UIMessage[] } = await req.json();
14+
const { getModel, toolsFilter, frontendTools } = getServerUtils(body);
1415

15-
const model = groq(getModel(body) ?? DEFAULT_AI_MODEL)
16-
const toolsFilter = getDisabledToolsFiter(body);
16+
const model = groq(getModel() ?? DEFAULT_AI_MODEL)
1717

1818
const result = streamText({
1919
model,
2020
messages: await convertToModelMessages(messages),
2121
tools: toolsFilter({
22+
...frontendTools,
2223
weather: weatherTool,
2324
}),
2425
experimental_transform: smoothStream({

apps/nextjs-example/app/test2/page.tsx

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
MoreHorizontalIcon,
1717
PencilIcon,
1818
RefreshCwIcon,
19+
Settings2Icon,
1920
SquareIcon,
2021
} from "lucide-react";
2122
import { TooltipIconButton } from "@/components/ai-chat/tooltip-icon-button";
@@ -62,8 +63,7 @@ import * as BranchPickerPrimitive from "@creatorem/ai-react/primitives/branch-pi
6263
import * as ThreadPrimitive from "@creatorem/ai-react/primitives/thread";
6364
import * as MessagePrimitive from "@creatorem/ai-react/primitives/message";
6465
import { AI_MODELS, DEFAULT_AI_MODEL } from "@/lib/ai-constants";
65-
import { useCallback } from "react";
66-
import { InputGroup } from "@/components/ui/input-group";
66+
import { Switch } from "@/components/ui/switch";
6767

6868
export default function Chat() {
6969
return (
@@ -321,11 +321,50 @@ const ModelSelector: React.FC = () => {
321321
);
322322
};
323323

324+
const ToolsSelector: React.FC = () => {
325+
return (
326+
<DropdownMenu>
327+
<DropdownMenuTrigger asChild>
328+
<Button variant="ghost">
329+
<Settings2Icon className="size-4" />
330+
Tools
331+
</Button>
332+
</DropdownMenuTrigger>
333+
<DropdownMenuContent side="top" align="start" className="z-90 w-64 [--radius:0.95rem]">
334+
<DropdownMenuLabel>AI Tools</DropdownMenuLabel>
335+
<DropdownMenuSeparator />
336+
<ComposerPrimitive.SelectTools empty={(
337+
<DropdownMenuItem disabled>No tools available</DropdownMenuItem>
338+
)} toolUI={({ toolName, toggle, isEnabled }) => (
339+
<DropdownMenuItem
340+
key={toolName}
341+
onSelect={toggle}
342+
className="flex cursor-pointer items-center justify-between gap-2"
343+
>
344+
<div className="flex flex-col items-start">
345+
{toolName.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase())}
346+
</div>
347+
{/* {isEnabled && (
348+
<CheckIcon className="text-primary size-4" />
349+
)} */}
350+
<Switch
351+
checked={isEnabled}
352+
onCheckedChange={() => toggle()}
353+
onClick={(e) => e.stopPropagation()}
354+
/>
355+
</DropdownMenuItem>
356+
)} />
357+
</DropdownMenuContent>
358+
</DropdownMenu>
359+
);
360+
};
361+
324362
const ComposerAction: FC = () => {
325363
return (
326364
<div className="aui-composer-action-wrapper relative mx-2 mb-2 flex items-center justify-between">
327365
<div className="flex">
328366
<ComposerAddAttachment />
367+
<ToolsSelector />
329368
</div>
330369

331370
<div className="flex gap-2">

apps/nextjs-example/app/test2/weather-tool-ui.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22

33
import { FC } from "react";
4-
import { useAssistantToolUI } from "@creatorem/ai-react";
4+
import { useToolUI } from "@creatorem/ai-react";
55
import type { ToolCallMessagePartProps } from "@creatorem/ai-chat/types/message-part-component-types";
66

77
const HOUR_LABELS = ["1PM", "2PM", "3PM", "4PM", "5PM"] as const;
@@ -177,7 +177,7 @@ const WeatherToolCard: FC<ToolCallMessagePartProps> = ({
177177
};
178178

179179
export const WeatherToolRegistration: FC = () => {
180-
useAssistantToolUI({
180+
useToolUI({
181181
toolName: "weather",
182182
render: WeatherToolCard,
183183
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"use client"
2+
3+
import * as React from "react"
4+
import * as SwitchPrimitives from '@radix-ui/react-switch';
5+
6+
import { cn } from "@/lib/utils"
7+
8+
const Switch: React.FC<React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>> = ({ className, ...props }) => (
9+
<SwitchPrimitives.Root
10+
className={cn(
11+
'focus-visible:ring-ring focus-visible:ring-offset-background data-[state=checked]:bg-primary data-[state=unchecked]:bg-input peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-xs transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
12+
className,
13+
)}
14+
{...props}
15+
>
16+
<SwitchPrimitives.Thumb
17+
className={cn(
18+
'bg-background pointer-events-none block h-4 w-4 rounded-full shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0',
19+
)}
20+
/>
21+
</SwitchPrimitives.Root>
22+
);
23+
Switch.displayName = SwitchPrimitives.Root.displayName;
24+
25+
export { Switch };
26+

apps/nextjs-example/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"@radix-ui/react-dropdown-menu": "^2.1.16",
3030
"@radix-ui/react-separator": "^1.1.8",
3131
"@radix-ui/react-slot": "^1.2.4",
32+
"@radix-ui/react-switch": "^1.2.6",
3233
"@radix-ui/react-tooltip": "^1.2.8",
3334
"ai": "^6.0.50",
3435
"class-variance-authority": "^0.7.1",

packages-test-3/ai-chat/src/ai-provider.tsx

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import { createStore, useStore, type StoreApi } from 'zustand';
66
import { ThreadAdapter, AttachmentAdapter, DictationAdapter } from "./types/adapters";
77
import { emptyStorageThreadAdapter } from "./adapters/empty-storage-adapter";
88
import { Thread, Threads } from "./types/entities";
9-
import { useChat, UseChatOptions } from "@ai-sdk/react";
9+
import { UseChatOptions } from "@ai-sdk/react";
1010
import { AiChatEventHandler, AiChatEvents } from "./primitives/events";
11-
import type { Toolkit } from "./types/toolbox";
11+
import { UITool } from "./types/tools-types";
1212

1313
type LanguageModelV1CallSettings = {
1414
maxTokens?: number;
@@ -26,6 +26,7 @@ type LanguageModelConfig = {
2626
modelName?: string;
2727
};
2828

29+
2930
export type AiContextType = {
3031
adapters?: {
3132
attachment?: AttachmentAdapter;
@@ -34,18 +35,19 @@ export type AiContextType = {
3435
}
3536
priority?: number | undefined;
3637
system?: string | undefined;
37-
tools?: Record<string, Tool<any, any>> | undefined;
38-
toolkit?: Toolkit | undefined;
3938
callSettings?: LanguageModelV1CallSettings | undefined;
4039
config?: LanguageModelConfig | undefined;
4140
selectedModel: string | null;
4241
setSelectedModel: (selectedModel: string | null) => void;
43-
disabledTools: string[];
44-
setDisabledTools: (disabledTools: string[]) => void;
4542
chatOptions?: Omit<UseChatOptions<Thread['messages'][0]> & ChatInit<Thread['messages'][0]>, 'id' | 'transport'> & {
4643
transportOptions?: HttpChatTransportInitOptions<Thread['messages'][0]>
4744
}
4845
eventHandler: AiChatEventHandler
46+
47+
tools: Record<string,UITool<any, any>>
48+
upsertTool: (
49+
tool: UITool<any, any>
50+
) => void;
4951
};
5052

5153
const AiContextStoreCtx = React.createContext<StoreApi<AiContextType> | null>(null);
@@ -72,16 +74,24 @@ export const useAiEvent = <TEvent extends keyof AiChatEvents>(name: TEvent, p: (
7274
}, [eventHandler, name, p])
7375
};
7476

75-
export function AiProvider({ children, ...value }: { children: React.ReactNode } & Omit<AiContextType, 'eventHandler' | 'selectedModel' | 'disabledTools' | 'setSelectedModel' | 'setDisabledTools'> & Partial<Pick<AiContextType, 'selectedModel' | 'disabledTools'>>) {
77+
export function AiProvider({ children, ...value }: { children: React.ReactNode } & Omit<AiContextType, 'eventHandler' | 'selectedModel' | 'tools' | 'upsertTool'>) {
7678
// Create store once
7779
const storeRef = useRef<StoreApi<AiContextType> | null>(null);
7880
if (storeRef.current === null) {
7981
storeRef.current = createStore<AiContextType>((set) => ({
8082
...value,
81-
selectedModel: value.selectedModel ?? null,
83+
selectedModel: null,
8284
setSelectedModel: (selectedModel) => set({ selectedModel }),
83-
disabledTools: value.disabledTools ?? [],
84-
setDisabledTools: (disabledTools) => set({ disabledTools }),
85+
tools: {},
86+
upsertTool: (tool) => {
87+
const toolName = tool.toolName;
88+
set((prev) => ({
89+
tools: {
90+
...prev.tools,
91+
[toolName]: tool,
92+
},
93+
}));
94+
},
8595
eventHandler: new AiChatEventHandler(),
8696
}));
8797
}
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
export { useAssistantTool } from "./use-assistant-tool";
2-
export { useAssistantToolUI } from "./use-assistant-tool-ui";
1+
export { useToolUI } from "./use-tool-ui";

packages-test-3/ai-chat/src/model-context/use-assistant-tool-ui.tsx

Lines changed: 0 additions & 21 deletions
This file was deleted.

0 commit comments

Comments
 (0)