From ab4990b2a4bef4d6c8f539f795eacd20a0f63b3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20B=C3=BCschel?= Date: Tue, 1 Apr 2025 17:33:14 +0800 Subject: [PATCH] feat: configure MCP servers in settings page --- app/(chat)/actions.ts | 117 ++++ app/(chat)/api/chat/route.ts | 198 ++++-- app/(chat)/settings/page.tsx | 540 ++++++++++++++++ components/message.tsx | 25 +- components/sidebar-user-nav.tsx | 10 +- components/ui/accordion.tsx | 58 ++ components/ui/alert.tsx | 59 ++ components/ui/badge.tsx | 36 ++ components/ui/dialog.tsx | 122 ++++ components/ui/switch.tsx | 29 + .../0006_overconfident_hannibal_king.sql | 14 + lib/db/migrations/meta/0006_snapshot.json | 578 ++++++++++++++++++ lib/db/migrations/meta/_journal.json | 7 + lib/db/queries.ts | 115 +++- lib/db/schema.ts | 14 + package.json | 8 +- pnpm-lock.yaml | 160 ++++- tailwind.config.ts | 158 +++-- 18 files changed, 2098 insertions(+), 150 deletions(-) create mode 100644 app/(chat)/settings/page.tsx create mode 100644 components/ui/accordion.tsx create mode 100644 components/ui/alert.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 components/ui/switch.tsx create mode 100644 lib/db/migrations/0006_overconfident_hannibal_king.sql create mode 100644 lib/db/migrations/meta/0006_snapshot.json diff --git a/app/(chat)/actions.ts b/app/(chat)/actions.ts index 8e5bd0226..ceac7e9a7 100644 --- a/app/(chat)/actions.ts +++ b/app/(chat)/actions.ts @@ -2,11 +2,18 @@ import { generateText, Message } from 'ai'; import { cookies } from 'next/headers'; +import { revalidatePath } from 'next/cache'; +import { auth } from '@/app/(auth)/auth'; import { deleteMessagesByChatIdAfterTimestamp, getMessageById, updateChatVisiblityById, + addMcpServer, + getMcpServersByUserId, + getMcpServerByIdAndUserId, + updateMcpServerStatus, + deleteMcpServer, } from '@/lib/db/queries'; import { VisibilityType } from '@/components/visibility-selector'; import { myProvider } from '@/lib/ai/providers'; @@ -52,3 +59,113 @@ export async function updateChatVisibility({ }) { await updateChatVisiblityById({ chatId, visibility }); } + +// --- MCP Server Actions --- + +export async function fetchMcpServers() { + const session = await auth(); + if (!session?.user?.id) { + throw new Error('Unauthorized: User not logged in.'); + } + try { + const servers = await getMcpServersByUserId({ userId: session.user.id }); + return servers; + } catch (error) { + console.error('Error fetching MCP servers:', error); + throw new Error('Failed to fetch MCP servers.'); + } +} + +export async function addMcpServerAction({ + name, + config, +}: { + name: string; + config: Record; +}) { + const session = await auth(); + if (!session?.user?.id) { + throw new Error('Unauthorized: User not logged in.'); + } + + if (!name || !config) { + throw new Error('Missing required fields: name and config.'); + } + + try { + const newServer = await addMcpServer({ + userId: session.user.id, + name, + config, + }); + revalidatePath('/settings'); // Revalidate the settings page + return { success: true, server: newServer }; + } catch (error) { + console.error('Error adding MCP server:', error); + return { success: false, error: 'Failed to add MCP server.' }; + } +} + +export async function toggleMcpServerAction({ + id, + isEnabled, +}: { + id: string; + isEnabled: boolean; +}) { + const session = await auth(); + if (!session?.user?.id) { + throw new Error('Unauthorized: User not logged in.'); + } + + try { + // Verify ownership before updating + const existingServer = await getMcpServerByIdAndUserId({ + id, + userId: session.user.id, + }); + if (!existingServer) { + throw new Error('Unauthorized: Server not found or not owned by user.'); + } + + const updatedServer = await updateMcpServerStatus({ id, isEnabled }); + revalidatePath('/settings'); // Revalidate the settings page + return { success: true, server: updatedServer }; + } catch (error) { + console.error('Error toggling MCP server status:', error); + // Distinguish between auth errors and DB errors if needed + const errorMessage = error instanceof Error && error.message.startsWith('Unauthorized') + ? error.message + : 'Failed to update MCP server status.'; + return { success: false, error: errorMessage }; + + } +} + +export async function deleteMcpServerAction({ id }: { id: string }) { + const session = await auth(); + if (!session?.user?.id) { + throw new Error('Unauthorized: User not logged in.'); + } + + try { + // Verify ownership before deleting + const existingServer = await getMcpServerByIdAndUserId({ + id, + userId: session.user.id, + }); + if (!existingServer) { + throw new Error('Unauthorized: Server not found or not owned by user.'); + } + + await deleteMcpServer({ id }); + revalidatePath('/settings'); // Revalidate the settings page + return { success: true }; + } catch (error) { + console.error('Error deleting MCP server:', error); + const errorMessage = error instanceof Error && error.message.startsWith('Unauthorized') + ? error.message + : 'Failed to delete MCP server.'; + return { success: false, error: errorMessage }; + } +} diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index e574d7dc5..2cd09d31f 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -4,7 +4,9 @@ import { createDataStreamResponse, smoothStream, streamText, + experimental_createMCPClient, } from 'ai'; +import { Experimental_StdioMCPTransport } from 'ai/mcp-stdio'; import { auth } from '@/app/(auth)/auth'; import { systemPrompt } from '@/lib/ai/prompts'; import { @@ -12,6 +14,7 @@ import { getChatById, saveChat, saveMessages, + getEnabledMcpServersByUserId, } from '@/lib/db/queries'; import { generateUUID, @@ -29,6 +32,8 @@ import { myProvider } from '@/lib/ai/providers'; export const maxDuration = 60; export async function POST(request: Request) { + let mcpClientsToClose: Awaited>[] = []; + try { const { id, @@ -45,6 +50,7 @@ export async function POST(request: Request) { if (!session || !session.user || !session.user.id) { return new Response('Unauthorized', { status: 401 }); } + const userId = session.user.id; const userMessage = getMostRecentUserMessage(messages); @@ -59,9 +65,9 @@ export async function POST(request: Request) { message: userMessage, }); - await saveChat({ id, userId: session.user.id, title }); + await saveChat({ id, userId: userId, title }); } else { - if (chat.userId !== session.user.id) { + if (chat.userId !== userId) { return new Response('Unauthorized', { status: 401 }); } } @@ -80,24 +86,9 @@ export async function POST(request: Request) { }); return createDataStreamResponse({ - execute: (dataStream) => { - const result = streamText({ - model: myProvider.languageModel(selectedChatModel), - system: systemPrompt({ selectedChatModel }), - messages, - maxSteps: 5, - experimental_activeTools: - selectedChatModel === 'chat-model-reasoning' - ? [] - : [ - 'getWeather', - 'createDocument', - 'updateDocument', - 'requestSuggestions', - ], - experimental_transform: smoothStream({ chunking: 'word' }), - experimental_generateMessageId: generateUUID, - tools: { + execute: async (dataStream) => { + try { + const staticTools = { getWeather, createDocument: createDocument({ session, dataStream }), updateDocument: updateDocument({ session, dataStream }), @@ -105,62 +96,136 @@ export async function POST(request: Request) { session, dataStream, }), - }, - onFinish: async ({ response }) => { - if (session.user?.id) { + }; + let combinedTools: Record = { ...staticTools }; + + try { + const enabledServers = await getEnabledMcpServersByUserId({ userId }); + + for (const server of enabledServers) { try { - const assistantId = getTrailingMessageId({ - messages: response.messages.filter( - (message) => message.role === 'assistant', - ), - }); - - if (!assistantId) { - throw new Error('No assistant message found!'); + let transport; + const config = server.config as any; + + if (config.transportType === 'sse') { + transport = { + type: 'sse' as const, + url: config.url, + }; + } else if (config.transportType === 'stdio') { + if (isProductionEnvironment) { + console.warn(`SECURITY WARNING: Initializing MCP client with stdio transport in production for server: ${server.name} (ID: ${server.id})`); + } + transport = new Experimental_StdioMCPTransport({ + command: config.command, + args: config.args || [], + }); + } else { + console.warn(`Unsupported MCP transport type '${config.transportType}' for server ${server.name}`); + continue; } - const [, assistantMessage] = appendResponseMessages({ - messages: [userMessage], - responseMessages: response.messages, - }); - - await saveMessages({ - messages: [ - { - id: assistantId, - chatId: id, - role: assistantMessage.role, - parts: assistantMessage.parts, - attachments: - assistantMessage.experimental_attachments ?? [], - createdAt: new Date(), - }, - ], - }); - } catch (_) { - console.error('Failed to save chat'); + const mcpClient = await experimental_createMCPClient({ transport }); + mcpClientsToClose.push(mcpClient); + + const mcpTools = await mcpClient.tools(); + combinedTools = { ...combinedTools, ...mcpTools }; + console.log(`Loaded ${Object.keys(mcpTools).length} tools from MCP server: ${server.name}`); + + } catch (mcpError) { + console.error(`Failed to initialize or get tools from MCP server ${server.name} (ID: ${server.id}):`, mcpError); } } - }, - experimental_telemetry: { - isEnabled: isProductionEnvironment, - functionId: 'stream-text', - }, - }); - - result.consumeStream(); - - result.mergeIntoDataStream(dataStream, { - sendReasoning: true, - }); + } catch (dbError) { + console.error('Failed to fetch enabled MCP servers:', dbError); + } + + const activeToolsList = selectedChatModel === 'chat-model-reasoning' + ? [] + : Object.keys(combinedTools); + + const result = streamText({ + model: myProvider.languageModel(selectedChatModel), + system: systemPrompt({ selectedChatModel }), + messages, + maxSteps: 5, + tools: combinedTools, + experimental_activeTools: activeToolsList, + experimental_transform: smoothStream({ chunking: 'word' }), + experimental_generateMessageId: generateUUID, + onFinish: async ({ response }) => { + if (session.user?.id) { + try { + const assistantId = getTrailingMessageId({ + messages: response.messages.filter( + (message) => message.role === 'assistant', + ), + }); + + if (!assistantId) { + throw new Error('No assistant message found!'); + } + + const [, assistantMessage] = appendResponseMessages({ + messages: [userMessage], + responseMessages: response.messages, + }); + + await saveMessages({ + messages: [ + { + id: assistantId, + chatId: id, + role: assistantMessage.role, + parts: assistantMessage.parts, + attachments: + assistantMessage.experimental_attachments ?? [], + createdAt: new Date(), + }, + ], + }); + } catch (_) { + console.error('Failed to save chat messages after stream completion'); + } + } + console.log(`Closing ${mcpClientsToClose.length} MCP clients in onFinish...`); + for (const client of mcpClientsToClose) { + try { + await client.close(); + } catch (closeError: unknown) { + console.error('Error closing MCP client in onFinish:', closeError); + } + } + mcpClientsToClose = []; + }, + experimental_telemetry: { + isEnabled: isProductionEnvironment, + functionId: 'stream-text', + }, + }); + + result.consumeStream(); + result.mergeIntoDataStream(dataStream, { sendReasoning: true }); + + } catch(streamError) { + console.error('Error during streamText execution or MCP setup:', streamError); + throw streamError; + } finally { + console.log('Stream execute try/catch finished.'); + } }, - onError: () => { - return 'Oops, an error occured!'; + onError: (error) => { + console.error('Data stream error:', error); + return 'Oops, an error occured!'; }, }); } catch (error) { + console.error('Error in POST /api/chat route (initial setup):', error); + for (const client of mcpClientsToClose) { + client.close().catch((closeError: unknown) => console.error('Error closing MCP client during outer catch:', closeError)); + } return new Response('An error occurred while processing your request!', { - status: 404, + status: 500, }); } } @@ -190,6 +255,7 @@ export async function DELETE(request: Request) { return new Response('Chat deleted', { status: 200 }); } catch (error) { + console.error('Error deleting chat:', error); return new Response('An error occurred while processing your request!', { status: 500, }); diff --git a/app/(chat)/settings/page.tsx b/app/(chat)/settings/page.tsx new file mode 100644 index 000000000..6444c076d --- /dev/null +++ b/app/(chat)/settings/page.tsx @@ -0,0 +1,540 @@ +"use client"; + +import { useState, useEffect, useTransition } from 'react'; +import { + fetchMcpServers, + addMcpServerAction, + toggleMcpServerAction, + deleteMcpServerAction, +} from '@/app/(chat)/actions'; +import type { McpServer } from '@/lib/db/schema'; // Assuming schema path +import { experimental_createMCPClient } from 'ai'; + +import { Button } from "@/components/ui/button" +import { Switch } from "@/components/ui/switch" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, + DialogClose +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" +import { Terminal, PlugZap, RefreshCw, Wrench } from "lucide-react" +import { Skeleton } from "@/components/ui/skeleton" +import { Badge } from "@/components/ui/badge" +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion" + +// Define type for tool schema +type ToolSchema = { + name: string; + description: string; + parameters?: Record; +}; + +// Define transport types from MCP client +type SSEConfig = { + type: 'sse'; + url: string; +}; + +type StdioMCPTransport = { + type: 'stdio'; + command: string; + args?: string[]; +}; + +type MCPTransport = SSEConfig | StdioMCPTransport; + +export default function Page() { + const [servers, setServers] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); + const [isPending, startTransition] = useTransition(); + const [serverSchemas, setServerSchemas] = useState>({}); + const [schemaLoading, setSchemaLoading] = useState>({}); + + // Form state for adding a server + const [newName, setNewName] = useState(''); + const [newTransportType, setNewTransportType] = useState<'sse' | 'stdio'>('sse'); + const [newUrl, setNewUrl] = useState(''); + const [newCommand, setNewCommand] = useState(''); + const [newArgs, setNewArgs] = useState(''); + // Add state for headers if needed + + useEffect(() => { + const loadServers = async () => { + setIsLoading(true); + setError(null); + try { + const fetchedServers = await fetchMcpServers(); + setServers(fetchedServers); + + // Initialize schema loading state for all servers + const initialSchemaLoading: Record = {}; + fetchedServers.forEach((server) => { + initialSchemaLoading[server.id] = false; + }); + setSchemaLoading(initialSchemaLoading); + + // Fetch schemas for all enabled servers + fetchedServers + .filter((server) => server.isEnabled) + .forEach((server) => { + fetchServerSchema(server); + }); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load servers'); + } finally { + setIsLoading(false); + } + }; + loadServers(); + }, []); + + const fetchServerSchema = async (server: McpServer) => { + setSchemaLoading((prev) => ({ ...prev, [server.id]: true })); + setError(null); // Clear previous errors specific to this server + try { + // Create MCP client based on server config + const config = server.config as any; + let clientOptions: { transport: any; name: string }; + + if (!config || typeof config !== 'object') { + throw new Error("Server configuration is missing or invalid."); + } + + if (config.transportType === 'sse') { + if (!config.url || typeof config.url !== 'string') { + console.error("Invalid SSE config for server", server.id, config); + throw new Error("Invalid SSE configuration: URL is missing or not a string."); + } + clientOptions = { + transport: { + type: 'sse', + url: config.url + }, + name: server.name + }; + } else if (config.transportType === 'stdio') { + if (!config.command || typeof config.command !== 'string') { + console.error("Invalid stdio config for server", server.id, config); + throw new Error("Invalid stdio configuration: Command is missing or not a string."); + } + // Ensure args is an array of strings, defaulting to empty array + const args = config.args && Array.isArray(config.args) + ? config.args.filter((arg: any): arg is string => typeof arg === 'string') + : []; + + clientOptions = { + transport: { + type: 'stdio', + command: config.command, + args: args + }, + name: server.name + }; + } else { + console.error("Unsupported transport type for server", server.id, config.transportType); + throw new Error(`Unsupported transport type: ${config.transportType}`); + } + + console.log('Attempting to create MCP Client with options:', JSON.stringify(clientOptions, null, 2)); + + const client = await experimental_createMCPClient(clientOptions); + + try { + // Get tools from the MCP server + const toolSet = await client.tools(); + + // Extract tool schemas + const schemas: ToolSchema[] = Object.entries(toolSet).map(([name, tool]: [string, any]) => ({ + name, + description: tool.description || 'No description available', + parameters: tool.parameters || {} + })); + + setServerSchemas((prev) => ({ ...prev, [server.id]: schemas })); + } finally { + // Always close the client + await client.close(); + } + } catch (err) { + console.error(`Failed to fetch schema for ${server.name} (ID: ${server.id}):`, err); + // Set specific error for this server card + setError(`Failed to refresh schema for ${server.name}: ${err instanceof Error ? err.message : String(err)}`); + setServerSchemas((prev) => ({ ...prev, [server.id]: [] })); // Clear schema on error + } finally { + setSchemaLoading((prev) => ({ ...prev, [server.id]: false })); + } + }; + + const handleRefreshSchema = (server: McpServer) => { + if (!server.isEnabled) return; + fetchServerSchema(server); + }; + + const handleAddServer = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); // Clear previous errors + + let config: Record = { transportType: newTransportType }; + if (newTransportType === 'sse') { + if (!newUrl) { + setError("URL is required for SSE transport."); + return; + } + config.url = newUrl; + // Add headers to config if implemented + } else { // stdio + if (!newCommand) { + setError("Command is required for stdio transport."); + return; + } + config.command = newCommand; + config.args = newArgs.split(' ').filter(Boolean); // Basic space splitting for args + } + + startTransition(async () => { + try { + const result = await addMcpServerAction({ name: newName, config }); + if (result?.success && result.server) { + const newServer = result.server as McpServer; + setServers((prev) => [...prev, newServer]); + + // Initialize schema loading state for the new server + setSchemaLoading((prev) => ({ ...prev, [newServer.id]: false })); + + // Fetch schema if the server is enabled + if (newServer.isEnabled) { + fetchServerSchema(newServer); + } + + setIsAddDialogOpen(false); + // Reset form + setNewName(''); + setNewTransportType('sse'); + setNewUrl(''); + setNewCommand(''); + setNewArgs(''); + } else { + setError(result?.error || 'Failed to add server'); + } + } catch (err) { + setError(err instanceof Error ? err.message : 'An unexpected error occurred'); + } + }); + }; + + const handleToggleServer = (id: string, currentStatus: boolean) => { + startTransition(async () => { + setError(null); + try { + const result = await toggleMcpServerAction({ id, isEnabled: !currentStatus }); + if (result?.success && result.server) { + setServers((prev) => + prev.map((s) => (s.id === id ? { ...s, isEnabled: result.server.isEnabled } : s)) + ); + + // Fetch schema if server was enabled + if (result.server.isEnabled) { + const server = servers.find(s => s.id === id); + if (server) { + fetchServerSchema({...server, isEnabled: true}); + } + } + } else { + setError(result?.error || 'Failed to toggle server status'); + } + } catch (err) { + setError(err instanceof Error ? err.message : 'An unexpected error occurred'); + } + }); + }; + + const handleDeleteServer = (id: string) => { + if (!confirm('Are you sure you want to delete this MCP server?')) return; + startTransition(async () => { + setError(null); + try { + const result = await deleteMcpServerAction({ id }); + if (result?.success) { + setServers((prev) => prev.filter((s) => s.id !== id)); + // Remove schema for the deleted server + setServerSchemas((prev) => { + const newSchemas = {...prev}; + delete newSchemas[id]; + return newSchemas; + }); + setSchemaLoading((prev) => { + const newLoading = {...prev}; + delete newLoading[id]; + return newLoading; + }); + } else { + setError(result?.error || 'Failed to delete server'); + } + } catch (err) { + setError(err instanceof Error ? err.message : 'An unexpected error occurred'); + } + }); + }; + + return ( +
+
+

MCP Servers

+ + + + + +
+ + {error &&

Error: {error}

} + + {isLoading ? ( +
+ {[1, 2].map((i) => ( + + + + + + + + + +
+ +
+ +
+
+ ))} +
+ ) : servers.length === 0 ? ( +
+ +

No MCP Servers Configured

+

+ Add your first MCP server to connect external tools. +

+
+ ) : ( +
+ {servers.map((server) => ( + + +
+ {server.name} + {server.isEnabled && ( + + )} +
+
+ +

ID: {server.id}

+

Type: {(server.config as any)?.transportType || 'N/A'}

+ {(server.config as any)?.transportType === 'sse' &&

URL: {(server.config as any)?.url}

} + {(server.config as any)?.transportType === 'stdio' &&

Command: {(server.config as any)?.command} {(server.config as any)?.args?.join(' ')}

} + + {/* Tool Schema Section */} + {server.isEnabled && ( +
+
+ +

Available Tools

+
+ + {schemaLoading[server.id] ? ( +
+ + +
+ ) : serverSchemas[server.id]?.length ? ( + + {serverSchemas[server.id].map((tool) => ( + + +
+ {tool.name} +
+
+ +
+

{tool.description}

+ {Object.keys(tool.parameters || {}).length > 0 && ( +
+ Parameters: +
+                                       {JSON.stringify(tool.parameters, null, 2)}
+                                     
+
+ )} +
+
+
+ ))} +
+ ) : ( +

+ {server.isEnabled + ? "No tools available or unable to connect to server" + : "Enable the server to view available tools"} +

+ )} +
+ )} +
+ +
+ + handleToggleServer(server.id, server.isEnabled)} + disabled={isPending} + /> +
+ +
+
+ ))} +
+ )} + + + + + Add New MCP Server + +
+
+
+ + setNewName(e.target.value)} + placeholder="My Custom Tools" + required + disabled={isPending} + /> +
+ +
+ + +
+ + {newTransportType === 'sse' && ( +
+ + setNewUrl(e.target.value)} + placeholder="https://my-mcp-server.com/sse" + required + disabled={isPending} + /> +
+ )} + + {newTransportType === 'stdio' && ( + <> + + + Security Warning + + Running local commands (stdio) from a web server can be insecure. + Ensure the command is safe and consider sandboxing if possible. + + +
+ + setNewCommand(e.target.value)} + placeholder="node" + required + disabled={isPending} + /> +
+
+ + setNewArgs(e.target.value)} + placeholder="dist/my-mcp-server.js (space-separated)" + disabled={isPending} + /> +
+ + )} +
+ + + + + + +
+
+
+
+ ); +} diff --git a/components/message.tsx b/components/message.tsx index 6b49e8918..a8176e123 100644 --- a/components/message.tsx +++ b/components/message.tsx @@ -19,6 +19,7 @@ import { MessageEditor } from './message-editor'; import { DocumentPreview } from './document-preview'; import { MessageReasoning } from './message-reasoning'; import { UseChatHelpers } from '@ai-sdk/react'; +import { BrainCircuit, FlaskRound, Loader2 } from 'lucide-react'; const PurePreviewMessage = ({ chatId, @@ -150,6 +151,9 @@ const PurePreviewMessage = ({ const { toolInvocation } = part; const { toolName, toolCallId, state } = toolInvocation; + // Check if it's a MCP tool (could be sequentialthinking or other MCP tools) + const isMcpTool = toolName === 'sequentialthinking' || toolName.startsWith('mcp_'); + if (state === 'call') { const { args } = toolInvocation; @@ -157,11 +161,23 @@ const PurePreviewMessage = ({
{toolName === 'getWeather' ? ( + ) : isMcpTool ? ( + // Nice loading state for MCP tools +
+
+ + + Executing {toolName === 'sequentialthinking' ? 'Sequential Thinking' : toolName.replace('mcp_', '').replace(/_/g, ' ')} + + +
+

Using MCP to process your query...

+
) : toolName === 'createDocument' ? ( ) : toolName === 'updateDocument' ? ( @@ -205,6 +221,13 @@ const PurePreviewMessage = ({ result={result} isReadonly={isReadonly} /> + ) : isMcpTool ? ( + // MCP tool results are meant for the LLM, not for direct user display + // Either hide them completely or show a subtle indication +
+ + Processing completed +
) : (
{JSON.stringify(result, null, 2)}
)} diff --git a/components/sidebar-user-nav.tsx b/components/sidebar-user-nav.tsx index cb41ec76c..3dad8e95a 100644 --- a/components/sidebar-user-nav.tsx +++ b/components/sidebar-user-nav.tsx @@ -1,4 +1,5 @@ 'use client'; + import { ChevronUp } from 'lucide-react'; import Image from 'next/image'; import type { User } from 'next-auth'; @@ -17,9 +18,10 @@ import { SidebarMenuButton, SidebarMenuItem, } from '@/components/ui/sidebar'; - +import { useRouter } from 'next/navigation'; export function SidebarUserNav({ user }: { user: User }) { const { setTheme, theme } = useTheme(); + const router = useRouter(); return ( @@ -42,6 +44,12 @@ export function SidebarUserNav({ user }: { user: User }) { side="top" className="w-[--radix-popper-anchor-width]" > + router.push('/settings')} + > + Settings + setTheme(theme === 'dark' ? 'light' : 'dark')} diff --git a/components/ui/accordion.tsx b/components/ui/accordion.tsx new file mode 100644 index 000000000..24c788c2c --- /dev/null +++ b/components/ui/accordion.tsx @@ -0,0 +1,58 @@ +"use client" + +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)) + +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/components/ui/alert.tsx b/components/ui/alert.tsx new file mode 100644 index 000000000..41fa7e056 --- /dev/null +++ b/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx new file mode 100644 index 000000000..f000e3ef5 --- /dev/null +++ b/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx new file mode 100644 index 000000000..f38593bdc --- /dev/null +++ b/components/ui/dialog.tsx @@ -0,0 +1,122 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/components/ui/switch.tsx b/components/ui/switch.tsx new file mode 100644 index 000000000..bc69cf2db --- /dev/null +++ b/components/ui/switch.tsx @@ -0,0 +1,29 @@ +"use client" + +import * as React from "react" +import * as SwitchPrimitives from "@radix-ui/react-switch" + +import { cn } from "@/lib/utils" + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +Switch.displayName = SwitchPrimitives.Root.displayName + +export { Switch } diff --git a/lib/db/migrations/0006_overconfident_hannibal_king.sql b/lib/db/migrations/0006_overconfident_hannibal_king.sql new file mode 100644 index 000000000..d3e583485 --- /dev/null +++ b/lib/db/migrations/0006_overconfident_hannibal_king.sql @@ -0,0 +1,14 @@ +CREATE TABLE IF NOT EXISTS "McpServer" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "userId" uuid NOT NULL, + "name" text NOT NULL, + "config" json NOT NULL, + "isEnabled" boolean DEFAULT false NOT NULL, + "createdAt" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "McpServer" ADD CONSTRAINT "McpServer_userId_User_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/lib/db/migrations/meta/0006_snapshot.json b/lib/db/migrations/meta/0006_snapshot.json new file mode 100644 index 000000000..c0ef28695 --- /dev/null +++ b/lib/db/migrations/meta/0006_snapshot.json @@ -0,0 +1,578 @@ +{ + "id": "802072aa-f852-4c84-95b7-a19dbf9af7e2", + "prevId": "c6c102e6-b64e-4f0c-a7a6-32df758de437", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.Chat": { + "name": "Chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'private'" + } + }, + "indexes": {}, + "foreignKeys": { + "Chat_userId_User_id_fk": { + "name": "Chat_userId_User_id_fk", + "tableFrom": "Chat", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.Document": { + "name": "Document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "text": { + "name": "text", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "Document_userId_User_id_fk": { + "name": "Document_userId_User_id_fk", + "tableFrom": "Document", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "Document_id_createdAt_pk": { + "name": "Document_id_createdAt_pk", + "columns": [ + "id", + "createdAt" + ] + } + }, + "uniqueConstraints": {} + }, + "public.McpServer": { + "name": "McpServer", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "isEnabled": { + "name": "isEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "McpServer_userId_User_id_fk": { + "name": "McpServer_userId_User_id_fk", + "tableFrom": "McpServer", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.Message_v2": { + "name": "Message_v2", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chatId": { + "name": "chatId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "parts": { + "name": "parts", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "attachments": { + "name": "attachments", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "Message_v2_chatId_Chat_id_fk": { + "name": "Message_v2_chatId_Chat_id_fk", + "tableFrom": "Message_v2", + "tableTo": "Chat", + "columnsFrom": [ + "chatId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.Message": { + "name": "Message", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chatId": { + "name": "chatId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "Message_chatId_Chat_id_fk": { + "name": "Message_chatId_Chat_id_fk", + "tableFrom": "Message", + "tableTo": "Chat", + "columnsFrom": [ + "chatId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.Suggestion": { + "name": "Suggestion", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "documentId": { + "name": "documentId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "documentCreatedAt": { + "name": "documentCreatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "originalText": { + "name": "originalText", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "suggestedText": { + "name": "suggestedText", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isResolved": { + "name": "isResolved", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "Suggestion_userId_User_id_fk": { + "name": "Suggestion_userId_User_id_fk", + "tableFrom": "Suggestion", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "Suggestion_documentId_documentCreatedAt_Document_id_createdAt_fk": { + "name": "Suggestion_documentId_documentCreatedAt_Document_id_createdAt_fk", + "tableFrom": "Suggestion", + "tableTo": "Document", + "columnsFrom": [ + "documentId", + "documentCreatedAt" + ], + "columnsTo": [ + "id", + "createdAt" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "Suggestion_id_pk": { + "name": "Suggestion_id_pk", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {} + }, + "public.User": { + "name": "User", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "email": { + "name": "email", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.Vote_v2": { + "name": "Vote_v2", + "schema": "", + "columns": { + "chatId": { + "name": "chatId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "messageId": { + "name": "messageId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "isUpvoted": { + "name": "isUpvoted", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "Vote_v2_chatId_Chat_id_fk": { + "name": "Vote_v2_chatId_Chat_id_fk", + "tableFrom": "Vote_v2", + "tableTo": "Chat", + "columnsFrom": [ + "chatId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "Vote_v2_messageId_Message_v2_id_fk": { + "name": "Vote_v2_messageId_Message_v2_id_fk", + "tableFrom": "Vote_v2", + "tableTo": "Message_v2", + "columnsFrom": [ + "messageId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "Vote_v2_chatId_messageId_pk": { + "name": "Vote_v2_chatId_messageId_pk", + "columns": [ + "chatId", + "messageId" + ] + } + }, + "uniqueConstraints": {} + }, + "public.Vote": { + "name": "Vote", + "schema": "", + "columns": { + "chatId": { + "name": "chatId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "messageId": { + "name": "messageId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "isUpvoted": { + "name": "isUpvoted", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "Vote_chatId_Chat_id_fk": { + "name": "Vote_chatId_Chat_id_fk", + "tableFrom": "Vote", + "tableTo": "Chat", + "columnsFrom": [ + "chatId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "Vote_messageId_Message_id_fk": { + "name": "Vote_messageId_Message_id_fk", + "tableFrom": "Vote", + "tableTo": "Message", + "columnsFrom": [ + "messageId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "Vote_chatId_messageId_pk": { + "name": "Vote_chatId_messageId_pk", + "columns": [ + "chatId", + "messageId" + ] + } + }, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/lib/db/migrations/meta/_journal.json b/lib/db/migrations/meta/_journal.json index b23886585..ff95a917b 100644 --- a/lib/db/migrations/meta/_journal.json +++ b/lib/db/migrations/meta/_journal.json @@ -43,6 +43,13 @@ "when": 1741934630596, "tag": "0005_wooden_whistler", "breakpoints": true + }, + { + "idx": 6, + "version": "7", + "when": 1743497514958, + "tag": "0006_overconfident_hannibal_king", + "breakpoints": true } ] } \ No newline at end of file diff --git a/lib/db/queries.ts b/lib/db/queries.ts index ebc876aae..6afaec38e 100644 --- a/lib/db/queries.ts +++ b/lib/db/queries.ts @@ -15,6 +15,8 @@ import { message, vote, type DBMessage, + mcpServer, + type McpServer, } from './schema'; import { ArtifactKind } from '@/components/artifact'; @@ -343,9 +345,118 @@ export async function updateChatVisiblityById({ visibility: 'private' | 'public'; }) { try { - return await db.update(chat).set({ visibility }).where(eq(chat.id, chatId)); + return await db + .update(chat) + .set({ visibility }) + .where(eq(chat.id, chatId)); + } catch (error) { + console.error('Failed to update chat visibility by id in database'); + throw error; + } +} + +// --- MCP Server Queries --- + +export async function addMcpServer({ + userId, + name, + config, +}: { + userId: string; + name: string; + config: Record; // Assuming config is a JSON object +}) { + try { + const [newServer] = await db + .insert(mcpServer) + .values({ + userId, + name, + config, + isEnabled: true, // Default to enabled when added + createdAt: new Date(), + }) + .returning(); + return newServer; + } catch (error) { + console.error('Failed to add MCP server to database', error); + throw error; + } +} + +export async function getMcpServersByUserId({ userId }: { userId: string }) { + try { + return await db + .select() + .from(mcpServer) + .where(eq(mcpServer.userId, userId)) + .orderBy(asc(mcpServer.createdAt)); + } catch (error) { + console.error('Failed to get MCP servers by user ID from database', error); + throw error; + } +} + +export async function getEnabledMcpServersByUserId({ + userId, +}: { userId: string }) { + try { + return await db + .select() + .from(mcpServer) + .where(and(eq(mcpServer.userId, userId), eq(mcpServer.isEnabled, true))) + .orderBy(asc(mcpServer.createdAt)); + } catch (error) { + console.error( + 'Failed to get enabled MCP servers by user ID from database', + error, + ); + throw error; + } +} + +export async function getMcpServerByIdAndUserId({ + id, + userId, +}: { id: string; userId: string }) { + try { + const [server] = await db + .select() + .from(mcpServer) + .where(and(eq(mcpServer.id, id), eq(mcpServer.userId, userId))); + return server; + } catch (error) { + console.error('Failed to get MCP server by ID and user ID', error); + throw error; + } +} + +export async function updateMcpServerStatus({ + id, + isEnabled, +}: { id: string; isEnabled: boolean }) { + try { + const [updatedServer] = await db + .update(mcpServer) + .set({ isEnabled }) + .where(eq(mcpServer.id, id)) + .returning(); + return updatedServer; + } catch (error) { + console.error('Failed to update MCP server status in database', error); + throw error; + } +} + +export async function deleteMcpServer({ id }: { id: string }) { + try { + const [deletedServer] = await db + .delete(mcpServer) + .where(eq(mcpServer.id, id)) + .returning(); + return deletedServer; } catch (error) { - console.error('Failed to update chat visibility in database'); + console.error('Failed to delete MCP server from database', error); throw error; } } diff --git a/lib/db/schema.ts b/lib/db/schema.ts index 1228aab9a..5b48a734d 100644 --- a/lib/db/schema.ts +++ b/lib/db/schema.ts @@ -150,3 +150,17 @@ export const suggestion = pgTable( ); export type Suggestion = InferSelectModel; + +// MCP Server configuration table +export const mcpServer = pgTable('McpServer', { + id: uuid('id').primaryKey().notNull().defaultRandom(), + userId: uuid('userId') + .notNull() + .references(() => user.id), + name: text('name').notNull(), + config: json('config').notNull(), // Stores MCP connection details (type, url/command, etc.) + isEnabled: boolean('isEnabled').notNull().default(false), + createdAt: timestamp('createdAt').notNull().defaultNow(), +}); + +export type McpServer = InferSelectModel; diff --git a/package.json b/package.json index 0d1a16f56..32a7a3ddc 100644 --- a/package.json +++ b/package.json @@ -27,20 +27,22 @@ "@codemirror/state": "^6.5.0", "@codemirror/theme-one-dark": "^6.1.2", "@codemirror/view": "^6.35.3", + "@radix-ui/react-accordion": "^1.2.3", "@radix-ui/react-alert-dialog": "^1.1.2", - "@radix-ui/react-dialog": "^1.1.2", + "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-separator": "^1.1.0", - "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-tooltip": "^1.1.3", "@radix-ui/react-visually-hidden": "^1.1.0", "@vercel/analytics": "^1.3.1", "@vercel/blob": "^0.24.1", "@vercel/postgres": "^0.10.0", - "ai": "4.2.0", + "ai": "4.2.10", "bcrypt-ts": "^5.0.2", "class-variance-authority": "^0.7.0", "classnames": "^2.5.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 00380fb26..27255a62a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,11 +32,14 @@ importers: '@codemirror/view': specifier: ^6.35.3 version: 6.36.4 + '@radix-ui/react-accordion': + specifier: ^1.2.3 + version: 1.2.3(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021) '@radix-ui/react-alert-dialog': specifier: ^1.1.2 version: 1.1.6(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021) '@radix-ui/react-dialog': - specifier: ^1.1.2 + specifier: ^1.1.6 version: 1.1.6(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021) '@radix-ui/react-dropdown-menu': specifier: ^2.1.2 @@ -54,8 +57,11 @@ importers: specifier: ^1.1.0 version: 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021) '@radix-ui/react-slot': - specifier: ^1.1.0 + specifier: ^1.1.2 version: 1.1.2(@types/react@18.3.18)(react@19.0.0-rc-45804af1-20241021) + '@radix-ui/react-switch': + specifier: ^1.1.3 + version: 1.1.3(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021) '@radix-ui/react-tooltip': specifier: ^1.1.3 version: 1.1.8(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021) @@ -72,8 +78,8 @@ importers: specifier: ^0.10.0 version: 0.10.0 ai: - specifier: 4.2.0 - version: 4.2.0(react@19.0.0-rc-45804af1-20241021)(zod@3.24.2) + specifier: 4.2.10 + version: 4.2.10(react@19.0.0-rc-45804af1-20241021)(zod@3.24.2) bcrypt-ts: specifier: ^5.0.2 version: 5.0.3 @@ -276,6 +282,12 @@ packages: peerDependencies: zod: ^3.23.8 + '@ai-sdk/provider-utils@2.2.3': + resolution: {integrity: sha512-o3fWTzkxzI5Af7U7y794MZkYNEsxbjLam2nxyoUZSScqkacb7vZ3EYHLh21+xCcSSzEC161C7pZAGHtC0hTUMw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.23.8 + '@ai-sdk/provider@1.1.0': resolution: {integrity: sha512-0M+qjp+clUD0R1E5eWQFhxEvWLNaOtGQRUaBn8CUABnSKredagq92hUS9VjOzGsTm37xLfpaxl97AVtbeOsHew==} engines: {node: '>=18'} @@ -290,12 +302,28 @@ packages: zod: optional: true + '@ai-sdk/react@1.2.5': + resolution: {integrity: sha512-0jOop3S2WkDOdO4X5I+5fTGqZlNX8/h1T1eYokpkR9xh8Vmrxqw8SsovqGvrddTsZykH8uXRsvI+G4FTyy894A==} + engines: {node: '>=18'} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + zod: ^3.23.8 + peerDependenciesMeta: + zod: + optional: true + '@ai-sdk/ui-utils@1.2.0': resolution: {integrity: sha512-0IZwCqe7E+GkCASTDPAbzMr+POm9GDzWvFd37FvzpOeKNeibmge/LZEkTDbGSa+3b928H8wPwOLsOXBWPLUPDQ==} engines: {node: '>=18'} peerDependencies: zod: ^3.23.8 + '@ai-sdk/ui-utils@1.2.4': + resolution: {integrity: sha512-wLTxEZrKZRyBmlVZv8nGXgLBg5tASlqXwbuhoDu0MhZa467ZFREEnosH/OC/novyEHTQXko2zC606xoVbMrUcA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.23.8 + '@ai-sdk/xai@1.2.1': resolution: {integrity: sha512-aVrucjm9ikb9blpkO0oo8nVPRRoA1SD+GvfRU69YgjbM3XVNK07nk/mEpq7xuEU2iD+7/T2Fkl2rjayS+Ee4Jg==} engines: {node: '>=18'} @@ -1127,6 +1155,19 @@ packages: '@radix-ui/primitive@1.1.1': resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==} + '@radix-ui/react-accordion@1.2.3': + resolution: {integrity: sha512-RIQ15mrcvqIkDARJeERSuXSry2N8uYnxkdDetpfmalT/+0ntOXLkFOsh9iwlAsCv+qcmhZjbdJogIm6WBa6c4A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-alert-dialog@1.1.6': resolution: {integrity: sha512-p4XnPqgej8sZAAReCAKgz1REYZEBLR8hU9Pg27wFnCWIMc8g1ccCs0FjBcy05V15VTu8pAePw/VDYeOm/uZ6yQ==} peerDependencies: @@ -1153,6 +1194,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-collapsible@1.1.3': + resolution: {integrity: sha512-jFSerheto1X03MUC0g6R7LedNW9EEGWdg9W1+MlpkMLwGkgkbUXLPBH/KIuWKXUoeYRVY11llqbTBDzuLg7qrw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-collection@1.1.2': resolution: {integrity: sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==} peerDependencies: @@ -1394,6 +1448,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-switch@1.1.3': + resolution: {integrity: sha512-1nc+vjEOQkJVsJtWPSiISGT6OKm4SiOdjMo+/icLxo2G4vxz1GntC5MzfL4v8ey9OEfw787QCD1y3mUv0NiFEQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-tooltip@1.1.8': resolution: {integrity: sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA==} peerDependencies: @@ -1652,8 +1719,8 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - ai@4.2.0: - resolution: {integrity: sha512-3xJWzBZpBS3n/UY360IopufV5dpfgYoY08eCAV2A2m7CcyJxVOAQ4lXvBGSsB+mR+BYJ8Y/JOesFfc0+k4jz3A==} + ai@4.2.10: + resolution: {integrity: sha512-rOfKbNRWlzwxbFll6W9oAdnC0R5VVbAJoof+p92CatHzA3reqQZmYn33IBnj+CgqeXYUsH9KX9Wnj7g2wCHc9Q==} engines: {node: '>=18'} peerDependencies: react: ^18 || ^19 || ^19.0.0-rc @@ -3886,6 +3953,13 @@ snapshots: secure-json-parse: 2.7.0 zod: 3.24.2 + '@ai-sdk/provider-utils@2.2.3(zod@3.24.2)': + dependencies: + '@ai-sdk/provider': 1.1.0 + nanoid: 3.3.9 + secure-json-parse: 2.7.0 + zod: 3.24.2 + '@ai-sdk/provider@1.1.0': dependencies: json-schema: 0.4.0 @@ -3900,6 +3974,16 @@ snapshots: optionalDependencies: zod: 3.24.2 + '@ai-sdk/react@1.2.5(react@19.0.0-rc-45804af1-20241021)(zod@3.24.2)': + dependencies: + '@ai-sdk/provider-utils': 2.2.3(zod@3.24.2) + '@ai-sdk/ui-utils': 1.2.4(zod@3.24.2) + react: 19.0.0-rc-45804af1-20241021 + swr: 2.3.3(react@19.0.0-rc-45804af1-20241021) + throttleit: 2.1.0 + optionalDependencies: + zod: 3.24.2 + '@ai-sdk/ui-utils@1.2.0(zod@3.24.2)': dependencies: '@ai-sdk/provider': 1.1.0 @@ -3907,6 +3991,13 @@ snapshots: zod: 3.24.2 zod-to-json-schema: 3.24.3(zod@3.24.2) + '@ai-sdk/ui-utils@1.2.4(zod@3.24.2)': + dependencies: + '@ai-sdk/provider': 1.1.0 + '@ai-sdk/provider-utils': 2.2.3(zod@3.24.2) + zod: 3.24.2 + zod-to-json-schema: 3.24.3(zod@3.24.2) + '@ai-sdk/xai@1.2.1(zod@3.24.2)': dependencies: '@ai-sdk/openai-compatible': 0.2.0(zod@3.24.2) @@ -4500,6 +4591,23 @@ snapshots: '@radix-ui/primitive@1.1.1': {} + '@radix-ui/react-accordion@1.2.3(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-collapsible': 1.1.3(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021) + '@radix-ui/react-collection': 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@19.0.0-rc-45804af1-20241021) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@19.0.0-rc-45804af1-20241021) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.18)(react@19.0.0-rc-45804af1-20241021) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.18)(react@19.0.0-rc-45804af1-20241021) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@19.0.0-rc-45804af1-20241021) + react: 19.0.0-rc-45804af1-20241021 + react-dom: 19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + '@radix-ui/react-alert-dialog@1.1.6(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021)': dependencies: '@radix-ui/primitive': 1.1.1 @@ -4523,6 +4631,22 @@ snapshots: '@types/react': 18.3.18 '@types/react-dom': 18.3.5(@types/react@18.3.18) + '@radix-ui/react-collapsible@1.1.3(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@19.0.0-rc-45804af1-20241021) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@19.0.0-rc-45804af1-20241021) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.18)(react@19.0.0-rc-45804af1-20241021) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@19.0.0-rc-45804af1-20241021) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.18)(react@19.0.0-rc-45804af1-20241021) + react: 19.0.0-rc-45804af1-20241021 + react-dom: 19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + '@radix-ui/react-collection@1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021)': dependencies: '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@19.0.0-rc-45804af1-20241021) @@ -4775,6 +4899,21 @@ snapshots: optionalDependencies: '@types/react': 18.3.18 + '@radix-ui/react-switch@1.1.3(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@19.0.0-rc-45804af1-20241021) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@19.0.0-rc-45804af1-20241021) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@19.0.0-rc-45804af1-20241021) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.18)(react@19.0.0-rc-45804af1-20241021) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.18)(react@19.0.0-rc-45804af1-20241021) + react: 19.0.0-rc-45804af1-20241021 + react-dom: 19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + '@radix-ui/react-tooltip@1.1.8(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021)': dependencies: '@radix-ui/primitive': 1.1.1 @@ -5010,14 +5149,13 @@ snapshots: acorn@8.14.1: {} - ai@4.2.0(react@19.0.0-rc-45804af1-20241021)(zod@3.24.2): + ai@4.2.10(react@19.0.0-rc-45804af1-20241021)(zod@3.24.2): dependencies: '@ai-sdk/provider': 1.1.0 - '@ai-sdk/provider-utils': 2.2.0(zod@3.24.2) - '@ai-sdk/react': 1.2.0(react@19.0.0-rc-45804af1-20241021)(zod@3.24.2) - '@ai-sdk/ui-utils': 1.2.0(zod@3.24.2) + '@ai-sdk/provider-utils': 2.2.3(zod@3.24.2) + '@ai-sdk/react': 1.2.5(react@19.0.0-rc-45804af1-20241021)(zod@3.24.2) + '@ai-sdk/ui-utils': 1.2.4(zod@3.24.2) '@opentelemetry/api': 1.9.0 - eventsource-parser: 3.0.0 jsondiffpatch: 0.6.0 zod: 3.24.2 optionalDependencies: diff --git a/tailwind.config.ts b/tailwind.config.ts index 92574c7e3..45614558b 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -8,72 +8,98 @@ const config: Config = { './app/**/*.{js,ts,jsx,tsx,mdx}', ], theme: { - extend: { - fontFamily: { - sans: ['var(--font-geist)'], - mono: ['var(--font-geist-mono)'], - }, - screens: { - 'toast-mobile': '600px', - }, - borderRadius: { - lg: 'var(--radius)', - md: 'calc(var(--radius) - 2px)', - sm: 'calc(var(--radius) - 4px)', - }, - colors: { - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', - card: { - DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))', - }, - popover: { - DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))', - }, - primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))', - }, - secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))', - }, - muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))', - }, - accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))', - }, - destructive: { - DEFAULT: 'hsl(var(--destructive))', - foreground: 'hsl(var(--destructive-foreground))', - }, - border: 'hsl(var(--border))', - input: 'hsl(var(--input))', - ring: 'hsl(var(--ring))', - chart: { - '1': 'hsl(var(--chart-1))', - '2': 'hsl(var(--chart-2))', - '3': 'hsl(var(--chart-3))', - '4': 'hsl(var(--chart-4))', - '5': 'hsl(var(--chart-5))', - }, - sidebar: { - DEFAULT: 'hsl(var(--sidebar-background))', - foreground: 'hsl(var(--sidebar-foreground))', - primary: 'hsl(var(--sidebar-primary))', - 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))', - accent: 'hsl(var(--sidebar-accent))', - 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))', - border: 'hsl(var(--sidebar-border))', - ring: 'hsl(var(--sidebar-ring))', - }, - }, - }, + extend: { + fontFamily: { + sans: [ + 'var(--font-geist)' + ], + mono: [ + 'var(--font-geist-mono)' + ] + }, + screens: { + 'toast-mobile': '600px' + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)' + }, + colors: { + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))' + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))' + }, + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))' + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))' + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))' + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))' + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))' + }, + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + chart: { + '1': 'hsl(var(--chart-1))', + '2': 'hsl(var(--chart-2))', + '3': 'hsl(var(--chart-3))', + '4': 'hsl(var(--chart-4))', + '5': 'hsl(var(--chart-5))' + }, + sidebar: { + DEFAULT: 'hsl(var(--sidebar-background))', + foreground: 'hsl(var(--sidebar-foreground))', + primary: 'hsl(var(--sidebar-primary))', + 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))', + accent: 'hsl(var(--sidebar-accent))', + 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))', + border: 'hsl(var(--sidebar-border))', + ring: 'hsl(var(--sidebar-ring))' + } + }, + keyframes: { + 'accordion-down': { + from: { + height: '0' + }, + to: { + height: 'var(--radix-accordion-content-height)' + } + }, + 'accordion-up': { + from: { + height: 'var(--radix-accordion-content-height)' + }, + to: { + height: '0' + } + } + }, + animation: { + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out' + } + } }, plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')], };