Spaces:
Running
Running
Commit
·
d8c725b
1
Parent(s):
6d663cf
ui updates and added anthropic models for testing
Browse files- ai/providers.ts +18 -20
- ai/tools.ts +0 -11
- app/api/chat/route.ts +23 -1
- components/chat-sidebar.tsx +89 -62
- components/markdown.tsx +22 -22
- components/message.tsx +55 -24
- components/messages.tsx +4 -3
- components/model-picker.tsx +7 -7
- components/theme-toggle.tsx +8 -3
- components/tool-invocation.tsx +51 -23
- instrumentation.ts +0 -13
- lib/hooks/use-scroll-to-bottom.tsx +59 -13
- package-lock.json +0 -0
- package.json +2 -9
- pnpm-lock.yaml +0 -0
ai/providers.ts
CHANGED
@@ -1,9 +1,8 @@
|
|
1 |
-
import { xai } from "@ai-sdk/xai";
|
2 |
import { openai } from "@ai-sdk/openai";
|
3 |
import { google } from "@ai-sdk/google";
|
4 |
import { groq } from "@ai-sdk/groq";
|
5 |
import { customProvider, wrapLanguageModel, extractReasoningMiddleware } from "ai";
|
6 |
-
import {
|
7 |
export interface ModelInfo {
|
8 |
provider: string;
|
9 |
name: string;
|
@@ -14,35 +13,41 @@ export interface ModelInfo {
|
|
14 |
|
15 |
const middleware = extractReasoningMiddleware({
|
16 |
tagName: 'think',
|
17 |
-
separator: '\n',
|
18 |
});
|
19 |
|
20 |
const languageModels = {
|
|
|
21 |
"gpt-4.1-mini": openai("gpt-4.1-mini"),
|
22 |
-
"
|
23 |
"qwen-qwq": wrapLanguageModel(
|
24 |
{
|
25 |
-
model: groq("qwen-qwq-32b"),
|
26 |
middleware
|
27 |
}
|
28 |
),
|
29 |
-
"command-a": cohere('command-a-03-2025')
|
30 |
};
|
31 |
|
32 |
export const modelDetails: Record<keyof typeof languageModels, ModelInfo> = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
"gpt-4.1-mini": {
|
34 |
provider: "OpenAI",
|
35 |
name: "GPT-4.1 Mini",
|
36 |
description: "Compact version of OpenAI's GPT-4.1 with good balance of capabilities, including vision.",
|
37 |
apiVersion: "gpt-4.1-mini",
|
38 |
-
capabilities: [
|
39 |
},
|
40 |
-
"
|
41 |
-
provider: "
|
42 |
-
name: "
|
43 |
-
description: "Latest version of
|
44 |
-
apiVersion: "
|
45 |
-
capabilities: ["
|
46 |
},
|
47 |
"qwen-qwq": {
|
48 |
provider: "Groq",
|
@@ -51,13 +56,6 @@ export const modelDetails: Record<keyof typeof languageModels, ModelInfo> = {
|
|
51 |
apiVersion: "qwen-qwq",
|
52 |
capabilities: ["Reasoning", "Efficient", "Agentic"]
|
53 |
},
|
54 |
-
"command-a": {
|
55 |
-
provider: "Cohere",
|
56 |
-
name: "Command A",
|
57 |
-
description: "Latest version of Cohere's Command A with strong reasoning and coding capabilities.",
|
58 |
-
apiVersion: "command-a-03-2025",
|
59 |
-
capabilities: ["Smart", "Fast", "Reasoning"]
|
60 |
-
}
|
61 |
};
|
62 |
|
63 |
export const model = customProvider({
|
|
|
|
|
1 |
import { openai } from "@ai-sdk/openai";
|
2 |
import { google } from "@ai-sdk/google";
|
3 |
import { groq } from "@ai-sdk/groq";
|
4 |
import { customProvider, wrapLanguageModel, extractReasoningMiddleware } from "ai";
|
5 |
+
import { anthropic } from "@ai-sdk/anthropic";
|
6 |
export interface ModelInfo {
|
7 |
provider: string;
|
8 |
name: string;
|
|
|
13 |
|
14 |
const middleware = extractReasoningMiddleware({
|
15 |
tagName: 'think',
|
|
|
16 |
});
|
17 |
|
18 |
const languageModels = {
|
19 |
+
"gemini-2.5-flash": google("gemini-2.5-flash-preview-04-17"),
|
20 |
"gpt-4.1-mini": openai("gpt-4.1-mini"),
|
21 |
+
"claude-3-7-sonnet": anthropic('claude-3-7-sonnet-20250219'),
|
22 |
"qwen-qwq": wrapLanguageModel(
|
23 |
{
|
24 |
+
model: groq("qwen-qwq-32b"),
|
25 |
middleware
|
26 |
}
|
27 |
),
|
|
|
28 |
};
|
29 |
|
30 |
export const modelDetails: Record<keyof typeof languageModels, ModelInfo> = {
|
31 |
+
"gemini-2.5-flash": {
|
32 |
+
provider: "Google",
|
33 |
+
name: "Gemini 2.5 Flash",
|
34 |
+
description: "Latest version of Google's Gemini 2.5 Flash with strong reasoning and coding capabilities.",
|
35 |
+
apiVersion: "gemini-2.5-flash-preview-04-17",
|
36 |
+
capabilities: ["Balance", "Efficient", "Agentic"]
|
37 |
+
},
|
38 |
"gpt-4.1-mini": {
|
39 |
provider: "OpenAI",
|
40 |
name: "GPT-4.1 Mini",
|
41 |
description: "Compact version of OpenAI's GPT-4.1 with good balance of capabilities, including vision.",
|
42 |
apiVersion: "gpt-4.1-mini",
|
43 |
+
capabilities: ["Balance", "Creative", "Vision"]
|
44 |
},
|
45 |
+
"claude-3-7-sonnet": {
|
46 |
+
provider: "Anthropic",
|
47 |
+
name: "Claude 3.7 Sonnet",
|
48 |
+
description: "Latest version of Anthropic's Claude 3.7 Sonnet with strong reasoning and coding capabilities.",
|
49 |
+
apiVersion: "claude-3-7-sonnet-20250219",
|
50 |
+
capabilities: ["Reasoning", "Efficient", "Agentic"]
|
51 |
},
|
52 |
"qwen-qwq": {
|
53 |
provider: "Groq",
|
|
|
56 |
apiVersion: "qwen-qwq",
|
57 |
capabilities: ["Reasoning", "Efficient", "Agentic"]
|
58 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
59 |
};
|
60 |
|
61 |
export const model = customProvider({
|
ai/tools.ts
DELETED
@@ -1,11 +0,0 @@
|
|
1 |
-
import { VercelAIToolSet } from "composio-core";
|
2 |
-
|
3 |
-
const toolset = new VercelAIToolSet();
|
4 |
-
|
5 |
-
export const composioTools = await toolset.getTools(
|
6 |
-
{
|
7 |
-
apps: [
|
8 |
-
"tavily",
|
9 |
-
]
|
10 |
-
}
|
11 |
-
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/chat/route.ts
CHANGED
@@ -113,8 +113,17 @@ export async function POST(req: Request) {
|
|
113 |
});
|
114 |
}
|
115 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
116 |
// if python is passed in the command, install the python package mentioned in args after -m with subprocess or use regex to find the package name
|
117 |
-
if (mcpServer.command.includes('python3')) {
|
118 |
const packageName = mcpServer.args[mcpServer.args.indexOf('-m') + 1];
|
119 |
console.log("installing python package", packageName);
|
120 |
const subprocess = spawn('pip3', ['install', packageName]);
|
@@ -195,6 +204,19 @@ export async function POST(req: Request) {
|
|
195 |
messages,
|
196 |
tools,
|
197 |
maxSteps: 20,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
198 |
onError: (error) => {
|
199 |
console.error(JSON.stringify(error, null, 2));
|
200 |
},
|
|
|
113 |
});
|
114 |
}
|
115 |
|
116 |
+
// Check for uvx pattern and transform to python3 -m uv run
|
117 |
+
if (mcpServer.command === 'uvx') {
|
118 |
+
console.log("Detected uvx pattern, transforming to python3 -m uv run");
|
119 |
+
mcpServer.command = 'python3';
|
120 |
+
// Get the tool name (first argument)
|
121 |
+
const toolName = mcpServer.args[0];
|
122 |
+
// Replace args with the new pattern
|
123 |
+
mcpServer.args = ['-m', 'uv', 'run', toolName, ...mcpServer.args.slice(1)];
|
124 |
+
}
|
125 |
// if python is passed in the command, install the python package mentioned in args after -m with subprocess or use regex to find the package name
|
126 |
+
else if (mcpServer.command.includes('python3')) {
|
127 |
const packageName = mcpServer.args[mcpServer.args.indexOf('-m') + 1];
|
128 |
console.log("installing python package", packageName);
|
129 |
const subprocess = spawn('pip3', ['install', packageName]);
|
|
|
204 |
messages,
|
205 |
tools,
|
206 |
maxSteps: 20,
|
207 |
+
providerOptions: {
|
208 |
+
google: {
|
209 |
+
thinkingConfig: {
|
210 |
+
thinkingBudget: 0,
|
211 |
+
},
|
212 |
+
},
|
213 |
+
anthropic: {
|
214 |
+
thinking: {
|
215 |
+
type: 'enabled',
|
216 |
+
budgetTokens: 12000
|
217 |
+
},
|
218 |
+
}
|
219 |
+
},
|
220 |
onError: (error) => {
|
221 |
console.error(JSON.stringify(error, null, 2));
|
222 |
},
|
components/chat-sidebar.tsx
CHANGED
@@ -1,8 +1,8 @@
|
|
1 |
"use client";
|
2 |
|
3 |
-
import { useState, useEffect
|
4 |
import { useRouter, usePathname } from "next/navigation";
|
5 |
-
import { MessageSquare, PlusCircle, Trash2, ServerIcon, Settings,
|
6 |
import {
|
7 |
Sidebar,
|
8 |
SidebarContent,
|
@@ -25,8 +25,6 @@ import Image from "next/image";
|
|
25 |
import { MCPServerManager } from "./mcp-server-manager";
|
26 |
import { ThemeToggle } from "./theme-toggle";
|
27 |
import { getUserId, updateUserId } from "@/lib/user-id";
|
28 |
-
import { useLocalStorage } from "@/lib/hooks/use-local-storage";
|
29 |
-
import { STORAGE_KEYS } from "@/lib/constants";
|
30 |
import { useChats } from "@/lib/hooks/use-chats";
|
31 |
import { cn } from "@/lib/utils";
|
32 |
import Link from "next/link";
|
@@ -51,6 +49,8 @@ import {
|
|
51 |
import { Input } from "@/components/ui/input";
|
52 |
import { Label } from "@/components/ui/label";
|
53 |
import { useMCP } from "@/lib/context/mcp-context";
|
|
|
|
|
54 |
|
55 |
export function ChatSidebar() {
|
56 |
const router = useRouter();
|
@@ -115,6 +115,23 @@ export function ChatSidebar() {
|
|
115 |
return null; // Or a loading spinner
|
116 |
}
|
117 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
118 |
return (
|
119 |
<Sidebar className="shadow-sm bg-background/80 dark:bg-background/40 backdrop-blur-md" collapsible="icon">
|
120 |
<SidebarHeader className="p-4 border-b border-border/40">
|
@@ -144,12 +161,7 @@ export function ChatSidebar() {
|
|
144 |
)}>
|
145 |
<SidebarMenu>
|
146 |
{isLoading ? (
|
147 |
-
|
148 |
-
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
149 |
-
{!isCollapsed && (
|
150 |
-
<span className="ml-2 text-xs text-muted-foreground">Loading...</span>
|
151 |
-
)}
|
152 |
-
</div>
|
153 |
) : chats.length === 0 ? (
|
154 |
<div className={`flex items-center justify-center py-3 ${isCollapsed ? "" : "px-4"}`}>
|
155 |
{isCollapsed ? (
|
@@ -164,50 +176,60 @@ export function ChatSidebar() {
|
|
164 |
)}
|
165 |
</div>
|
166 |
) : (
|
167 |
-
|
168 |
-
|
169 |
-
<
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
pathname === `/chat/${chat.id}` ? "bg-secondary/60 hover:bg-secondary/60" : ""
|
176 |
-
)}
|
177 |
>
|
178 |
-
<
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
"
|
185 |
-
pathname === `/chat/${chat.id}` ? "
|
186 |
-
)} />
|
187 |
-
{!isCollapsed && (
|
188 |
-
<span className={cn(
|
189 |
-
"ml-2 truncate text-sm",
|
190 |
-
pathname === `/chat/${chat.id}` ? "text-foreground font-medium" : "text-foreground/80"
|
191 |
-
)} title={chat.title}>
|
192 |
-
{chat.title.length > 18 ? `${chat.title.slice(0, 18)}...` : chat.title}
|
193 |
-
</span>
|
194 |
)}
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
size="icon"
|
200 |
-
className="h-6 w-6 text-muted-foreground hover:text-foreground flex-shrink-0"
|
201 |
-
onClick={(e) => handleDeleteChat(chat.id, e)}
|
202 |
-
title="Delete chat"
|
203 |
>
|
204 |
-
<
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
211 |
)}
|
212 |
</SidebarMenu>
|
213 |
</SidebarGroupContent>
|
@@ -265,18 +287,23 @@ export function ChatSidebar() {
|
|
265 |
|
266 |
<SidebarFooter className="p-4 border-t border-border/40 mt-auto">
|
267 |
<div className={`flex flex-col ${isCollapsed ? "items-center" : ""} gap-3`}>
|
268 |
-
<
|
269 |
-
|
270 |
-
|
271 |
-
"w-full bg-primary text-primary-foreground hover:bg-primary/90",
|
272 |
-
isCollapsed ? "w-8 h-8 p-0" : ""
|
273 |
-
)}
|
274 |
-
onClick={handleNewChat}
|
275 |
-
title={isCollapsed ? "New Chat" : undefined}
|
276 |
>
|
277 |
-
<
|
278 |
-
|
279 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
280 |
|
281 |
<DropdownMenu modal={false}>
|
282 |
<DropdownMenuTrigger asChild>
|
|
|
1 |
"use client";
|
2 |
|
3 |
+
import { useState, useEffect } from "react";
|
4 |
import { useRouter, usePathname } from "next/navigation";
|
5 |
+
import { MessageSquare, PlusCircle, Trash2, ServerIcon, Settings, Sparkles, ChevronsUpDown, Copy, Pencil, Github } from "lucide-react";
|
6 |
import {
|
7 |
Sidebar,
|
8 |
SidebarContent,
|
|
|
25 |
import { MCPServerManager } from "./mcp-server-manager";
|
26 |
import { ThemeToggle } from "./theme-toggle";
|
27 |
import { getUserId, updateUserId } from "@/lib/user-id";
|
|
|
|
|
28 |
import { useChats } from "@/lib/hooks/use-chats";
|
29 |
import { cn } from "@/lib/utils";
|
30 |
import Link from "next/link";
|
|
|
49 |
import { Input } from "@/components/ui/input";
|
50 |
import { Label } from "@/components/ui/label";
|
51 |
import { useMCP } from "@/lib/context/mcp-context";
|
52 |
+
import { Skeleton } from "@/components/ui/skeleton";
|
53 |
+
import { AnimatePresence, motion } from "motion/react";
|
54 |
|
55 |
export function ChatSidebar() {
|
56 |
const router = useRouter();
|
|
|
115 |
return null; // Or a loading spinner
|
116 |
}
|
117 |
|
118 |
+
// Create chat loading skeletons
|
119 |
+
const renderChatSkeletons = () => {
|
120 |
+
return Array(3).fill(0).map((_, index) => (
|
121 |
+
<SidebarMenuItem key={`skeleton-${index}`}>
|
122 |
+
<div className={`flex items-center gap-2 px-3 py-2 ${isCollapsed ? "justify-center" : ""}`}>
|
123 |
+
<Skeleton className="h-4 w-4 rounded-full" />
|
124 |
+
{!isCollapsed && (
|
125 |
+
<>
|
126 |
+
<Skeleton className="h-4 w-full max-w-[180px]" />
|
127 |
+
<Skeleton className="h-5 w-5 ml-auto rounded-md flex-shrink-0" />
|
128 |
+
</>
|
129 |
+
)}
|
130 |
+
</div>
|
131 |
+
</SidebarMenuItem>
|
132 |
+
));
|
133 |
+
};
|
134 |
+
|
135 |
return (
|
136 |
<Sidebar className="shadow-sm bg-background/80 dark:bg-background/40 backdrop-blur-md" collapsible="icon">
|
137 |
<SidebarHeader className="p-4 border-b border-border/40">
|
|
|
161 |
)}>
|
162 |
<SidebarMenu>
|
163 |
{isLoading ? (
|
164 |
+
renderChatSkeletons()
|
|
|
|
|
|
|
|
|
|
|
165 |
) : chats.length === 0 ? (
|
166 |
<div className={`flex items-center justify-center py-3 ${isCollapsed ? "" : "px-4"}`}>
|
167 |
{isCollapsed ? (
|
|
|
176 |
)}
|
177 |
</div>
|
178 |
) : (
|
179 |
+
<AnimatePresence initial={false}>
|
180 |
+
{chats.map((chat) => (
|
181 |
+
<motion.div
|
182 |
+
key={chat.id}
|
183 |
+
initial={{ opacity: 0, height: 0, y: -10 }}
|
184 |
+
animate={{ opacity: 1, height: "auto", y: 0 }}
|
185 |
+
exit={{ opacity: 0, height: 0 }}
|
186 |
+
transition={{ duration: 0.2 }}
|
|
|
|
|
187 |
>
|
188 |
+
<SidebarMenuItem>
|
189 |
+
<SidebarMenuButton
|
190 |
+
asChild
|
191 |
+
tooltip={isCollapsed ? chat.title : undefined}
|
192 |
+
data-active={pathname === `/chat/${chat.id}`}
|
193 |
+
className={cn(
|
194 |
+
"transition-all hover:bg-primary/10 active:bg-primary/15",
|
195 |
+
pathname === `/chat/${chat.id}` ? "bg-secondary/60 hover:bg-secondary/60" : ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
196 |
)}
|
197 |
+
>
|
198 |
+
<Link
|
199 |
+
href={`/chat/${chat.id}`}
|
200 |
+
className="flex items-center justify-between w-full gap-1"
|
|
|
|
|
|
|
|
|
201 |
>
|
202 |
+
<div className="flex items-center min-w-0 overflow-hidden flex-1 pr-2">
|
203 |
+
<MessageSquare className={cn(
|
204 |
+
"h-4 w-4 flex-shrink-0",
|
205 |
+
pathname === `/chat/${chat.id}` ? "text-foreground" : "text-muted-foreground"
|
206 |
+
)} />
|
207 |
+
{!isCollapsed && (
|
208 |
+
<span className={cn(
|
209 |
+
"ml-2 truncate text-sm",
|
210 |
+
pathname === `/chat/${chat.id}` ? "text-foreground font-medium" : "text-foreground/80"
|
211 |
+
)} title={chat.title}>
|
212 |
+
{chat.title.length > 18 ? `${chat.title.slice(0, 18)}...` : chat.title}
|
213 |
+
</span>
|
214 |
+
)}
|
215 |
+
</div>
|
216 |
+
{!isCollapsed && (
|
217 |
+
<Button
|
218 |
+
variant="ghost"
|
219 |
+
size="icon"
|
220 |
+
className="h-6 w-6 text-muted-foreground hover:text-foreground flex-shrink-0"
|
221 |
+
onClick={(e) => handleDeleteChat(chat.id, e)}
|
222 |
+
title="Delete chat"
|
223 |
+
>
|
224 |
+
<Trash2 className="h-3.5 w-3.5" />
|
225 |
+
</Button>
|
226 |
+
)}
|
227 |
+
</Link>
|
228 |
+
</SidebarMenuButton>
|
229 |
+
</SidebarMenuItem>
|
230 |
+
</motion.div>
|
231 |
+
))}
|
232 |
+
</AnimatePresence>
|
233 |
)}
|
234 |
</SidebarMenu>
|
235 |
</SidebarGroupContent>
|
|
|
287 |
|
288 |
<SidebarFooter className="p-4 border-t border-border/40 mt-auto">
|
289 |
<div className={`flex flex-col ${isCollapsed ? "items-center" : ""} gap-3`}>
|
290 |
+
<motion.div
|
291 |
+
whileHover={{ scale: 1.02 }}
|
292 |
+
whileTap={{ scale: 0.98 }}
|
|
|
|
|
|
|
|
|
|
|
293 |
>
|
294 |
+
<Button
|
295 |
+
variant="default"
|
296 |
+
className={cn(
|
297 |
+
"w-full bg-primary text-primary-foreground hover:bg-primary/90",
|
298 |
+
isCollapsed ? "w-8 h-8 p-0" : ""
|
299 |
+
)}
|
300 |
+
onClick={handleNewChat}
|
301 |
+
title={isCollapsed ? "New Chat" : undefined}
|
302 |
+
>
|
303 |
+
<PlusCircle className={`${isCollapsed ? "" : "mr-2"} h-4 w-4`} />
|
304 |
+
{!isCollapsed && <span>New Chat</span>}
|
305 |
+
</Button>
|
306 |
+
</motion.div>
|
307 |
|
308 |
<DropdownMenu modal={false}>
|
309 |
<DropdownMenuTrigger asChild>
|
components/markdown.tsx
CHANGED
@@ -7,7 +7,7 @@ import { cn } from "@/lib/utils";
|
|
7 |
|
8 |
const components: Partial<Components> = {
|
9 |
pre: ({ children, ...props }) => (
|
10 |
-
<pre className="overflow-x-auto rounded-lg bg-zinc-100 dark:bg-zinc-800/50 p-
|
11 |
{children}
|
12 |
</pre>
|
13 |
),
|
@@ -18,7 +18,7 @@ const components: Partial<Components> = {
|
|
18 |
if (isInline) {
|
19 |
return (
|
20 |
<code
|
21 |
-
className="px-1
|
22 |
{...props}
|
23 |
>
|
24 |
{children}
|
@@ -32,22 +32,22 @@ const components: Partial<Components> = {
|
|
32 |
);
|
33 |
},
|
34 |
ol: ({ node, children, ...props }) => (
|
35 |
-
<ol className="list-decimal list-outside ml-4 space-y-
|
36 |
{children}
|
37 |
</ol>
|
38 |
),
|
39 |
ul: ({ node, children, ...props }) => (
|
40 |
-
<ul className="list-disc list-outside ml-4 space-y-
|
41 |
{children}
|
42 |
</ul>
|
43 |
),
|
44 |
li: ({ node, children, ...props }) => (
|
45 |
-
<li className="leading-
|
46 |
{children}
|
47 |
</li>
|
48 |
),
|
49 |
p: ({ node, children, ...props }) => (
|
50 |
-
<p className="leading-
|
51 |
{children}
|
52 |
</p>
|
53 |
),
|
@@ -63,7 +63,7 @@ const components: Partial<Components> = {
|
|
63 |
),
|
64 |
blockquote: ({ node, children, ...props }) => (
|
65 |
<blockquote
|
66 |
-
className="border-l-2 border-zinc-200 dark:border-zinc-700 pl-
|
67 |
{...props}
|
68 |
>
|
69 |
{children}
|
@@ -72,7 +72,7 @@ const components: Partial<Components> = {
|
|
72 |
a: ({ node, children, ...props }) => (
|
73 |
// @ts-expect-error error
|
74 |
<Link
|
75 |
-
className="text-blue-500 hover:underline hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 transition-colors"
|
76 |
target="_blank"
|
77 |
rel="noreferrer"
|
78 |
{...props}
|
@@ -81,72 +81,72 @@ const components: Partial<Components> = {
|
|
81 |
</Link>
|
82 |
),
|
83 |
h1: ({ node, children, ...props }) => (
|
84 |
-
<h1 className="text-
|
85 |
{children}
|
86 |
</h1>
|
87 |
),
|
88 |
h2: ({ node, children, ...props }) => (
|
89 |
-
<h2 className="text-
|
90 |
{children}
|
91 |
</h2>
|
92 |
),
|
93 |
h3: ({ node, children, ...props }) => (
|
94 |
-
<h3 className="text-
|
95 |
{children}
|
96 |
</h3>
|
97 |
),
|
98 |
h4: ({ node, children, ...props }) => (
|
99 |
-
<h4 className="text-
|
100 |
{children}
|
101 |
</h4>
|
102 |
),
|
103 |
h5: ({ node, children, ...props }) => (
|
104 |
-
<h5 className="text-
|
105 |
{children}
|
106 |
</h5>
|
107 |
),
|
108 |
h6: ({ node, children, ...props }) => (
|
109 |
-
<h6 className="text-
|
110 |
{children}
|
111 |
</h6>
|
112 |
),
|
113 |
table: ({ node, children, ...props }) => (
|
114 |
-
<div className="my-
|
115 |
-
<table className="min-w-full divide-y divide-zinc-200 dark:divide-zinc-700" {...props}>
|
116 |
{children}
|
117 |
</table>
|
118 |
</div>
|
119 |
),
|
120 |
thead: ({ node, children, ...props }) => (
|
121 |
-
<thead className="bg-zinc-50 dark:bg-zinc-800/50" {...props}>
|
122 |
{children}
|
123 |
</thead>
|
124 |
),
|
125 |
tbody: ({ node, children, ...props }) => (
|
126 |
-
<tbody className="divide-y divide-zinc-200 dark:divide-zinc-700 bg-white dark:bg-transparent" {...props}>
|
127 |
{children}
|
128 |
</tbody>
|
129 |
),
|
130 |
tr: ({ node, children, ...props }) => (
|
131 |
-
<tr className="transition-colors hover:bg-zinc-50 dark:hover:bg-zinc-800/30" {...props}>
|
132 |
{children}
|
133 |
</tr>
|
134 |
),
|
135 |
th: ({ node, children, ...props }) => (
|
136 |
<th
|
137 |
-
className="px-
|
138 |
{...props}
|
139 |
>
|
140 |
{children}
|
141 |
</th>
|
142 |
),
|
143 |
td: ({ node, children, ...props }) => (
|
144 |
-
<td className="px-
|
145 |
{children}
|
146 |
</td>
|
147 |
),
|
148 |
hr: ({ node, ...props }) => (
|
149 |
-
<hr className="my-
|
150 |
),
|
151 |
};
|
152 |
|
|
|
7 |
|
8 |
const components: Partial<Components> = {
|
9 |
pre: ({ children, ...props }) => (
|
10 |
+
<pre className="overflow-x-auto rounded-lg bg-zinc-100 dark:bg-zinc-800/50 black:bg-zinc-800/50 p-2.5 my-1.5 text-sm" {...props}>
|
11 |
{children}
|
12 |
</pre>
|
13 |
),
|
|
|
18 |
if (isInline) {
|
19 |
return (
|
20 |
<code
|
21 |
+
className="px-1 py-0.5 rounded-md bg-zinc-100 dark:bg-zinc-800/50 black:bg-zinc-800/50 text-zinc-700 dark:text-zinc-300 black:text-zinc-300 text-[0.9em] font-mono"
|
22 |
{...props}
|
23 |
>
|
24 |
{children}
|
|
|
32 |
);
|
33 |
},
|
34 |
ol: ({ node, children, ...props }) => (
|
35 |
+
<ol className="list-decimal list-outside ml-4 space-y-0.5 my-1.5" {...props}>
|
36 |
{children}
|
37 |
</ol>
|
38 |
),
|
39 |
ul: ({ node, children, ...props }) => (
|
40 |
+
<ul className="list-disc list-outside ml-4 space-y-0.5 my-1.5" {...props}>
|
41 |
{children}
|
42 |
</ul>
|
43 |
),
|
44 |
li: ({ node, children, ...props }) => (
|
45 |
+
<li className="leading-normal" {...props}>
|
46 |
{children}
|
47 |
</li>
|
48 |
),
|
49 |
p: ({ node, children, ...props }) => (
|
50 |
+
<p className="leading-relaxed my-1" {...props}>
|
51 |
{children}
|
52 |
</p>
|
53 |
),
|
|
|
63 |
),
|
64 |
blockquote: ({ node, children, ...props }) => (
|
65 |
<blockquote
|
66 |
+
className="border-l-2 border-zinc-200 dark:border-zinc-700 black:border-zinc-700 pl-3 my-1.5 italic text-zinc-600 dark:text-zinc-400 black:text-zinc-400"
|
67 |
{...props}
|
68 |
>
|
69 |
{children}
|
|
|
72 |
a: ({ node, children, ...props }) => (
|
73 |
// @ts-expect-error error
|
74 |
<Link
|
75 |
+
className="text-blue-500 hover:underline hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 black:text-blue-400 black:hover:text-blue-300 transition-colors"
|
76 |
target="_blank"
|
77 |
rel="noreferrer"
|
78 |
{...props}
|
|
|
81 |
</Link>
|
82 |
),
|
83 |
h1: ({ node, children, ...props }) => (
|
84 |
+
<h1 className="text-2xl font-semibold mt-3 mb-1.5 text-zinc-800 dark:text-zinc-200 black:text-zinc-200" {...props}>
|
85 |
{children}
|
86 |
</h1>
|
87 |
),
|
88 |
h2: ({ node, children, ...props }) => (
|
89 |
+
<h2 className="text-xl font-semibold mt-2.5 mb-1.5 text-zinc-800 dark:text-zinc-200 black:text-zinc-200" {...props}>
|
90 |
{children}
|
91 |
</h2>
|
92 |
),
|
93 |
h3: ({ node, children, ...props }) => (
|
94 |
+
<h3 className="text-lg font-semibold mt-2 mb-1 text-zinc-800 dark:text-zinc-200 black:text-zinc-200" {...props}>
|
95 |
{children}
|
96 |
</h3>
|
97 |
),
|
98 |
h4: ({ node, children, ...props }) => (
|
99 |
+
<h4 className="text-base font-semibold mt-2 mb-1 text-zinc-800 dark:text-zinc-200 black:text-zinc-200" {...props}>
|
100 |
{children}
|
101 |
</h4>
|
102 |
),
|
103 |
h5: ({ node, children, ...props }) => (
|
104 |
+
<h5 className="text-sm font-semibold mt-2 mb-1 text-zinc-800 dark:text-zinc-200 black:text-zinc-200" {...props}>
|
105 |
{children}
|
106 |
</h5>
|
107 |
),
|
108 |
h6: ({ node, children, ...props }) => (
|
109 |
+
<h6 className="text-xs font-semibold mt-2 mb-0.5 text-zinc-800 dark:text-zinc-200 black:text-zinc-200" {...props}>
|
110 |
{children}
|
111 |
</h6>
|
112 |
),
|
113 |
table: ({ node, children, ...props }) => (
|
114 |
+
<div className="my-1.5 overflow-x-auto">
|
115 |
+
<table className="min-w-full divide-y divide-zinc-200 dark:divide-zinc-700 black:divide-zinc-700" {...props}>
|
116 |
{children}
|
117 |
</table>
|
118 |
</div>
|
119 |
),
|
120 |
thead: ({ node, children, ...props }) => (
|
121 |
+
<thead className="bg-zinc-50 dark:bg-zinc-800/50 black:bg-zinc-800/50" {...props}>
|
122 |
{children}
|
123 |
</thead>
|
124 |
),
|
125 |
tbody: ({ node, children, ...props }) => (
|
126 |
+
<tbody className="divide-y divide-zinc-200 dark:divide-zinc-700 black:divide-zinc-700 bg-white dark:bg-transparent black:bg-transparent" {...props}>
|
127 |
{children}
|
128 |
</tbody>
|
129 |
),
|
130 |
tr: ({ node, children, ...props }) => (
|
131 |
+
<tr className="transition-colors hover:bg-zinc-50 dark:hover:bg-zinc-800/30 black:hover:bg-zinc-800/30" {...props}>
|
132 |
{children}
|
133 |
</tr>
|
134 |
),
|
135 |
th: ({ node, children, ...props }) => (
|
136 |
<th
|
137 |
+
className="px-3 py-1.5 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 black:text-zinc-400 uppercase tracking-wider"
|
138 |
{...props}
|
139 |
>
|
140 |
{children}
|
141 |
</th>
|
142 |
),
|
143 |
td: ({ node, children, ...props }) => (
|
144 |
+
<td className="px-3 py-1.5 text-sm" {...props}>
|
145 |
{children}
|
146 |
</td>
|
147 |
),
|
148 |
hr: ({ node, ...props }) => (
|
149 |
+
<hr className="my-1.5 border-zinc-200 dark:border-zinc-700 black:border-zinc-700" {...props} />
|
150 |
),
|
151 |
};
|
152 |
|
components/message.tsx
CHANGED
@@ -6,7 +6,7 @@ import { memo, useCallback, useEffect, useState } from "react";
|
|
6 |
import equal from "fast-deep-equal";
|
7 |
import { Markdown } from "./markdown";
|
8 |
import { cn } from "@/lib/utils";
|
9 |
-
import { ChevronDownIcon, ChevronUpIcon, LightbulbIcon } from "lucide-react";
|
10 |
import { SpinnerIcon } from "./icons";
|
11 |
import { ToolInvocation } from "./tool-invocation";
|
12 |
import { CopyButton } from "./copy-button";
|
@@ -37,51 +37,82 @@ export function ReasoningMessagePart({
|
|
37 |
}, [isReasoning, memoizedSetIsExpanded]);
|
38 |
|
39 |
return (
|
40 |
-
<div className="flex flex-col
|
41 |
{isReasoning ? (
|
42 |
-
<div className=
|
43 |
-
|
|
|
|
|
|
|
|
|
44 |
<SpinnerIcon />
|
45 |
</div>
|
46 |
-
<div className="text-
|
47 |
</div>
|
48 |
) : (
|
49 |
-
<
|
50 |
-
|
51 |
-
|
52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
53 |
</div>
|
54 |
-
<
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
onClick={() => setIsExpanded(!isExpanded)}
|
62 |
-
>
|
63 |
{isExpanded ? (
|
64 |
-
<ChevronDownIcon className="h-3
|
65 |
) : (
|
66 |
-
<ChevronUpIcon className="h-3
|
67 |
)}
|
68 |
-
</
|
69 |
-
</
|
70 |
)}
|
71 |
|
72 |
<AnimatePresence initial={false}>
|
73 |
{isExpanded && (
|
74 |
<motion.div
|
75 |
key="reasoning"
|
76 |
-
className=
|
|
|
|
|
|
|
|
|
77 |
initial={{ height: 0, opacity: 0 }}
|
78 |
animate={{ height: "auto", opacity: 1 }}
|
79 |
exit={{ height: 0, opacity: 0 }}
|
80 |
transition={{ duration: 0.2, ease: "easeInOut" }}
|
81 |
>
|
|
|
|
|
|
|
82 |
{part.details.map((detail, detailIndex) =>
|
83 |
detail.type === "text" ? (
|
84 |
-
<
|
|
|
|
|
85 |
) : (
|
86 |
"<redacted>"
|
87 |
),
|
|
|
6 |
import equal from "fast-deep-equal";
|
7 |
import { Markdown } from "./markdown";
|
8 |
import { cn } from "@/lib/utils";
|
9 |
+
import { ChevronDownIcon, ChevronUpIcon, LightbulbIcon, BrainIcon } from "lucide-react";
|
10 |
import { SpinnerIcon } from "./icons";
|
11 |
import { ToolInvocation } from "./tool-invocation";
|
12 |
import { CopyButton } from "./copy-button";
|
|
|
37 |
}, [isReasoning, memoizedSetIsExpanded]);
|
38 |
|
39 |
return (
|
40 |
+
<div className="flex flex-col mb-2 group">
|
41 |
{isReasoning ? (
|
42 |
+
<div className={cn(
|
43 |
+
"flex items-center gap-2.5 rounded-full py-1.5 px-3",
|
44 |
+
"bg-indigo-50/50 dark:bg-indigo-900/10 text-indigo-700 dark:text-indigo-300",
|
45 |
+
"border border-indigo-200/50 dark:border-indigo-700/20 w-fit"
|
46 |
+
)}>
|
47 |
+
<div className="animate-spin h-3.5 w-3.5">
|
48 |
<SpinnerIcon />
|
49 |
</div>
|
50 |
+
<div className="text-xs font-medium tracking-tight">Thinking...</div>
|
51 |
</div>
|
52 |
) : (
|
53 |
+
<button
|
54 |
+
onClick={() => setIsExpanded(!isExpanded)}
|
55 |
+
className={cn(
|
56 |
+
"flex items-center justify-between w-full",
|
57 |
+
"rounded-md py-2 px-3 mb-0.5",
|
58 |
+
"bg-muted/50 border border-border/60 hover:border-border/80",
|
59 |
+
"transition-all duration-150 cursor-pointer",
|
60 |
+
isExpanded ? "bg-muted border-primary/20" : ""
|
61 |
+
)}
|
62 |
+
>
|
63 |
+
<div className="flex items-center gap-2.5">
|
64 |
+
<div className={cn(
|
65 |
+
"flex items-center justify-center w-6 h-6 rounded-full",
|
66 |
+
"bg-amber-50 dark:bg-amber-900/20",
|
67 |
+
"text-amber-600 dark:text-amber-400 ring-1 ring-amber-200 dark:ring-amber-700/30",
|
68 |
+
)}>
|
69 |
+
<LightbulbIcon className="h-3.5 w-3.5" />
|
70 |
+
</div>
|
71 |
+
<div className="text-sm font-medium text-foreground flex items-center gap-1.5">
|
72 |
+
Reasoning
|
73 |
+
<span className="text-xs text-muted-foreground font-normal">
|
74 |
+
(click to {isExpanded ? "hide" : "view"})
|
75 |
+
</span>
|
76 |
+
</div>
|
77 |
</div>
|
78 |
+
<div className={cn(
|
79 |
+
"flex items-center justify-center",
|
80 |
+
"rounded-full p-0.5 w-5 h-5",
|
81 |
+
"text-muted-foreground hover:text-foreground",
|
82 |
+
"bg-background/80 border border-border/50",
|
83 |
+
"transition-colors",
|
84 |
+
)}>
|
|
|
|
|
85 |
{isExpanded ? (
|
86 |
+
<ChevronDownIcon className="h-3 w-3" />
|
87 |
) : (
|
88 |
+
<ChevronUpIcon className="h-3 w-3" />
|
89 |
)}
|
90 |
+
</div>
|
91 |
+
</button>
|
92 |
)}
|
93 |
|
94 |
<AnimatePresence initial={false}>
|
95 |
{isExpanded && (
|
96 |
<motion.div
|
97 |
key="reasoning"
|
98 |
+
className={cn(
|
99 |
+
"text-sm text-muted-foreground flex flex-col gap-2",
|
100 |
+
"pl-3.5 ml-0.5 mt-1",
|
101 |
+
"border-l border-amber-200/50 dark:border-amber-700/30"
|
102 |
+
)}
|
103 |
initial={{ height: 0, opacity: 0 }}
|
104 |
animate={{ height: "auto", opacity: 1 }}
|
105 |
exit={{ height: 0, opacity: 0 }}
|
106 |
transition={{ duration: 0.2, ease: "easeInOut" }}
|
107 |
>
|
108 |
+
<div className="text-xs text-muted-foreground/70 pl-1 font-medium">
|
109 |
+
The assistant's thought process:
|
110 |
+
</div>
|
111 |
{part.details.map((detail, detailIndex) =>
|
112 |
detail.type === "text" ? (
|
113 |
+
<div key={detailIndex} className="px-2 py-1.5 bg-muted/10 rounded-md border border-border/30">
|
114 |
+
<Markdown>{detail.text}</Markdown>
|
115 |
+
</div>
|
116 |
) : (
|
117 |
"<redacted>"
|
118 |
),
|
components/messages.tsx
CHANGED
@@ -11,11 +11,12 @@ export const Messages = ({
|
|
11 |
isLoading: boolean;
|
12 |
status: "error" | "submitted" | "streaming" | "ready";
|
13 |
}) => {
|
14 |
-
|
|
|
15 |
return (
|
16 |
<div
|
17 |
className="h-full overflow-y-auto no-scrollbar"
|
18 |
-
|
19 |
>
|
20 |
<div className="max-w-lg sm:max-w-3xl mx-auto py-4">
|
21 |
{messages.map((m, i) => (
|
@@ -27,7 +28,7 @@ export const Messages = ({
|
|
27 |
status={status}
|
28 |
/>
|
29 |
))}
|
30 |
-
|
31 |
</div>
|
32 |
</div>
|
33 |
);
|
|
|
11 |
isLoading: boolean;
|
12 |
status: "error" | "submitted" | "streaming" | "ready";
|
13 |
}) => {
|
14 |
+
const [containerRef, endRef] = useScrollToBottom();
|
15 |
+
|
16 |
return (
|
17 |
<div
|
18 |
className="h-full overflow-y-auto no-scrollbar"
|
19 |
+
ref={containerRef}
|
20 |
>
|
21 |
<div className="max-w-lg sm:max-w-3xl mx-auto py-4">
|
22 |
{messages.map((m, i) => (
|
|
|
28 |
status={status}
|
29 |
/>
|
30 |
))}
|
31 |
+
<div className="h-1" ref={endRef} />
|
32 |
</div>
|
33 |
</div>
|
34 |
);
|
components/model-picker.tsx
CHANGED
@@ -34,7 +34,7 @@ export const ModelPicker = ({ selectedModel, setSelectedModel }: ModelPickerProp
|
|
34 |
// Function to get the appropriate icon for each provider
|
35 |
const getProviderIcon = (provider: string) => {
|
36 |
switch (provider.toLowerCase()) {
|
37 |
-
case '
|
38 |
return <Sparkles className="h-3 w-3 text-yellow-500" />;
|
39 |
case 'openai':
|
40 |
return <Zap className="h-3 w-3 text-green-500" />;
|
@@ -122,25 +122,25 @@ export const ModelPicker = ({ selectedModel, setSelectedModel }: ModelPickerProp
|
|
122 |
defaultValue={validModelId}
|
123 |
>
|
124 |
<SelectTrigger
|
125 |
-
className="max-w-[
|
126 |
>
|
127 |
<SelectValue
|
128 |
placeholder="Select model"
|
129 |
-
className="text-xs font-medium flex items-center gap-1 sm:gap-2"
|
130 |
>
|
131 |
<div className="flex items-center gap-1 sm:gap-2">
|
132 |
{getProviderIcon(modelDetails[validModelId].provider)}
|
133 |
-
<
|
134 |
</div>
|
135 |
</SelectValue>
|
136 |
</SelectTrigger>
|
137 |
<SelectContent
|
138 |
align="start"
|
139 |
-
className="bg-background/95 dark:bg-muted/95 backdrop-blur-sm border-border/80 rounded-lg overflow-hidden p-0 w-[280px] sm:w-[350px] md:w-[
|
140 |
>
|
141 |
-
<div className="grid grid-cols-1 sm:grid-cols-[120px_1fr] md:grid-cols-[
|
142 |
{/* Model selector column */}
|
143 |
-
<div className="sm:border-r border-border/40 bg-muted/20 p-
|
144 |
<SelectGroup className="space-y-1">
|
145 |
{MODELS.map((id) => {
|
146 |
const modelId = id as modelID;
|
|
|
34 |
// Function to get the appropriate icon for each provider
|
35 |
const getProviderIcon = (provider: string) => {
|
36 |
switch (provider.toLowerCase()) {
|
37 |
+
case 'anthropic':
|
38 |
return <Sparkles className="h-3 w-3 text-yellow-500" />;
|
39 |
case 'openai':
|
40 |
return <Zap className="h-3 w-3 text-green-500" />;
|
|
|
122 |
defaultValue={validModelId}
|
123 |
>
|
124 |
<SelectTrigger
|
125 |
+
className="max-w-[200px] sm:max-w-fit sm:w-56 px-2 sm:px-3 h-8 sm:h-9 rounded-full group border-primary/20 bg-primary/5 hover:bg-primary/10 dark:bg-primary/10 dark:hover:bg-primary/20 transition-all duration-200 ring-offset-background focus:ring-2 focus:ring-primary/30 focus:ring-offset-2"
|
126 |
>
|
127 |
<SelectValue
|
128 |
placeholder="Select model"
|
129 |
+
className="text-xs font-medium flex items-center gap-1 sm:gap-2 text-primary dark:text-primary-foreground"
|
130 |
>
|
131 |
<div className="flex items-center gap-1 sm:gap-2">
|
132 |
{getProviderIcon(modelDetails[validModelId].provider)}
|
133 |
+
<span className="font-medium truncate">{modelDetails[validModelId].name}</span>
|
134 |
</div>
|
135 |
</SelectValue>
|
136 |
</SelectTrigger>
|
137 |
<SelectContent
|
138 |
align="start"
|
139 |
+
className="bg-background/95 dark:bg-muted/95 backdrop-blur-sm border-border/80 rounded-lg overflow-hidden p-0 w-[280px] sm:w-[350px] md:w-[515px]"
|
140 |
>
|
141 |
+
<div className="grid grid-cols-1 sm:grid-cols-[120px_1fr] md:grid-cols-[200px_1fr] items-start">
|
142 |
{/* Model selector column */}
|
143 |
+
<div className="sm:border-r border-border/40 bg-muted/20 p-0 pr-1">
|
144 |
<SelectGroup className="space-y-1">
|
145 |
{MODELS.map((id) => {
|
146 |
const modelId = id as modelID;
|
components/theme-toggle.tsx
CHANGED
@@ -8,7 +8,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge
|
|
8 |
import { cn } from "@/lib/utils"
|
9 |
|
10 |
export function ThemeToggle({ className, ...props }: React.ComponentProps<typeof Button>) {
|
11 |
-
const {
|
12 |
|
13 |
return (
|
14 |
<DropdownMenu>
|
@@ -28,7 +28,7 @@ export function ThemeToggle({ className, ...props }: React.ComponentProps<typeof
|
|
28 |
<DropdownMenuContent align="end">
|
29 |
<DropdownMenuItem onSelect={() => setTheme("dark")}>
|
30 |
<Flame className="mr-2 h-4 w-4" />
|
31 |
-
<span>
|
32 |
</DropdownMenuItem>
|
33 |
<DropdownMenuItem onSelect={() => setTheme("light")}>
|
34 |
<Sun className="mr-2 h-4 w-4" />
|
@@ -36,7 +36,12 @@ export function ThemeToggle({ className, ...props }: React.ComponentProps<typeof
|
|
36 |
</DropdownMenuItem>
|
37 |
<DropdownMenuItem onSelect={() => setTheme("black")}>
|
38 |
<CircleDashed className="mr-2 h-4 w-4" />
|
39 |
-
<span>
|
|
|
|
|
|
|
|
|
|
|
40 |
</DropdownMenuItem>
|
41 |
</DropdownMenuContent>
|
42 |
</DropdownMenu>
|
|
|
8 |
import { cn } from "@/lib/utils"
|
9 |
|
10 |
export function ThemeToggle({ className, ...props }: React.ComponentProps<typeof Button>) {
|
11 |
+
const { setTheme } = useTheme()
|
12 |
|
13 |
return (
|
14 |
<DropdownMenu>
|
|
|
28 |
<DropdownMenuContent align="end">
|
29 |
<DropdownMenuItem onSelect={() => setTheme("dark")}>
|
30 |
<Flame className="mr-2 h-4 w-4" />
|
31 |
+
<span>Dark</span>
|
32 |
</DropdownMenuItem>
|
33 |
<DropdownMenuItem onSelect={() => setTheme("light")}>
|
34 |
<Sun className="mr-2 h-4 w-4" />
|
|
|
36 |
</DropdownMenuItem>
|
37 |
<DropdownMenuItem onSelect={() => setTheme("black")}>
|
38 |
<CircleDashed className="mr-2 h-4 w-4" />
|
39 |
+
<span>Black</span>
|
40 |
+
</DropdownMenuItem>
|
41 |
+
{/* sunset theme */}
|
42 |
+
<DropdownMenuItem onSelect={() => setTheme("sunset")}>
|
43 |
+
<Sun className="mr-2 h-4 w-4" />
|
44 |
+
<span>Sunset</span>
|
45 |
</DropdownMenuItem>
|
46 |
</DropdownMenuContent>
|
47 |
</DropdownMenu>
|
components/tool-invocation.tsx
CHANGED
@@ -12,6 +12,7 @@ import {
|
|
12 |
ArrowRight,
|
13 |
Circle,
|
14 |
} from "lucide-react";
|
|
|
15 |
|
16 |
interface ToolInvocationProps {
|
17 |
toolName: string;
|
@@ -46,11 +47,21 @@ export function ToolInvocation({
|
|
46 |
const getStatusIcon = () => {
|
47 |
if (state === "call") {
|
48 |
if (isLatestMessage && status !== "ready") {
|
49 |
-
return <Loader2 className="animate-spin h-3.5 w-3.5 text-
|
50 |
}
|
51 |
-
return <Circle className="h-3.5 w-3.5 fill-
|
52 |
}
|
53 |
-
return <CheckCircle2 size={14} className="text-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
54 |
};
|
55 |
|
56 |
const formatContent = (content: any): string => {
|
@@ -70,26 +81,37 @@ export function ToolInvocation({
|
|
70 |
};
|
71 |
|
72 |
return (
|
73 |
-
<div className=
|
|
|
|
|
|
|
|
|
74 |
<div
|
75 |
-
className=
|
|
|
|
|
|
|
76 |
onClick={() => setIsExpanded(!isExpanded)}
|
77 |
>
|
78 |
-
<div className="flex items-center justify-center w-5 h-5">
|
79 |
-
<TerminalSquare className="h-3.5 w-3.5
|
80 |
</div>
|
81 |
<div className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground flex-1">
|
82 |
-
<span className="text-foreground
|
83 |
<ArrowRight className="h-3 w-3 text-muted-foreground/50" />
|
84 |
-
<span className="
|
|
|
|
|
85 |
</div>
|
86 |
-
<div className="flex items-center gap-2">
|
87 |
{getStatusIcon()}
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
|
|
|
|
93 |
</div>
|
94 |
</div>
|
95 |
|
@@ -101,27 +123,33 @@ export function ToolInvocation({
|
|
101 |
exit="collapsed"
|
102 |
variants={variants}
|
103 |
transition={{ duration: 0.2 }}
|
104 |
-
className="space-y-2
|
105 |
>
|
106 |
{!!args && (
|
107 |
-
<div className="space-y-1">
|
108 |
-
<div className="flex items-center gap-1.5 text-xs text-muted-foreground/70">
|
109 |
<Code className="h-3 w-3" />
|
110 |
-
<span>Arguments</span>
|
111 |
</div>
|
112 |
-
<pre className=
|
|
|
|
|
|
|
113 |
{formatContent(args)}
|
114 |
</pre>
|
115 |
</div>
|
116 |
)}
|
117 |
|
118 |
{!!result && (
|
119 |
-
<div className="space-y-1">
|
120 |
<div className="flex items-center gap-1.5 text-xs text-muted-foreground/70">
|
121 |
<ArrowRight className="h-3 w-3" />
|
122 |
-
<span>Result</span>
|
123 |
</div>
|
124 |
-
<pre className=
|
|
|
|
|
|
|
125 |
{formatContent(result)}
|
126 |
</pre>
|
127 |
</div>
|
|
|
12 |
ArrowRight,
|
13 |
Circle,
|
14 |
} from "lucide-react";
|
15 |
+
import { cn } from "@/lib/utils";
|
16 |
|
17 |
interface ToolInvocationProps {
|
18 |
toolName: string;
|
|
|
47 |
const getStatusIcon = () => {
|
48 |
if (state === "call") {
|
49 |
if (isLatestMessage && status !== "ready") {
|
50 |
+
return <Loader2 className="animate-spin h-3.5 w-3.5 text-primary/70" />;
|
51 |
}
|
52 |
+
return <Circle className="h-3.5 w-3.5 fill-muted-foreground/10 text-muted-foreground/70" />;
|
53 |
}
|
54 |
+
return <CheckCircle2 size={14} className="text-primary/90" />;
|
55 |
+
};
|
56 |
+
|
57 |
+
const getStatusClass = () => {
|
58 |
+
if (state === "call") {
|
59 |
+
if (isLatestMessage && status !== "ready") {
|
60 |
+
return "text-primary";
|
61 |
+
}
|
62 |
+
return "text-muted-foreground";
|
63 |
+
}
|
64 |
+
return "text-primary";
|
65 |
};
|
66 |
|
67 |
const formatContent = (content: any): string => {
|
|
|
81 |
};
|
82 |
|
83 |
return (
|
84 |
+
<div className={cn(
|
85 |
+
"flex flex-col mb-2 rounded-md border border-border/50 overflow-hidden",
|
86 |
+
"bg-gradient-to-b from-background to-muted/30 backdrop-blur-sm",
|
87 |
+
"transition-all duration-200 hover:border-border/80 group"
|
88 |
+
)}>
|
89 |
<div
|
90 |
+
className={cn(
|
91 |
+
"flex items-center gap-2.5 px-3 py-2 cursor-pointer transition-colors",
|
92 |
+
"hover:bg-muted/20"
|
93 |
+
)}
|
94 |
onClick={() => setIsExpanded(!isExpanded)}
|
95 |
>
|
96 |
+
<div className="flex items-center justify-center rounded-full w-5 h-5 bg-primary/5 text-primary">
|
97 |
+
<TerminalSquare className="h-3.5 w-3.5" />
|
98 |
</div>
|
99 |
<div className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground flex-1">
|
100 |
+
<span className="text-foreground font-semibold tracking-tight">{toolName}</span>
|
101 |
<ArrowRight className="h-3 w-3 text-muted-foreground/50" />
|
102 |
+
<span className={cn("font-medium", getStatusClass())}>
|
103 |
+
{state === "call" ? (isLatestMessage && status !== "ready" ? "Running" : "Waiting") : "Completed"}
|
104 |
+
</span>
|
105 |
</div>
|
106 |
+
<div className="flex items-center gap-2 opacity-70 group-hover:opacity-100 transition-opacity">
|
107 |
{getStatusIcon()}
|
108 |
+
<div className="bg-muted/30 rounded-full p-0.5 border border-border/30">
|
109 |
+
{isExpanded ? (
|
110 |
+
<ChevronUpIcon className="h-3 w-3 text-foreground/70" />
|
111 |
+
) : (
|
112 |
+
<ChevronDownIcon className="h-3 w-3 text-foreground/70" />
|
113 |
+
)}
|
114 |
+
</div>
|
115 |
</div>
|
116 |
</div>
|
117 |
|
|
|
123 |
exit="collapsed"
|
124 |
variants={variants}
|
125 |
transition={{ duration: 0.2 }}
|
126 |
+
className="space-y-2 px-3 pb-3"
|
127 |
>
|
128 |
{!!args && (
|
129 |
+
<div className="space-y-1.5">
|
130 |
+
<div className="flex items-center gap-1.5 text-xs text-muted-foreground/70 pt-1.5">
|
131 |
<Code className="h-3 w-3" />
|
132 |
+
<span className="font-medium">Arguments</span>
|
133 |
</div>
|
134 |
+
<pre className={cn(
|
135 |
+
"text-xs font-mono p-2.5 rounded-md overflow-x-auto",
|
136 |
+
"border border-border/40 bg-muted/10"
|
137 |
+
)}>
|
138 |
{formatContent(args)}
|
139 |
</pre>
|
140 |
</div>
|
141 |
)}
|
142 |
|
143 |
{!!result && (
|
144 |
+
<div className="space-y-1.5">
|
145 |
<div className="flex items-center gap-1.5 text-xs text-muted-foreground/70">
|
146 |
<ArrowRight className="h-3 w-3" />
|
147 |
+
<span className="font-medium">Result</span>
|
148 |
</div>
|
149 |
+
<pre className={cn(
|
150 |
+
"text-xs font-mono p-2.5 rounded-md overflow-x-auto max-h-[300px] overflow-y-auto",
|
151 |
+
"border border-border/40 bg-muted/10"
|
152 |
+
)}>
|
153 |
{formatContent(result)}
|
154 |
</pre>
|
155 |
</div>
|
instrumentation.ts
DELETED
@@ -1,13 +0,0 @@
|
|
1 |
-
import { registerOTel } from '@vercel/otel'
|
2 |
-
|
3 |
-
export function register() {
|
4 |
-
registerOTel({
|
5 |
-
serviceName: 'next-app',
|
6 |
-
instrumentations: [
|
7 |
-
{
|
8 |
-
name: 'ai-instrumentation',
|
9 |
-
include: [/^\/api\/ai/]
|
10 |
-
}
|
11 |
-
]
|
12 |
-
})
|
13 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lib/hooks/use-scroll-to-bottom.tsx
CHANGED
@@ -6,27 +6,73 @@ export function useScrollToBottom(): [
|
|
6 |
] {
|
7 |
const containerRef = useRef<HTMLDivElement>(null);
|
8 |
const endRef = useRef<HTMLDivElement>(null);
|
|
|
9 |
|
10 |
useEffect(() => {
|
11 |
const container = containerRef.current;
|
12 |
const end = endRef.current;
|
13 |
|
14 |
-
if (container
|
15 |
-
|
16 |
-
|
17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
18 |
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
24 |
});
|
25 |
|
26 |
-
|
27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
28 |
}, []);
|
29 |
|
30 |
-
|
31 |
-
return [containerRef, endRef];
|
32 |
}
|
|
|
6 |
] {
|
7 |
const containerRef = useRef<HTMLDivElement>(null);
|
8 |
const endRef = useRef<HTMLDivElement>(null);
|
9 |
+
const isUserScrollingRef = useRef(false);
|
10 |
|
11 |
useEffect(() => {
|
12 |
const container = containerRef.current;
|
13 |
const end = endRef.current;
|
14 |
|
15 |
+
if (!container || !end) return;
|
16 |
+
|
17 |
+
// Initial scroll to bottom
|
18 |
+
setTimeout(() => {
|
19 |
+
end.scrollIntoView({ behavior: 'instant', block: 'end' });
|
20 |
+
}, 100);
|
21 |
+
|
22 |
+
// Track if user has manually scrolled up
|
23 |
+
const handleScroll = () => {
|
24 |
+
if (!container) return;
|
25 |
+
|
26 |
+
const { scrollTop, scrollHeight, clientHeight } = container;
|
27 |
+
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
|
28 |
+
|
29 |
+
// If user is scrolled up, mark as manually scrolling
|
30 |
+
isUserScrollingRef.current = distanceFromBottom > 100;
|
31 |
+
};
|
32 |
+
|
33 |
+
// Handle mutations
|
34 |
+
const observer = new MutationObserver((mutations) => {
|
35 |
+
if (!container || !end) return;
|
36 |
|
37 |
+
// Check if mutation is related to expand/collapse
|
38 |
+
const isToggleSection = mutations.some(mutation => {
|
39 |
+
// Check if the target or parent is a motion-div (expanded content)
|
40 |
+
let target = mutation.target as HTMLElement;
|
41 |
+
let isExpand = false;
|
42 |
+
|
43 |
+
while (target && target !== container) {
|
44 |
+
if (target.classList?.contains('motion-div')) {
|
45 |
+
isExpand = true;
|
46 |
+
break;
|
47 |
+
}
|
48 |
+
target = target.parentElement as HTMLElement;
|
49 |
+
}
|
50 |
+
return isExpand;
|
51 |
});
|
52 |
|
53 |
+
// Don't scroll for expand/collapse actions
|
54 |
+
if (isToggleSection) return;
|
55 |
+
|
56 |
+
// Only auto-scroll if user hasn't manually scrolled up
|
57 |
+
if (!isUserScrollingRef.current) {
|
58 |
+
// For new messages, use smooth scrolling
|
59 |
+
end.scrollIntoView({ behavior: 'smooth', block: 'end' });
|
60 |
+
}
|
61 |
+
});
|
62 |
+
|
63 |
+
observer.observe(container, {
|
64 |
+
childList: true,
|
65 |
+
subtree: true,
|
66 |
+
});
|
67 |
+
|
68 |
+
// Add scroll event listener
|
69 |
+
container.addEventListener('scroll', handleScroll);
|
70 |
+
|
71 |
+
return () => {
|
72 |
+
observer.disconnect();
|
73 |
+
container.removeEventListener('scroll', handleScroll);
|
74 |
+
};
|
75 |
}, []);
|
76 |
|
77 |
+
return [containerRef, endRef] as [RefObject<HTMLDivElement>, RefObject<HTMLDivElement>];
|
|
|
78 |
}
|
package-lock.json
DELETED
The diff for this file is too large to render.
See raw diff
|
|
package.json
CHANGED
@@ -13,18 +13,13 @@
|
|
13 |
"db:studio": "drizzle-kit studio"
|
14 |
},
|
15 |
"dependencies": {
|
|
|
16 |
"@ai-sdk/cohere": "^1.2.9",
|
17 |
"@ai-sdk/google": "^1.2.12",
|
18 |
"@ai-sdk/groq": "^1.2.8",
|
19 |
"@ai-sdk/openai": "^1.3.16",
|
20 |
"@ai-sdk/react": "^1.2.9",
|
21 |
-
"@ai-sdk/xai": "^1.2.13",
|
22 |
-
"@auth/core": "^0.38.0",
|
23 |
-
"@auth/drizzle-adapter": "^1.8.0",
|
24 |
"@neondatabase/serverless": "^1.0.0",
|
25 |
-
"@opentelemetry/api-logs": "^0.200.0",
|
26 |
-
"@opentelemetry/instrumentation": "^0.200.0",
|
27 |
-
"@opentelemetry/sdk-logs": "^0.200.0",
|
28 |
"@radix-ui/react-accordion": "^1.2.7",
|
29 |
"@radix-ui/react-avatar": "^1.1.6",
|
30 |
"@radix-ui/react-dialog": "^1.1.10",
|
@@ -39,13 +34,11 @@
|
|
39 |
"@tanstack/react-query": "^5.74.4",
|
40 |
"@vercel/otel": "^1.11.0",
|
41 |
"ai": "^4.3.9",
|
42 |
-
"autoevals": "^0.0.127",
|
43 |
-
"braintrust": "^0.0.198",
|
44 |
"class-variance-authority": "^0.7.1",
|
45 |
"clsx": "^2.1.1",
|
46 |
-
"composio-core": "^0.5.31",
|
47 |
"drizzle-orm": "^0.42.0",
|
48 |
"fast-deep-equal": "^3.1.3",
|
|
|
49 |
"groq-sdk": "^0.19.0",
|
50 |
"lucide-react": "^0.488.0",
|
51 |
"motion": "^12.7.3",
|
|
|
13 |
"db:studio": "drizzle-kit studio"
|
14 |
},
|
15 |
"dependencies": {
|
16 |
+
"@ai-sdk/anthropic": "^1.2.10",
|
17 |
"@ai-sdk/cohere": "^1.2.9",
|
18 |
"@ai-sdk/google": "^1.2.12",
|
19 |
"@ai-sdk/groq": "^1.2.8",
|
20 |
"@ai-sdk/openai": "^1.3.16",
|
21 |
"@ai-sdk/react": "^1.2.9",
|
|
|
|
|
|
|
22 |
"@neondatabase/serverless": "^1.0.0",
|
|
|
|
|
|
|
23 |
"@radix-ui/react-accordion": "^1.2.7",
|
24 |
"@radix-ui/react-avatar": "^1.1.6",
|
25 |
"@radix-ui/react-dialog": "^1.1.10",
|
|
|
34 |
"@tanstack/react-query": "^5.74.4",
|
35 |
"@vercel/otel": "^1.11.0",
|
36 |
"ai": "^4.3.9",
|
|
|
|
|
37 |
"class-variance-authority": "^0.7.1",
|
38 |
"clsx": "^2.1.1",
|
|
|
39 |
"drizzle-orm": "^0.42.0",
|
40 |
"fast-deep-equal": "^3.1.3",
|
41 |
+
"framer-motion": "^12.7.4",
|
42 |
"groq-sdk": "^0.19.0",
|
43 |
"lucide-react": "^0.488.0",
|
44 |
"motion": "^12.7.3",
|
pnpm-lock.yaml
CHANGED
The diff for this file is too large to render.
See raw diff
|
|