```
├── .env.example
├── .gitignore
├── LICENSE
├── README.md
├── ai/
├── providers.ts
├── app/
├── actions.ts
├── api/
├── chat/
├── route.ts
├── chats/
├── [id]/
├── route.ts
├── route.ts
├── chat/
├── [id]/
├── page.tsx
├── favicon.ico
├── globals.css
├── layout.tsx
├── opengraph-image.png
├── page.tsx
├── providers.tsx
├── twitter-image.png
├── components.json
├── components/
├── api-key-manager.tsx
├── chat-sidebar.tsx
├── chat.tsx
├── copy-button.tsx
├── deploy-button.tsx
├── icons.tsx
├── input.tsx
├── markdown.tsx
├── mcp-server-manager.tsx
├── message.tsx
├── messages.tsx
├── model-picker.tsx
├── project-overview.tsx
├── suggested-prompts.tsx
├── textarea.tsx
├── theme-provider.tsx
├── theme-toggle.tsx
├── tool-invocation.tsx
├── ui/
├── accordion.tsx
├── avatar.tsx
├── badge.tsx
├── button.tsx
├── dialog.tsx
├── dropdown-menu.tsx
├── input.tsx
├── label.tsx
├── popover.tsx
├── scroll-area.tsx
├── select.tsx
├── separator.tsx
├── sheet.tsx
├── sidebar.tsx
├── skeleton.tsx
├── sonner.tsx
├── text-morph.tsx
├── textarea.tsx
├── tooltip.tsx
├── drizzle.config.ts
├── drizzle/
├── 0000_supreme_rocket_raccoon.sql
├── 0001_curious_paper_doll.sql
├── 0002_free_cobalt_man.sql
├── 0003_oval_energizer.sql
├── 0004_tense_ricochet.sql
├── 0005_early_payback.sql
├── meta/
├── 0000_snapshot.json
```
## /.env.example
```example path="/.env.example"
XAI_API_KEY=""
OPENAI_API_KEY=
DATABASE_URL="postgresql://username:password@host:port/database"
```
## /.gitignore
```gitignore path="/.gitignore"
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env.local
.env
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
```
## /LICENSE
``` path="/LICENSE"
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2025 Zaid Mukaddam
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
```
## /README.md
Scira MCP Chat
An open-source AI chatbot app powered by Model Context Protocol (MCP), built with Next.js and the AI SDK by Vercel.
Features •
MCP Configuration •
License
## Features
- Streaming text responses powered by the [AI SDK by Vercel](https://sdk.vercel.ai/docs), allowing multiple AI providers to be used interchangeably with just a few lines of code.
- Full integration with [Model Context Protocol (MCP)](https://modelcontextprotocol.io) servers to expand available tools and capabilities.
- Multiple MCP transport types (SSE and stdio) for connecting to various tool providers.
- Built-in tool integration for extending AI capabilities.
- Reasoning model support.
- [shadcn/ui](https://ui.shadcn.com/) components for a modern, responsive UI powered by [Tailwind CSS](https://tailwindcss.com).
- Built with the latest [Next.js](https://nextjs.org) App Router.
## MCP Server Configuration
This application supports connecting to Model Context Protocol (MCP) servers to access their tools. You can add and manage MCP servers through the settings icon in the chat interface.
### Adding an MCP Server
1. Click the settings icon (⚙️) next to the model selector in the chat interface.
2. Enter a name for your MCP server.
3. Select the transport type:
- **SSE (Server-Sent Events)**: For HTTP-based remote servers
- **stdio (Standard I/O)**: For local servers running on the same machine
#### SSE Configuration
If you select SSE transport:
1. Enter the server URL (e.g., `https://mcp.example.com/token/sse`)
2. Click "Add Server"
#### stdio Configuration
If you select stdio transport:
1. Enter the command to execute (e.g., `npx`)
2. Enter the command arguments (e.g., `-y @modelcontextprotocol/server-google-maps`)
- You can enter space-separated arguments or paste a JSON array
3. Click "Add Server"
4. Click "Use" to activate the server for the current chat session.
### Available MCP Servers
You can use any MCP-compatible server with this application. Here are some examples:
- [Composio](https://composio.dev/mcp) - Provides search, code interpreter, and other tools
- [Zapier MCP](https://zapier.com/mcp) - Provides access to Zapier tools
- Any MCP server using stdio transport with npx and python3
## License
This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
## /ai/providers.ts
```ts path="/ai/providers.ts"
import { createOpenAI } from "@ai-sdk/openai";
import { createGroq } from "@ai-sdk/groq";
import { createAnthropic } from "@ai-sdk/anthropic";
import { createXai } from "@ai-sdk/xai";
import {
customProvider,
wrapLanguageModel,
extractReasoningMiddleware
} from "ai";
export interface ModelInfo {
provider: string;
name: string;
description: string;
apiVersion: string;
capabilities: string[];
}
const middleware = extractReasoningMiddleware({
tagName: 'think',
});
// Helper to get API keys from environment variables first, then localStorage
const getApiKey = (key: string): string | undefined => {
// Check for environment variables first
if (process.env[key]) {
return process.env[key] || undefined;
}
// Fall back to localStorage if available
if (typeof window !== 'undefined') {
return window.localStorage.getItem(key) || undefined;
}
return undefined;
};
// Create provider instances with API keys from localStorage
const openaiClient = createOpenAI({
apiKey: getApiKey('OPENAI_API_KEY'),
});
const anthropicClient = createAnthropic({
apiKey: getApiKey('ANTHROPIC_API_KEY'),
});
const groqClient = createGroq({
apiKey: getApiKey('GROQ_API_KEY'),
});
const xaiClient = createXai({
apiKey: getApiKey('XAI_API_KEY'),
});
const languageModels = {
"gpt-4.1-mini": openaiClient("gpt-4.1-mini"),
"claude-3-7-sonnet": anthropicClient('claude-3-7-sonnet-20250219'),
"qwen-qwq": wrapLanguageModel(
{
model: groqClient("qwen-qwq-32b"),
middleware
}
),
"grok-3-mini": xaiClient("grok-3-mini-latest"),
};
export const modelDetails: Record = {
"gpt-4.1-mini": {
provider: "OpenAI",
name: "GPT-4.1 Mini",
description: "Compact version of OpenAI's GPT-4.1 with good balance of capabilities, including vision.",
apiVersion: "gpt-4.1-mini",
capabilities: ["Balance", "Creative", "Vision"]
},
"claude-3-7-sonnet": {
provider: "Anthropic",
name: "Claude 3.7 Sonnet",
description: "Latest version of Anthropic's Claude 3.7 Sonnet with strong reasoning and coding capabilities.",
apiVersion: "claude-3-7-sonnet-20250219",
capabilities: ["Reasoning", "Efficient", "Agentic"]
},
"qwen-qwq": {
provider: "Groq",
name: "Qwen QWQ",
description: "Latest version of Alibaba's Qwen QWQ with strong reasoning and coding capabilities.",
apiVersion: "qwen-qwq",
capabilities: ["Reasoning", "Efficient", "Agentic"]
},
"grok-3-mini": {
provider: "XAI",
name: "Grok 3 Mini",
description: "Latest version of XAI's Grok 3 Mini with strong reasoning and coding capabilities.",
apiVersion: "grok-3-mini-latest",
capabilities: ["Reasoning", "Efficient", "Agentic"]
},
};
// Update API keys when localStorage changes (for runtime updates)
if (typeof window !== 'undefined') {
window.addEventListener('storage', (event) => {
// Reload the page if any API key changed to refresh the providers
if (event.key?.includes('API_KEY')) {
window.location.reload();
}
});
}
export const model = customProvider({
languageModels,
});
export type modelID = keyof typeof languageModels;
export const MODELS = Object.keys(languageModels);
export const defaultModel: modelID = "qwen-qwq";
```
## /app/actions.ts
```ts path="/app/actions.ts"
"use server";
import { openai } from "@ai-sdk/openai";
import { generateObject } from "ai";
import { z } from "zod";
// Helper to extract text content from a message regardless of format
function getMessageText(message: any): string {
// Check if the message has parts (new format)
if (message.parts && Array.isArray(message.parts)) {
const textParts = message.parts.filter((p: any) => p.type === 'text' && p.text);
if (textParts.length > 0) {
return textParts.map((p: any) => p.text).join('\n');
}
}
// Fallback to content (old format)
if (typeof message.content === 'string') {
return message.content;
}
// If content is an array (potentially of parts), try to extract text
if (Array.isArray(message.content)) {
const textItems = message.content.filter((item: any) =>
typeof item === 'string' || (item.type === 'text' && item.text)
);
if (textItems.length > 0) {
return textItems.map((item: any) =>
typeof item === 'string' ? item : item.text
).join('\n');
}
}
return '';
}
export async function generateTitle(messages: any[]) {
// Convert messages to a format that OpenAI can understand
const normalizedMessages = messages.map(msg => ({
role: msg.role,
content: getMessageText(msg)
}));
const { object } = await generateObject({
model: openai("gpt-4.1"),
schema: z.object({
title: z.string().min(1).max(100),
}),
system: `
You are a helpful assistant that generates titles for chat conversations.
The title should be a short description of the conversation.
The title should be no more than 30 characters.
The title should be unique and not generic.
`,
messages: [
...normalizedMessages,
{
role: "user",
content: "Generate a title for the conversation.",
},
],
});
return object.title;
}
```
## /app/api/chat/route.ts
```ts path="/app/api/chat/route.ts"
import { model, type modelID } from "@/ai/providers";
import { streamText, type UIMessage } from "ai";
import { appendResponseMessages } from 'ai';
import { saveChat, saveMessages, convertToDBMessages } from '@/lib/chat-store';
import { nanoid } from 'nanoid';
import { db } from '@/lib/db';
import { chats } from '@/lib/db/schema';
import { eq, and } from 'drizzle-orm';
import { experimental_createMCPClient as createMCPClient, MCPTransport } from 'ai';
import { Experimental_StdioMCPTransport as StdioMCPTransport } from 'ai/mcp-stdio';
import { spawn } from "child_process";
// Allow streaming responses up to 30 seconds
export const maxDuration = 120;
interface KeyValuePair {
key: string;
value: string;
}
interface MCPServerConfig {
url: string;
type: 'sse' | 'stdio';
command?: string;
args?: string[];
env?: KeyValuePair[];
headers?: KeyValuePair[];
}
export async function POST(req: Request) {
const {
messages,
chatId,
selectedModel,
userId,
mcpServers = [],
}: {
messages: UIMessage[];
chatId?: string;
selectedModel: modelID;
userId: string;
mcpServers?: MCPServerConfig[];
} = await req.json();
if (!userId) {
return new Response(
JSON.stringify({ error: "User ID is required" }),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
const id = chatId || nanoid();
// Check if chat already exists for the given ID
// If not, we'll create it in onFinish
let isNewChat = false;
if (chatId) {
try {
const existingChat = await db.query.chats.findFirst({
where: and(
eq(chats.id, chatId),
eq(chats.userId, userId)
)
});
isNewChat = !existingChat;
} catch (error) {
console.error("Error checking for existing chat:", error);
// Continue anyway, we'll create the chat in onFinish
isNewChat = true;
}
} else {
// No ID provided, definitely new
isNewChat = true;
}
// Initialize tools
let tools = {};
const mcpClients: any[] = [];
// Process each MCP server configuration
for (const mcpServer of mcpServers) {
try {
// Create appropriate transport based on type
let transport: MCPTransport | { type: 'sse', url: string, headers?: Record };
if (mcpServer.type === 'sse') {
// Convert headers array to object for SSE transport
const headers: Record = {};
if (mcpServer.headers && mcpServer.headers.length > 0) {
mcpServer.headers.forEach(header => {
if (header.key) headers[header.key] = header.value || '';
});
}
transport = {
type: 'sse' as const,
url: mcpServer.url,
headers: Object.keys(headers).length > 0 ? headers : undefined
};
} else if (mcpServer.type === 'stdio') {
// For stdio transport, we need command and args
if (!mcpServer.command || !mcpServer.args || mcpServer.args.length === 0) {
console.warn("Skipping stdio MCP server due to missing command or args");
continue;
}
// Convert env array to object for stdio transport
const env: Record = {};
if (mcpServer.env && mcpServer.env.length > 0) {
mcpServer.env.forEach(envVar => {
if (envVar.key) env[envVar.key] = envVar.value || '';
});
}
// Check for uvx pattern and transform to python3 -m uv run
if (mcpServer.command === 'uvx') {
// install uv
const subprocess = spawn('pip3', ['install', 'uv']);
subprocess.on('close', (code: number) => {
if (code !== 0) {
console.error(`Failed to install uv: ${code}`);
}
});
// wait for the subprocess to finish
await new Promise((resolve) => {
subprocess.on('close', resolve);
console.log("installed uv");
});
console.log("Detected uvx pattern, transforming to python3 -m uv run");
mcpServer.command = 'python3';
// Get the tool name (first argument)
const toolName = mcpServer.args[0];
// Replace args with the new pattern
mcpServer.args = ['-m', 'uv', 'run', toolName, ...mcpServer.args.slice(1)];
}
// 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
else if (mcpServer.command.includes('python3')) {
const packageName = mcpServer.args[mcpServer.args.indexOf('-m') + 1];
console.log("installing python package", packageName);
const subprocess = spawn('pip3', ['install', packageName]);
subprocess.on('close', (code: number) => {
if (code !== 0) {
console.error(`Failed to install python package: ${code}`);
}
});
// wait for the subprocess to finish
await new Promise((resolve) => {
subprocess.on('close', resolve);
console.log("installed python package", packageName);
});
}
transport = new StdioMCPTransport({
command: mcpServer.command,
args: mcpServer.args,
env: Object.keys(env).length > 0 ? env : undefined
});
} else {
console.warn(`Skipping MCP server with unsupported transport type: ${mcpServer.type}`);
continue;
}
const mcpClient = await createMCPClient({ transport });
mcpClients.push(mcpClient);
const mcptools = await mcpClient.tools();
console.log(`MCP tools from ${mcpServer.type} transport:`, Object.keys(mcptools));
// Add MCP tools to tools object
tools = { ...tools, ...mcptools };
} catch (error) {
console.error("Failed to initialize MCP client:", error);
// Continue with other servers instead of failing the entire request
}
}
// Register cleanup for all clients
if (mcpClients.length > 0) {
req.signal.addEventListener('abort', async () => {
for (const client of mcpClients) {
try {
await client.close();
} catch (error) {
console.error("Error closing MCP client:", error);
}
}
});
}
console.log("messages", messages);
console.log("parts", messages.map(m => m.parts.map(p => p)));
// If there was an error setting up MCP clients but we at least have composio tools, continue
const result = streamText({
model: model.languageModel(selectedModel),
system: `You are a helpful assistant with access to a variety of tools.
Today's date is ${new Date().toISOString().split('T')[0]}.
The tools are very powerful, and you can use them to answer the user's question.
So choose the tool that is most relevant to the user's question.
If tools are not available, say you don't know or if the user wants a tool they can add one from the server icon in bottom left corner in the sidebar.
You can use multiple tools in a single response.
Always respond after using the tools for better user experience.
You can run multiple steps using all the tools!!!!
Make sure to use the right tool to respond to the user's question.
Multiple tools can be used in a single response and multiple steps can be used to answer the user's question.
## Response Format
- Markdown is supported.
- Respond according to tool's response.
- Use the tools to answer the user's question.
- If you don't know the answer, use the tools to find the answer or say you don't know.
`,
messages,
tools,
maxSteps: 20,
providerOptions: {
google: {
thinkingConfig: {
thinkingBudget: 2048,
},
},
anthropic: {
thinking: {
type: 'enabled',
budgetTokens: 12000
},
}
},
onError: (error) => {
console.error(JSON.stringify(error, null, 2));
},
async onFinish({ response }) {
const allMessages = appendResponseMessages({
messages,
responseMessages: response.messages,
});
await saveChat({
id,
userId,
messages: allMessages,
});
const dbMessages = convertToDBMessages(allMessages, id);
await saveMessages({ messages: dbMessages });
// close all mcp clients
// for (const client of mcpClients) {
// await client.close();
// }
}
});
result.consumeStream()
return result.toDataStreamResponse({
sendReasoning: true,
getErrorMessage: (error) => {
if (error instanceof Error) {
if (error.message.includes("Rate limit")) {
return "Rate limit exceeded. Please try again later.";
}
}
console.error(error);
return "An error occurred.";
},
});
}
```
## /app/api/chats/[id]/route.ts
```ts path="/app/api/chats/[id]/route.ts"
import { NextResponse } from "next/server";
import { getChatById, deleteChat } from "@/lib/chat-store";
interface Params {
params: {
id: string;
};
}
export async function GET(request: Request, { params }: Params) {
try {
const userId = request.headers.get('x-user-id');
if (!userId) {
return NextResponse.json({ error: "User ID is required" }, { status: 400 });
}
const { id } = await params;
const chat = await getChatById(id, userId);
if (!chat) {
return NextResponse.json(
{ error: "Chat not found" },
{ status: 404 }
);
}
return NextResponse.json(chat);
} catch (error) {
console.error("Error fetching chat:", error);
return NextResponse.json(
{ error: "Failed to fetch chat" },
{ status: 500 }
);
}
}
export async function DELETE(request: Request, { params }: Params) {
try {
const userId = request.headers.get('x-user-id');
if (!userId) {
return NextResponse.json({ error: "User ID is required" }, { status: 400 });
}
const { id } = await params;
await deleteChat(id, userId);
return NextResponse.json({ success: true });
} catch (error) {
console.error("Error deleting chat:", error);
return NextResponse.json(
{ error: "Failed to delete chat" },
{ status: 500 }
);
}
}
```
## /app/api/chats/route.ts
```ts path="/app/api/chats/route.ts"
import { NextResponse } from "next/server";
import { getChats } from "@/lib/chat-store";
export async function GET(request: Request) {
try {
const userId = request.headers.get('x-user-id');
if (!userId) {
return NextResponse.json({ error: "User ID is required" }, { status: 400 });
}
const chats = await getChats(userId);
return NextResponse.json(chats);
} catch (error) {
console.error("Error fetching chats:", error);
return NextResponse.json(
{ error: "Failed to fetch chats" },
{ status: 500 }
);
}
}
```
## /app/chat/[id]/page.tsx
```tsx path="/app/chat/[id]/page.tsx"
"use client";
import Chat from "@/components/chat";
import { getUserId } from "@/lib/user-id";
import { useQueryClient } from "@tanstack/react-query";
import { useParams } from "next/navigation";
import { useEffect } from "react";
export default function ChatPage() {
const params = useParams();
const chatId = params?.id as string;
const queryClient = useQueryClient();
const userId = getUserId();
// Prefetch chat data
useEffect(() => {
async function prefetchChat() {
if (!chatId || !userId) return;
// Check if data already exists in cache
const existingData = queryClient.getQueryData(['chat', chatId, userId]);
if (existingData) return;
// Prefetch the data
await queryClient.prefetchQuery({
queryKey: ['chat', chatId, userId] as const,
queryFn: async () => {
try {
const response = await fetch(`/api/chats/${chatId}`, {
headers: {
'x-user-id': userId
}
});
if (!response.ok) {
throw new Error('Failed to load chat');
}
return response.json();
} catch (error) {
console.error('Error prefetching chat:', error);
return null;
}
},
staleTime: 1000 * 60 * 5, // 5 minutes
});
}
prefetchChat();
}, [chatId, userId, queryClient]);
return ;
}
```
## /app/favicon.ico
Binary file available at https://raw.githubusercontent.com/zaidmukaddam/scira-mcp-chat/refs/heads/main/app/favicon.ico
## /app/globals.css
```css path="/app/globals.css"
@import "tailwindcss";
@plugin "tailwindcss-animate";
@custom-variant dark (&:is(.dark *));
@custom-variant sunset (&:is(.sunset *));
@custom-variant black (&:is(.black *));
:root {
--background: oklch(0.99 0.01 56.32);
--foreground: oklch(0.34 0.01 2.77);
--card: oklch(1.00 0 0);
--card-foreground: oklch(0.34 0.01 2.77);
--popover: oklch(1.00 0 0);
--popover-foreground: oklch(0.34 0.01 2.77);
--primary: oklch(0.74 0.16 34.71);
--primary-foreground: oklch(1.00 0 0);
--secondary: oklch(0.96 0.02 28.90);
--secondary-foreground: oklch(0.56 0.13 32.74);
--muted: oklch(0.97 0.02 39.40);
--muted-foreground: oklch(0.49 0.05 26.45);
--accent: oklch(0.83 0.11 58.00);
--accent-foreground: oklch(0.34 0.01 2.77);
--destructive: oklch(0.61 0.21 22.24);
--destructive-foreground: oklch(1.00 0 0);
--border: oklch(0.93 0.04 38.69);
--input: oklch(0.93 0.04 38.69);
--ring: oklch(0.74 0.16 34.71);
--chart-1: oklch(0.74 0.16 34.71);
--chart-2: oklch(0.83 0.11 58.00);
--chart-3: oklch(0.88 0.08 54.93);
--chart-4: oklch(0.82 0.11 40.89);
--chart-5: oklch(0.64 0.13 32.07);
--sidebar: oklch(0.97 0.02 39.40);
--sidebar-foreground: oklch(0.34 0.01 2.77);
--sidebar-primary: oklch(0.74 0.16 34.71);
--sidebar-primary-foreground: oklch(1.00 0 0);
--sidebar-accent: oklch(0.83 0.11 58.00);
--sidebar-accent-foreground: oklch(0.34 0.01 2.77);
--sidebar-border: oklch(0.93 0.04 38.69);
--sidebar-ring: oklch(0.74 0.16 34.71);
--font-sans: Montserrat, sans-serif;
--font-serif: Merriweather, serif;
--font-mono: Ubuntu Mono, monospace;
--radius: 0.625rem;
--shadow-2xs: 0px 6px 12px -3px hsl(0 0% 0% / 0.04);
--shadow-xs: 0px 6px 12px -3px hsl(0 0% 0% / 0.04);
--shadow-sm: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 1px 2px -4px hsl(0 0% 0% / 0.09);
--shadow: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 1px 2px -4px hsl(0 0% 0% / 0.09);
--shadow-md: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 2px 4px -4px hsl(0 0% 0% / 0.09);
--shadow-lg: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 4px 6px -4px hsl(0 0% 0% / 0.09);
--shadow-xl: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 8px 10px -4px hsl(0 0% 0% / 0.09);
--shadow-2xl: 0px 6px 12px -3px hsl(0 0% 0% / 0.22);
}
.dark {
--background: oklch(0.26 0.02 352.40);
--foreground: oklch(0.94 0.01 51.32);
--card: oklch(0.32 0.02 341.45);
--card-foreground: oklch(0.94 0.01 51.32);
--popover: oklch(0.32 0.02 341.45);
--popover-foreground: oklch(0.94 0.01 51.32);
--primary: oklch(0.57 0.15 35.26);
--primary-foreground: oklch(1.00 0 0);
--secondary: oklch(0.36 0.02 342.27);
--secondary-foreground: oklch(0.94 0.01 51.32);
--muted: oklch(0.32 0.02 341.45);
--muted-foreground: oklch(0.84 0.02 52.63);
--accent: oklch(0.83 0.11 58.00);
--accent-foreground: oklch(0.26 0.02 352.40);
--destructive: oklch(0.51 0.16 20.19);
--destructive-foreground: oklch(1.00 0 0);
--border: oklch(0.36 0.02 342.27);
--input: oklch(0.36 0.02 342.27);
--ring: oklch(0.74 0.16 34.71);
--chart-1: oklch(0.74 0.16 34.71);
--chart-2: oklch(0.83 0.11 58.00);
--chart-3: oklch(0.88 0.08 54.93);
--chart-4: oklch(0.82 0.11 40.89);
--chart-5: oklch(0.64 0.13 32.07);
--sidebar: oklch(0.26 0.02 352.40);
--sidebar-foreground: oklch(0.94 0.01 51.32);
--sidebar-primary: oklch(0.47 0.08 34.31);
--sidebar-primary-foreground: oklch(1.00 0 0);
--sidebar-accent: oklch(0.67 0.09 56.00);
--sidebar-accent-foreground: oklch(0.26 0.01 353.48);
--sidebar-border: oklch(0.36 0.02 342.27);
--sidebar-ring: oklch(0.74 0.16 34.71);
--font-sans: Montserrat, sans-serif;
--font-serif: Merriweather, serif;
--font-mono: Ubuntu Mono, monospace;
--radius: 0.625rem;
--shadow-2xs: 0px 6px 12px -3px hsl(0 0% 0% / 0.04);
--shadow-xs: 0px 6px 12px -3px hsl(0 0% 0% / 0.04);
--shadow-sm: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 1px 2px -4px hsl(0 0% 0% / 0.09);
--shadow: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 1px 2px -4px hsl(0 0% 0% / 0.09);
--shadow-md: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 2px 4px -4px hsl(0 0% 0% / 0.09);
--shadow-lg: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 4px 6px -4px hsl(0 0% 0% / 0.09);
--shadow-xl: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 8px 10px -4px hsl(0 0% 0% / 0.09);
--shadow-2xl: 0px 6px 12px -3px hsl(0 0% 0% / 0.22);
}
.sunset {
--background: oklch(0.98 0.03 80.00);
--foreground: oklch(0.34 0.01 2.77);
--card: oklch(1.00 0 0);
--card-foreground: oklch(0.34 0.01 2.77);
--popover: oklch(1.00 0 0);
--popover-foreground: oklch(0.34 0.01 2.77);
--primary: oklch(0.65 0.26 34.00);
--primary-foreground: oklch(1.00 0 0);
--secondary: oklch(0.96 0.05 60.00);
--secondary-foreground: oklch(0.56 0.13 32.74);
--muted: oklch(0.97 0.02 39.40);
--muted-foreground: oklch(0.49 0.05 26.45);
--accent: oklch(0.83 0.22 50.00);
--accent-foreground: oklch(0.34 0.01 2.77);
--destructive: oklch(0.61 0.21 22.24);
--destructive-foreground: oklch(1.00 0 0);
--border: oklch(0.93 0.06 60.00);
--input: oklch(0.93 0.06 60.00);
--ring: oklch(0.65 0.26 34.00);
--chart-1: oklch(0.65 0.26 34.00);
--chart-2: oklch(0.83 0.22 50.00);
--chart-3: oklch(0.88 0.15 54.93);
--chart-4: oklch(0.82 0.20 40.89);
--chart-5: oklch(0.64 0.18 32.07);
--sidebar: oklch(0.97 0.04 70.00);
--sidebar-foreground: oklch(0.34 0.01 2.77);
--sidebar-primary: oklch(0.65 0.26 34.00);
--sidebar-primary-foreground: oklch(1.00 0 0);
--sidebar-accent: oklch(0.83 0.22 50.00);
--sidebar-accent-foreground: oklch(0.34 0.01 2.77);
--sidebar-border: oklch(0.93 0.06 60.00);
--sidebar-ring: oklch(0.65 0.26 34.00);
--font-sans: Montserrat, sans-serif;
--font-serif: Merriweather, serif;
--font-mono: Ubuntu Mono, monospace;
--radius: 0.625rem;
--shadow-2xs: 0px 6px 12px -3px hsl(0 0% 0% / 0.04);
--shadow-xs: 0px 6px 12px -3px hsl(0 0% 0% / 0.04);
--shadow-sm: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 1px 2px -4px hsl(0 0% 0% / 0.09);
--shadow: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 1px 2px -4px hsl(0 0% 0% / 0.09);
--shadow-md: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 2px 4px -4px hsl(0 0% 0% / 0.09);
--shadow-lg: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 4px 6px -4px hsl(0 0% 0% / 0.09);
--shadow-xl: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 8px 10px -4px hsl(0 0% 0% / 0.09);
--shadow-2xl: 0px 6px 12px -3px hsl(0 0% 0% / 0.22);
}
.black {
--background: oklch(0.15 0.01 350.00);
--foreground: oklch(0.95 0.01 60.00);
--card: oklch(0.20 0.01 340.00);
--card-foreground: oklch(0.95 0.01 60.00);
--popover: oklch(0.20 0.01 340.00);
--popover-foreground: oklch(0.95 0.01 60.00);
--primary: oklch(0.45 0.10 35.00);
--primary-foreground: oklch(1.00 0 0);
--secondary: oklch(0.25 0.01 340.00);
--secondary-foreground: oklch(0.95 0.01 60.00);
--muted: oklch(0.22 0.01 340.00);
--muted-foreground: oklch(0.86 0.01 60.00);
--accent: oklch(0.70 0.09 58.00);
--accent-foreground: oklch(0.15 0.01 350.00);
--destructive: oklch(0.45 0.16 20.00);
--destructive-foreground: oklch(1.00 0 0);
--border: oklch(0.25 0.01 340.00);
--input: oklch(0.25 0.01 340.00);
--ring: oklch(0.45 0.10 35.00);
--chart-1: oklch(0.45 0.10 35.00);
--chart-2: oklch(0.70 0.09 58.00);
--chart-3: oklch(0.80 0.06 54.00);
--chart-4: oklch(0.75 0.08 40.00);
--chart-5: oklch(0.55 0.10 32.00);
--sidebar: oklch(0.15 0.01 350.00);
--sidebar-foreground: oklch(0.95 0.01 60.00);
--sidebar-primary: oklch(0.40 0.06 34.00);
--sidebar-primary-foreground: oklch(1.00 0 0);
--sidebar-accent: oklch(0.60 0.07 56.00);
--sidebar-accent-foreground: oklch(0.15 0.01 350.00);
--sidebar-border: oklch(0.25 0.01 340.00);
--sidebar-ring: oklch(0.45 0.10 35.00);
--font-sans: Montserrat, sans-serif;
--font-serif: Merriweather, serif;
--font-mono: Ubuntu Mono, monospace;
--radius: 0.625rem;
--shadow-2xs: 0px 6px 12px -3px hsl(0 0% 0% / 0.04);
--shadow-xs: 0px 6px 12px -3px hsl(0 0% 0% / 0.04);
--shadow-sm: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 1px 2px -4px hsl(0 0% 0% / 0.09);
--shadow: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 1px 2px -4px hsl(0 0% 0% / 0.09);
--shadow-md: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 2px 4px -4px hsl(0 0% 0% / 0.09);
--shadow-lg: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 4px 6px -4px hsl(0 0% 0% / 0.09);
--shadow-xl: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 8px 10px -4px hsl(0 0% 0% / 0.09);
--shadow-2xl: 0px 6px 12px -3px hsl(0 0% 0% / 0.22);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--font-sans: var(--font-sans);
--font-mono: var(--font-mono);
--font-serif: var(--font-serif);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--shadow-2xs: var(--shadow-2xs);
--shadow-xs: var(--shadow-xs);
--shadow-sm: var(--shadow-sm);
--shadow: var(--shadow);
--shadow-md: var(--shadow-md);
--shadow-lg: var(--shadow-lg);
--shadow-xl: var(--shadow-xl);
--shadow-2xl: var(--shadow-2xl);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
letter-spacing: var(--tracking-normal);
}
}
@layer utilities {
/* Hide scrollbar for Chrome, Safari and Opera */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
/* Use Firefox-specific scrollbar hiding when supported */
scrollbar-width: none;
}
}
```
## /app/layout.tsx
```tsx path="/app/layout.tsx"
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import { ChatSidebar } from "@/components/chat-sidebar";
import { SidebarTrigger } from "@/components/ui/sidebar";
import { Menu } from "lucide-react";
import { Providers } from "./providers";
import "./globals.css";
import Script from "next/script";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
metadataBase: new URL("https://mcp.scira.ai"),
title: "Scira MCP Chat",
description: "Scira MCP Chat is a minimalistic MCP client with a good feature set.",
openGraph: {
siteName: "Scira MCP Chat",
url: "https://mcp.scira.ai",
images: [
{
url: "https://mcp.scira.ai/opengraph-image.png",
width: 1200,
height: 630,
},
],
},
twitter: {
card: "summary_large_image",
title: "Scira MCP Chat",
description: "Scira MCP Chat is a minimalistic MCP client with a good feature set.",
images: ["https://mcp.scira.ai/twitter-image.png"],
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
);
}
```
## /app/opengraph-image.png
Binary file available at https://raw.githubusercontent.com/zaidmukaddam/scira-mcp-chat/refs/heads/main/app/opengraph-image.png
## /app/page.tsx
```tsx path="/app/page.tsx"
import Chat from "@/components/chat";
export default function Page() {
return ;
}
```
## /app/providers.tsx
```tsx path="/app/providers.tsx"
"use client";
import { ReactNode, useEffect, useState } from "react";
import { ThemeProvider } from "@/components/theme-provider";
import { SidebarProvider } from "@/components/ui/sidebar";
import { Toaster } from "sonner";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useLocalStorage } from "@/lib/hooks/use-local-storage";
import { STORAGE_KEYS } from "@/lib/constants";
import { MCPProvider } from "@/lib/context/mcp-context";
// Create a client
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
refetchOnWindowFocus: true,
},
},
});
export function Providers({ children }: { children: ReactNode }) {
const [sidebarOpen, setSidebarOpen] = useLocalStorage(
STORAGE_KEYS.SIDEBAR_STATE,
true
);
return (
{children}
);
}
```
## /app/twitter-image.png
Binary file available at https://raw.githubusercontent.com/zaidmukaddam/scira-mcp-chat/refs/heads/main/app/twitter-image.png
## /components.json
```json path="/components.json"
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
```
## /components/api-key-manager.tsx
```tsx path="/components/api-key-manager.tsx"
import { useState, useEffect } from "react";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { toast } from "sonner";
// API key configuration
interface ApiKeyConfig {
name: string;
key: string;
storageKey: string;
label: string;
placeholder: string;
}
// Available API keys configuration
const API_KEYS_CONFIG: ApiKeyConfig[] = [
{
name: "OpenAI",
key: "openai",
storageKey: "OPENAI_API_KEY",
label: "OpenAI API Key",
placeholder: "sk-..."
},
{
name: "Anthropic",
key: "anthropic",
storageKey: "ANTHROPIC_API_KEY",
label: "Anthropic API Key",
placeholder: "sk-ant-..."
},
{
name: "Groq",
key: "groq",
storageKey: "GROQ_API_KEY",
label: "Groq API Key",
placeholder: "gsk_..."
},
{
name: "XAI",
key: "xai",
storageKey: "XAI_API_KEY",
label: "XAI API Key",
placeholder: "xai-..."
}
];
interface ApiKeyManagerProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
export function ApiKeyManager({ open, onOpenChange }: ApiKeyManagerProps) {
// State to store API keys
const [apiKeys, setApiKeys] = useState>({});
// Load API keys from localStorage on initial mount
useEffect(() => {
const storedKeys: Record = {};
API_KEYS_CONFIG.forEach(config => {
const value = localStorage.getItem(config.storageKey);
if (value) {
storedKeys[config.key] = value;
}
});
setApiKeys(storedKeys);
}, []);
// Update API key in state
const handleApiKeyChange = (key: string, value: string) => {
setApiKeys(prev => ({
...prev,
[key]: value
}));
};
// Save API keys to localStorage
const handleSaveApiKeys = () => {
try {
API_KEYS_CONFIG.forEach(config => {
const value = apiKeys[config.key];
if (value && value.trim()) {
localStorage.setItem(config.storageKey, value.trim());
} else {
localStorage.removeItem(config.storageKey);
}
});
toast.success("API keys saved successfully");
onOpenChange(false);
} catch (error) {
console.error("Error saving API keys:", error);
toast.error("Failed to save API keys");
}
};
// Clear all API keys
const handleClearApiKeys = () => {
try {
API_KEYS_CONFIG.forEach(config => {
localStorage.removeItem(config.storageKey);
});
setApiKeys({});
toast.success("All API keys cleared");
} catch (error) {
console.error("Error clearing API keys:", error);
toast.error("Failed to clear API keys");
}
};
return (
API Key Settings
Enter your own API keys for different AI providers. Keys are stored securely in your browser's local storage.
{API_KEYS_CONFIG.map(config => (
{config.label}
handleApiKeyChange(config.key, e.target.value)}
placeholder={config.placeholder}
/>
))}
Clear All Keys
onOpenChange(false)}
>
Cancel
Save Keys
);
}
```
## /components/chat-sidebar.tsx
```tsx path="/components/chat-sidebar.tsx"
"use client";
import { useState, useEffect } from "react";
import { useRouter, usePathname } from "next/navigation";
import { MessageSquare, PlusCircle, Trash2, ServerIcon, Settings, Sparkles, ChevronsUpDown, Copy, Pencil, Github, Key } from "lucide-react";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuBadge,
useSidebar
} from "@/components/ui/sidebar";
import { Separator } from "@/components/ui/separator";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { toast } from "sonner";
import Image from "next/image";
import { MCPServerManager } from "./mcp-server-manager";
import { ApiKeyManager } from "./api-key-manager";
import { ThemeToggle } from "./theme-toggle";
import { getUserId, updateUserId } from "@/lib/user-id";
import { useChats } from "@/lib/hooks/use-chats";
import { cn } from "@/lib/utils";
import Link from "next/link";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useMCP } from "@/lib/context/mcp-context";
import { Skeleton } from "@/components/ui/skeleton";
import { AnimatePresence, motion } from "motion/react";
export function ChatSidebar() {
const router = useRouter();
const pathname = usePathname();
const [userId, setUserId] = useState('');
const [mcpSettingsOpen, setMcpSettingsOpen] = useState(false);
const [apiKeySettingsOpen, setApiKeySettingsOpen] = useState(false);
const { state } = useSidebar();
const isCollapsed = state === "collapsed";
const [editUserIdOpen, setEditUserIdOpen] = useState(false);
const [newUserId, setNewUserId] = useState('');
// Get MCP server data from context
const { mcpServers, setMcpServers, selectedMcpServers, setSelectedMcpServers } = useMCP();
// Initialize userId
useEffect(() => {
setUserId(getUserId());
}, []);
// Use TanStack Query to fetch chats
const { chats, isLoading, deleteChat, refreshChats } = useChats(userId);
// Start a new chat
const handleNewChat = () => {
router.push('/');
};
// Delete a chat
const handleDeleteChat = async (chatId: string, e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
deleteChat(chatId);
// If we're currently on the deleted chat's page, navigate to home
if (pathname === `/chat/${chatId}`) {
router.push('/');
}
};
// Get active MCP servers status
const activeServersCount = selectedMcpServers.length;
// Handle user ID update
const handleUpdateUserId = () => {
if (!newUserId.trim()) {
toast.error("User ID cannot be empty");
return;
}
updateUserId(newUserId.trim());
setUserId(newUserId.trim());
setEditUserIdOpen(false);
toast.success("User ID updated successfully");
// Refresh the page to reload chats with new user ID
window.location.reload();
};
// Show loading state if user ID is not yet initialized
if (!userId) {
return null; // Or a loading spinner
}
// Create chat loading skeletons
const renderChatSkeletons = () => {
return Array(3).fill(0).map((_, index) => (
{!isCollapsed && (
<>
>
)}
));
};
return (
{!isCollapsed && (
MCP
)}
Chats
{isLoading ? (
renderChatSkeletons()
) : chats.length === 0 ? (
{isCollapsed ? (
) : (
No conversations yet
)}
) : (
{chats.map((chat) => (
{!isCollapsed && (
{chat.title.length > 18 ? `${chat.title.slice(0, 18)}...` : chat.title}
)}
{!isCollapsed && (
handleDeleteChat(chat.id, e)}
title="Delete chat"
>
)}
))}
)}
MCP Servers
setMcpSettingsOpen(true)}
className={cn(
"w-full flex items-center gap-2 transition-all",
"hover:bg-secondary/50 active:bg-secondary/70"
)}
tooltip={isCollapsed ? "MCP Servers" : undefined}
>
0 ? "text-primary" : "text-muted-foreground"
)} />
{!isCollapsed && (
MCP Servers
)}
{activeServersCount > 0 && !isCollapsed ? (
{activeServersCount}
) : activeServersCount > 0 && isCollapsed ? (
{activeServersCount}
) : null}
{!isCollapsed && New Chat }
{isCollapsed ? (
{userId.substring(0, 2).toUpperCase()}
) : (
{userId.substring(0, 2).toUpperCase()}
User ID
{userId.substring(0, 16)}...
)}
{userId.substring(0, 2).toUpperCase()}
User ID
{userId}
{
e.preventDefault();
navigator.clipboard.writeText(userId);
toast.success("User ID copied to clipboard");
}}>
Copy User ID
{
e.preventDefault();
setEditUserIdOpen(true);
}}>
Edit User ID
{
e.preventDefault();
setMcpSettingsOpen(true);
}}>
MCP Settings
{
e.preventDefault();
setApiKeySettingsOpen(true);
}}>
API Keys
{
e.preventDefault();
window.open("https://git.new/s-mcp", "_blank");
}}>
GitHub
e.preventDefault()}>
{
setEditUserIdOpen(open);
if (open) {
setNewUserId(userId);
}
}}>
Edit User ID
Update your user ID for chat synchronization. This will affect which chats are visible to you.
setEditUserIdOpen(false)}
>
Cancel
Save Changes
);
}
```
## /components/chat.tsx
```tsx path="/components/chat.tsx"
"use client";
import { defaultModel, type modelID } from "@/ai/providers";
import { Message, useChat } from "@ai-sdk/react";
import { useState, useEffect, useMemo, useCallback } from "react";
import { Textarea } from "./textarea";
import { ProjectOverview } from "./project-overview";
import { Messages } from "./messages";
import { toast } from "sonner";
import { useRouter, useParams } from "next/navigation";
import { getUserId } from "@/lib/user-id";
import { useLocalStorage } from "@/lib/hooks/use-local-storage";
import { STORAGE_KEYS } from "@/lib/constants";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { convertToUIMessages } from "@/lib/chat-store";
import { type Message as DBMessage } from "@/lib/db/schema";
import { nanoid } from "nanoid";
import { useMCP } from "@/lib/context/mcp-context";
// Type for chat data from DB
interface ChatData {
id: string;
messages: DBMessage[];
createdAt: string;
updatedAt: string;
}
export default function Chat() {
const router = useRouter();
const params = useParams();
const chatId = params?.id as string | undefined;
const queryClient = useQueryClient();
const [selectedModel, setSelectedModel] = useLocalStorage("selectedModel", defaultModel);
const [userId, setUserId] = useState('');
const [generatedChatId, setGeneratedChatId] = useState('');
// Get MCP server data from context
const { mcpServersForApi } = useMCP();
// Initialize userId
useEffect(() => {
setUserId(getUserId());
}, []);
// Generate a chat ID if needed
useEffect(() => {
if (!chatId) {
setGeneratedChatId(nanoid());
}
}, [chatId]);
// Use React Query to fetch chat history
const { data: chatData, isLoading: isLoadingChat } = useQuery({
queryKey: ['chat', chatId, userId] as const,
queryFn: async ({ queryKey }) => {
const [_, chatId, userId] = queryKey;
if (!chatId || !userId) return null;
try {
const response = await fetch(`/api/chats/${chatId}`, {
headers: {
'x-user-id': userId
}
});
if (!response.ok) {
throw new Error('Failed to load chat');
}
const data = await response.json();
return data as ChatData;
} catch (error) {
console.error('Error loading chat history:', error);
toast.error('Failed to load chat history');
throw error;
}
},
enabled: !!chatId && !!userId,
retry: 1,
staleTime: 1000 * 60 * 5, // 5 minutes
refetchOnWindowFocus: false
});
// Prepare initial messages from query data
const initialMessages = useMemo(() => {
if (!chatData || !chatData.messages || chatData.messages.length === 0) {
return [];
}
// Convert DB messages to UI format, then ensure it matches the Message type from @ai-sdk/react
const uiMessages = convertToUIMessages(chatData.messages);
return uiMessages.map(msg => ({
id: msg.id,
role: msg.role as Message['role'], // Ensure role is properly typed
content: msg.content,
parts: msg.parts,
} as Message));
}, [chatData]);
const { messages, input, handleInputChange, handleSubmit, status, stop } =
useChat({
id: chatId || generatedChatId, // Use generated ID if no chatId in URL
initialMessages,
maxSteps: 20,
body: {
selectedModel,
mcpServers: mcpServersForApi,
chatId: chatId || generatedChatId, // Use generated ID if no chatId in URL
userId,
},
experimental_throttle: 500,
onFinish: () => {
// Invalidate the chats query to refresh the sidebar
if (userId) {
queryClient.invalidateQueries({ queryKey: ['chats', userId] });
}
},
onError: (error) => {
toast.error(
error.message.length > 0
? error.message
: "An error occured, please try again later.",
{ position: "top-center", richColors: true },
);
},
});
// Custom submit handler
const handleFormSubmit = useCallback((e: React.FormEvent) => {
e.preventDefault();
if (!chatId && generatedChatId && input.trim()) {
// If this is a new conversation, redirect to the chat page with the generated ID
const effectiveChatId = generatedChatId;
// Submit the form
handleSubmit(e);
// Redirect to the chat page with the generated ID
router.push(`/chat/${effectiveChatId}`);
} else {
// Normal submission for existing chats
handleSubmit(e);
}
}, [chatId, generatedChatId, input, handleSubmit, router]);
const isLoading = status === "streaming" || status === "submitted" || isLoadingChat;
return (
{messages.length === 0 && !isLoadingChat ? (
) : (
<>
>
)}
);
}
```
## /components/copy-button.tsx
```tsx path="/components/copy-button.tsx"
import { CheckIcon, CopyIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { useCopy } from "@/lib/hooks/use-copy";
import { Button } from "./ui/button";
interface CopyButtonProps {
text: string;
className?: string;
}
export function CopyButton({ text, className }: CopyButtonProps) {
const { copied, copy } = useCopy();
return (
copy(text)}
title="Copy to clipboard"
>
{copied ? (
<>
Copied!
>
) : (
<>
Copy
>
)}
);
}
```
## /components/deploy-button.tsx
```tsx path="/components/deploy-button.tsx"
import Link from "next/link";
export const DeployButton = () => (
Deploy
);
```
## /components/icons.tsx
```tsx path="/components/icons.tsx"
import Link from "next/link";
import * as React from "react";
import type { SVGProps } from "react";
export const VercelIcon = ({ size = 17 }) => {
return (
Vercel Icon
);
};
export const SpinnerIcon = ({ size = 16 }: { size?: number }) => (
Spinner Icon
);
export const Github = (props: SVGProps) => (
GitHub Icon
);
export function StarButton() {
return (
Star on GitHub
);
}
export const XAiIcon = ({ size = 16 }) => {
return (
xAI Icon
);
};
```
## /components/input.tsx
```tsx path="/components/input.tsx"
import { ArrowUp } from "lucide-react";
import { Input as ShadcnInput } from "./ui/input";
interface InputProps {
input: string;
handleInputChange: (event: React.ChangeEvent) => void;
isLoading: boolean;
status: string;
stop: () => void;
}
export const Input = ({
input,
handleInputChange,
isLoading,
status,
stop,
}: InputProps) => {
return (
{status === "streaming" || status === "submitted" ? (
) : (
)}
);
};
```
## /components/markdown.tsx
```tsx path="/components/markdown.tsx"
/* eslint-disable @typescript-eslint/no-unused-vars */
import Link from "next/link";
import React, { memo } from "react";
import ReactMarkdown, { type Components } from "react-markdown";
import remarkGfm from "remark-gfm";
import { cn } from "@/lib/utils";
const components: Partial = {
pre: ({ children, ...props }) => (
{children}
),
code: ({ children, className, ...props }: React.HTMLProps & { className?: string }) => {
const match = /language-(\w+)/.exec(className || '');
const isInline = !match && !className;
if (isInline) {
return (
{children}
);
}
return (
{children}
);
},
ol: ({ node, children, ...props }) => (
{children}
),
ul: ({ node, children, ...props }) => (
),
li: ({ node, children, ...props }) => (
{children}
),
p: ({ node, children, ...props }) => (
{children}
),
strong: ({ node, children, ...props }) => (
{children}
),
em: ({ node, children, ...props }) => (
{children}
),
blockquote: ({ node, children, ...props }) => (
{children}
),
a: ({ node, children, ...props }) => (
// @ts-expect-error error
{children}
),
h1: ({ node, children, ...props }) => (
{children}
),
h2: ({ node, children, ...props }) => (
{children}
),
h3: ({ node, children, ...props }) => (
{children}
),
h4: ({ node, children, ...props }) => (
{children}
),
h5: ({ node, children, ...props }) => (
{children}
),
h6: ({ node, children, ...props }) => (
{children}
),
table: ({ node, children, ...props }) => (
),
thead: ({ node, children, ...props }) => (
{children}
),
tbody: ({ node, children, ...props }) => (
{children}
),
tr: ({ node, children, ...props }) => (
{children}
),
th: ({ node, children, ...props }) => (
{children}
),
td: ({ node, children, ...props }) => (
{children}
),
hr: ({ node, ...props }) => (
),
};
const remarkPlugins = [remarkGfm];
const NonMemoizedMarkdown = ({ children }: { children: string }) => {
return (
{children}
);
};
export const Markdown = memo(
NonMemoizedMarkdown,
(prevProps, nextProps) => prevProps.children === nextProps.children,
);
```
## /components/mcp-server-manager.tsx
```tsx path="/components/mcp-server-manager.tsx"
"use client";
import { useState } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle
} from "./ui/dialog";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
import {
PlusCircle,
ServerIcon,
X,
Terminal,
Globe,
ExternalLink,
Trash2,
CheckCircle,
Plus,
Cog,
Edit2,
Eye,
EyeOff
} from "lucide-react";
import { toast } from "sonner";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger
} from "./ui/accordion";
import { KeyValuePair, MCPServer } from "@/lib/context/mcp-context";
// Default template for a new MCP server
const INITIAL_NEW_SERVER: Omit = {
name: '',
url: '',
type: 'sse',
command: 'node',
args: [],
env: [],
headers: []
};
interface MCPServerManagerProps {
servers: MCPServer[];
onServersChange: (servers: MCPServer[]) => void;
selectedServers: string[];
onSelectedServersChange: (serverIds: string[]) => void;
open: boolean;
onOpenChange: (open: boolean) => void;
}
// Check if a key name might contain sensitive information
const isSensitiveKey = (key: string): boolean => {
const sensitivePatterns = [
/key/i,
/token/i,
/secret/i,
/password/i,
/pass/i,
/auth/i,
/credential/i
];
return sensitivePatterns.some(pattern => pattern.test(key));
};
// Mask a sensitive value
const maskValue = (value: string): string => {
if (!value) return '';
if (value.length < 8) return '••••••';
return value.substring(0, 3) + '•'.repeat(Math.min(10, value.length - 4)) + value.substring(value.length - 1);
};
export const MCPServerManager = ({
servers,
onServersChange,
selectedServers,
onSelectedServersChange,
open,
onOpenChange
}: MCPServerManagerProps) => {
const [newServer, setNewServer] = useState>(INITIAL_NEW_SERVER);
const [view, setView] = useState<'list' | 'add'>('list');
const [newEnvVar, setNewEnvVar] = useState({ key: '', value: '' });
const [newHeader, setNewHeader] = useState({ key: '', value: '' });
const [editingServerId, setEditingServerId] = useState(null);
const [showSensitiveEnvValues, setShowSensitiveEnvValues] = useState>({});
const [showSensitiveHeaderValues, setShowSensitiveHeaderValues] = useState>({});
const [editingEnvIndex, setEditingEnvIndex] = useState(null);
const [editingHeaderIndex, setEditingHeaderIndex] = useState(null);
const [editedEnvValue, setEditedEnvValue] = useState('');
const [editedHeaderValue, setEditedHeaderValue] = useState('');
const resetAndClose = () => {
setView('list');
setNewServer(INITIAL_NEW_SERVER);
setNewEnvVar({ key: '', value: '' });
setNewHeader({ key: '', value: '' });
setShowSensitiveEnvValues({});
setShowSensitiveHeaderValues({});
setEditingEnvIndex(null);
setEditingHeaderIndex(null);
onOpenChange(false);
};
const addServer = () => {
if (!newServer.name) {
toast.error("Server name is required");
return;
}
if (newServer.type === 'sse' && !newServer.url) {
toast.error("Server URL is required for SSE transport");
return;
}
if (newServer.type === 'stdio' && (!newServer.command || !newServer.args?.length)) {
toast.error("Command and at least one argument are required for stdio transport");
return;
}
const id = crypto.randomUUID();
const updatedServers = [...servers, { ...newServer, id }];
onServersChange(updatedServers);
toast.success(`Added MCP server: ${newServer.name}`);
setView('list');
setNewServer(INITIAL_NEW_SERVER);
setNewEnvVar({ key: '', value: '' });
setNewHeader({ key: '', value: '' });
setShowSensitiveEnvValues({});
setShowSensitiveHeaderValues({});
};
const removeServer = (id: string, e: React.MouseEvent) => {
e.stopPropagation();
const updatedServers = servers.filter(server => server.id !== id);
onServersChange(updatedServers);
// If the removed server was selected, remove it from selected servers
if (selectedServers.includes(id)) {
onSelectedServersChange(selectedServers.filter(serverId => serverId !== id));
}
toast.success("Server removed");
};
const toggleServer = (id: string) => {
if (selectedServers.includes(id)) {
// Remove from selected servers
onSelectedServersChange(selectedServers.filter(serverId => serverId !== id));
const server = servers.find(s => s.id === id);
if (server) {
toast.success(`Disabled MCP server: ${server.name}`);
}
} else {
// Add to selected servers
onSelectedServersChange([...selectedServers, id]);
const server = servers.find(s => s.id === id);
if (server) {
toast.success(`Enabled MCP server: ${server.name}`);
}
}
};
const clearAllServers = () => {
if (selectedServers.length > 0) {
onSelectedServersChange([]);
toast.success("All MCP servers disabled");
resetAndClose();
}
};
const handleArgsChange = (value: string) => {
try {
// Try to parse as JSON if it starts with [ (array)
const argsArray = value.trim().startsWith('[')
? JSON.parse(value)
: value.split(' ').filter(Boolean);
setNewServer({ ...newServer, args: argsArray });
} catch (error) {
// If parsing fails, just split by spaces
setNewServer({ ...newServer, args: value.split(' ').filter(Boolean) });
}
};
const addEnvVar = () => {
if (!newEnvVar.key) return;
setNewServer({
...newServer,
env: [...(newServer.env || []), { ...newEnvVar }]
});
setNewEnvVar({ key: '', value: '' });
};
const removeEnvVar = (index: number) => {
const updatedEnv = [...(newServer.env || [])];
updatedEnv.splice(index, 1);
setNewServer({ ...newServer, env: updatedEnv });
// Clean up visibility state for this index
const updatedVisibility = { ...showSensitiveEnvValues };
delete updatedVisibility[index];
setShowSensitiveEnvValues(updatedVisibility);
// If currently editing this value, cancel editing
if (editingEnvIndex === index) {
setEditingEnvIndex(null);
}
};
const startEditEnvValue = (index: number, value: string) => {
setEditingEnvIndex(index);
setEditedEnvValue(value);
};
const saveEditedEnvValue = () => {
if (editingEnvIndex !== null) {
const updatedEnv = [...(newServer.env || [])];
updatedEnv[editingEnvIndex] = {
...updatedEnv[editingEnvIndex],
value: editedEnvValue
};
setNewServer({ ...newServer, env: updatedEnv });
setEditingEnvIndex(null);
}
};
const addHeader = () => {
if (!newHeader.key) return;
setNewServer({
...newServer,
headers: [...(newServer.headers || []), { ...newHeader }]
});
setNewHeader({ key: '', value: '' });
};
const removeHeader = (index: number) => {
const updatedHeaders = [...(newServer.headers || [])];
updatedHeaders.splice(index, 1);
setNewServer({ ...newServer, headers: updatedHeaders });
// Clean up visibility state for this index
const updatedVisibility = { ...showSensitiveHeaderValues };
delete updatedVisibility[index];
setShowSensitiveHeaderValues(updatedVisibility);
// If currently editing this value, cancel editing
if (editingHeaderIndex === index) {
setEditingHeaderIndex(null);
}
};
const startEditHeaderValue = (index: number, value: string) => {
setEditingHeaderIndex(index);
setEditedHeaderValue(value);
};
const saveEditedHeaderValue = () => {
if (editingHeaderIndex !== null) {
const updatedHeaders = [...(newServer.headers || [])];
updatedHeaders[editingHeaderIndex] = {
...updatedHeaders[editingHeaderIndex],
value: editedHeaderValue
};
setNewServer({ ...newServer, headers: updatedHeaders });
setEditingHeaderIndex(null);
}
};
const toggleSensitiveEnvValue = (index: number) => {
setShowSensitiveEnvValues(prev => ({
...prev,
[index]: !prev[index]
}));
};
const toggleSensitiveHeaderValue = (index: number) => {
setShowSensitiveHeaderValues(prev => ({
...prev,
[index]: !prev[index]
}));
};
const hasAdvancedConfig = (server: MCPServer) => {
return (server.env && server.env.length > 0) ||
(server.headers && server.headers.length > 0);
};
// Editing support
const startEditing = (server: MCPServer) => {
setEditingServerId(server.id);
setNewServer({
name: server.name,
url: server.url,
type: server.type,
command: server.command,
args: server.args,
env: server.env,
headers: server.headers
});
setView('add');
// Reset sensitive value visibility states
setShowSensitiveEnvValues({});
setShowSensitiveHeaderValues({});
setEditingEnvIndex(null);
setEditingHeaderIndex(null);
};
const handleFormCancel = () => {
if (view === 'add') {
setView('list');
setEditingServerId(null);
setNewServer(INITIAL_NEW_SERVER);
setShowSensitiveEnvValues({});
setShowSensitiveHeaderValues({});
setEditingEnvIndex(null);
setEditingHeaderIndex(null);
} else {
resetAndClose();
}
};
const updateServer = () => {
if (!newServer.name) {
toast.error("Server name is required");
return;
}
if (newServer.type === 'sse' && !newServer.url) {
toast.error("Server URL is required for SSE transport");
return;
}
if (newServer.type === 'stdio' && (!newServer.command || !newServer.args?.length)) {
toast.error("Command and at least one argument are required for stdio transport");
return;
}
const updated = servers.map(s =>
s.id === editingServerId ? { ...newServer, id: editingServerId! } : s
);
onServersChange(updated);
toast.success(`Updated MCP server: ${newServer.name}`);
setView('list');
setEditingServerId(null);
setNewServer(INITIAL_NEW_SERVER);
setShowSensitiveEnvValues({});
setShowSensitiveHeaderValues({});
};
return (
MCP Server Configuration
Connect to Model Context Protocol servers to access additional AI tools.
{selectedServers.length > 0 && (
{selectedServers.length} server{selectedServers.length !== 1 ? 's' : ''} currently active
)}
{view === 'list' ? (
{servers.length > 0 ? (
Available Servers
Select multiple servers to combine their tools
{servers
.sort((a, b) => {
const aActive = selectedServers.includes(a.id);
const bActive = selectedServers.includes(b.id);
if (aActive && !bActive) return -1;
if (!aActive && bActive) return 1;
return 0;
})
.map((server) => {
const isActive = selectedServers.includes(server.id);
return (
{/* Server Header with Type Badge and Delete Button */}
{server.type === 'sse' ? (
) : (
)}
{server.name}
{hasAdvancedConfig(server) && (
)}
{server.type.toUpperCase()}
removeServer(server.id, e)}
className="p-1 rounded-full hover:bg-muted/70"
aria-label="Remove server"
>
startEditing(server)}
className="p-1 rounded-full hover:bg-muted/50"
aria-label="Edit server"
>
{/* Server Details */}
{server.type === 'sse'
? server.url
: `${server.command} ${server.args?.join(' ')}`
}
{/* Action Button */}
toggleServer(server.id)}
>
{isActive && }
{isActive ? "Active" : "Enable Server"}
);
})}
) : (
No MCP Servers Added
Add your first MCP server to access additional AI tools
)}
) : (
{editingServerId ? "Edit MCP Server" : "Add New MCP Server"}
Server Name
setNewServer({ ...newServer, name: e.target.value })}
placeholder="My MCP Server"
className="relative z-0"
/>
Transport Type
Choose how to connect to your MCP server:
setNewServer({ ...newServer, type: 'sse' })}
className={`flex items-center gap-2 p-3 rounded-md text-left border transition-all ${
newServer.type === 'sse'
? 'border-primary bg-primary/10 ring-1 ring-primary'
: 'border-border hover:border-border/80 hover:bg-muted/50'
}`}
>
setNewServer({ ...newServer, type: 'stdio' })}
className={`flex items-center gap-2 p-3 rounded-md text-left border transition-all ${
newServer.type === 'stdio'
? 'border-primary bg-primary/10 ring-1 ring-primary'
: 'border-border hover:border-border/80 hover:bg-muted/50'
}`}
>
{newServer.type === 'sse' ? (
) : (
<>
>
)}
{/* Advanced Configuration */}
Environment Variables
{newServer.env && newServer.env.length > 0 ? (
{newServer.env.map((env, index) => (
{env.key}
=
{editingEnvIndex === index ? (
setEditedEnvValue(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && saveEditedEnvValue()}
autoFocus
/>
Save
) : (
<>
{isSensitiveKey(env.key) && !showSensitiveEnvValues[index]
? maskValue(env.value)
: env.value}
{isSensitiveKey(env.key) && (
toggleSensitiveEnvValue(index)}
className="p-1 hover:bg-muted/50 rounded-full"
>
{showSensitiveEnvValues[index] ? (
) : (
)}
)}
startEditEnvValue(index, env.value)}
className="p-1 hover:bg-muted/50 rounded-full"
>
>
)}
removeEnvVar(index)}
className="h-6 w-6 p-0 ml-2"
>
))}
) : (
No environment variables added
)}
Environment variables will be passed to the MCP server process.
{newServer.type === 'sse' ? 'HTTP Headers' : 'Additional Configuration'}
Key
setNewHeader({ ...newHeader, key: e.target.value })}
placeholder="Authorization"
className="h-8 relative z-0"
/>
Value
setNewHeader({ ...newHeader, value: e.target.value })}
placeholder="Bearer token123"
className="h-8 relative z-0"
/>
{newServer.headers && newServer.headers.length > 0 ? (
{newServer.headers.map((header, index) => (
{header.key}
:
{editingHeaderIndex === index ? (
setEditedHeaderValue(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && saveEditedHeaderValue()}
autoFocus
/>
Save
) : (
<>
{isSensitiveKey(header.key) && !showSensitiveHeaderValues[index]
? maskValue(header.value)
: header.value}
{isSensitiveKey(header.key) && (
toggleSensitiveHeaderValue(index)}
className="p-1 hover:bg-muted/50 rounded-full"
>
{showSensitiveHeaderValues[index] ? (
) : (
)}
)}
startEditHeaderValue(index, header.value)}
className="p-1 hover:bg-muted/50 rounded-full"
>
>
)}
removeHeader(index)}
className="h-6 w-6 p-0 ml-2"
>
))}
) : (
No {newServer.type === 'sse' ? 'headers' : 'additional configuration'} added
)}
{newServer.type === 'sse'
? 'HTTP headers will be sent with requests to the SSE endpoint.'
: 'Additional configuration parameters for the stdio transport.'}
)}
{/* Persistent fixed footer with buttons */}
{view === 'list' ? (
<>
Disable All
setView('add')}
size="sm"
className="gap-1.5"
>
Add Server
>
) : (
<>
Cancel
{editingServerId ? "Save Changes" : "Add Server"}
>
)}
);
};
```
## /components/message.tsx
```tsx path="/components/message.tsx"
"use client";
import type { Message as TMessage } from "ai";
import { AnimatePresence, motion } from "motion/react";
import { memo, useCallback, useEffect, useState } from "react";
import equal from "fast-deep-equal";
import { Markdown } from "./markdown";
import { cn } from "@/lib/utils";
import { ChevronDownIcon, ChevronUpIcon, LightbulbIcon, BrainIcon } from "lucide-react";
import { SpinnerIcon } from "./icons";
import { ToolInvocation } from "./tool-invocation";
import { CopyButton } from "./copy-button";
interface ReasoningPart {
type: "reasoning";
reasoning: string;
details: Array<{ type: "text"; text: string }>;
}
interface ReasoningMessagePartProps {
part: ReasoningPart;
isReasoning: boolean;
}
export function ReasoningMessagePart({
part,
isReasoning,
}: ReasoningMessagePartProps) {
const [isExpanded, setIsExpanded] = useState(false);
const memoizedSetIsExpanded = useCallback((value: boolean) => {
setIsExpanded(value);
}, []);
useEffect(() => {
memoizedSetIsExpanded(isReasoning);
}, [isReasoning, memoizedSetIsExpanded]);
return (
{isReasoning ? (
) : (
setIsExpanded(!isExpanded)}
className={cn(
"flex items-center justify-between w-full",
"rounded-md py-2 px-3 mb-0.5",
"bg-muted/50 border border-border/60 hover:border-border/80",
"transition-all duration-150 cursor-pointer",
isExpanded ? "bg-muted border-primary/20" : ""
)}
>
Reasoning
(click to {isExpanded ? "hide" : "view"})
{isExpanded ? (
) : (
)}
)}
{isExpanded && (
The assistant's thought process:
{part.details.map((detail, detailIndex) =>
detail.type === "text" ? (
{detail.text}
) : (
""
),
)}
)}
);
}
const PurePreviewMessage = ({
message,
isLatestMessage,
status,
}: {
message: TMessage;
isLoading: boolean;
status: "error" | "submitted" | "streaming" | "ready";
isLatestMessage: boolean;
}) => {
// Create a string with all text parts for copy functionality
const getMessageText = () => {
if (!message.parts) return "";
return message.parts
.filter(part => part.type === "text")
.map(part => (part.type === "text" ? part.text : ""))
.join("\n\n");
};
// Only show copy button if the message is from the assistant and not currently streaming
const shouldShowCopyButton = message.role === "assistant" && (!isLatestMessage || status !== "streaming");
return (
{message.parts?.map((part, i) => {
switch (part.type) {
case "text":
return (
{part.text}
);
case "tool-invocation":
const { toolName, state, args } = part.toolInvocation;
const result = 'result' in part.toolInvocation ? part.toolInvocation.result : null;
return (
);
case "reasoning":
return (
);
default:
return null;
}
})}
{shouldShowCopyButton && (
)}
);
};
export const Message = memo(PurePreviewMessage, (prevProps, nextProps) => {
if (prevProps.status !== nextProps.status) return false;
if (prevProps.message.annotations !== nextProps.message.annotations)
return false;
if (!equal(prevProps.message.parts, nextProps.message.parts)) return false;
return true;
});
```
## /components/messages.tsx
```tsx path="/components/messages.tsx"
import type { Message as TMessage } from "ai";
import { Message } from "./message";
import { useScrollToBottom } from "@/lib/hooks/use-scroll-to-bottom";
export const Messages = ({
messages,
isLoading,
status,
}: {
messages: TMessage[];
isLoading: boolean;
status: "error" | "submitted" | "streaming" | "ready";
}) => {
const [containerRef, endRef] = useScrollToBottom();
return (
{messages.map((m, i) => (
))}
);
};
```
## /components/model-picker.tsx
```tsx path="/components/model-picker.tsx"
"use client";
import { MODELS, modelDetails, type modelID, defaultModel } from "@/ai/providers";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "./ui/select";
import { cn } from "@/lib/utils";
import { Sparkles, Zap, Info, Bolt, Code, Brain, Lightbulb, Image, Gauge, Rocket, Bot } from "lucide-react";
import { useState, useEffect } from "react";
interface ModelPickerProps {
selectedModel: modelID;
setSelectedModel: (model: modelID) => void;
}
export const ModelPicker = ({ selectedModel, setSelectedModel }: ModelPickerProps) => {
const [hoveredModel, setHoveredModel] = useState(null);
// Ensure we always have a valid model ID
const validModelId = MODELS.includes(selectedModel) ? selectedModel : defaultModel;
// If the selected model is invalid, update it to the default
useEffect(() => {
if (selectedModel !== validModelId) {
setSelectedModel(validModelId as modelID);
}
}, [selectedModel, validModelId, setSelectedModel]);
// Function to get the appropriate icon for each provider
const getProviderIcon = (provider: string) => {
switch (provider.toLowerCase()) {
case 'anthropic':
return ;
case 'openai':
return ;
case 'google':
return ;
case 'groq':
return ;
case 'xai':
return ;
default:
return ;
}
};
// Function to get capability icon
const getCapabilityIcon = (capability: string) => {
switch (capability.toLowerCase()) {
case 'code':
return
;
case 'reasoning':
return ;
case 'research':
return ;
case 'vision':
return ;
case 'fast':
case 'rapid':
return ;
case 'efficient':
case 'compact':
return ;
case 'creative':
case 'balance':
return ;
case 'agentic':
return ;
default:
return ;
}
};
// Get capability badge color
const getCapabilityColor = (capability: string) => {
switch (capability.toLowerCase()) {
case 'code':
return "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300";
case 'reasoning':
case 'research':
return "bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300";
case 'vision':
return "bg-indigo-100 text-indigo-800 dark:bg-indigo-900/30 dark:text-indigo-300";
case 'fast':
case 'rapid':
return "bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300";
case 'efficient':
case 'compact':
return "bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-300";
case 'creative':
case 'balance':
return "bg-rose-100 text-rose-800 dark:bg-rose-900/30 dark:text-rose-300";
case 'agentic':
return "bg-cyan-100 text-cyan-800 dark:bg-cyan-900/30 dark:text-cyan-300";
default:
return "bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300";
}
};
// Get current model details to display
const displayModelId = hoveredModel || validModelId;
const currentModelDetails = modelDetails[displayModelId];
// Handle model change
const handleModelChange = (modelId: string) => {
if (MODELS.includes(modelId)) {
const typedModelId = modelId as modelID;
setSelectedModel(typedModelId);
}
};
return (
{getProviderIcon(modelDetails[validModelId].provider)}
{modelDetails[validModelId].name}
{/* Model selector column */}
{MODELS.map((id) => {
const modelId = id as modelID;
return (
setHoveredModel(modelId)}
onMouseLeave={() => setHoveredModel(null)}
className={cn(
"!px-2 sm:!px-3 py-1.5 sm:py-2 cursor-pointer rounded-md text-xs transition-colors duration-150",
"hover:bg-primary/5 hover:text-primary-foreground",
"focus:bg-primary/10 focus:text-primary focus:outline-none",
"data-[highlighted]:bg-primary/10 data-[highlighted]:text-primary",
validModelId === id && "!bg-primary/15 !text-primary font-medium"
)}
>
{getProviderIcon(modelDetails[modelId].provider)}
{modelDetails[modelId].name}
{modelDetails[modelId].provider}
);
})}
{/* Model details column - hidden on smallest screens, visible on sm+ */}
{getProviderIcon(currentModelDetails.provider)}
{currentModelDetails.name}
Provider: {currentModelDetails.provider}
{/* Capability badges */}
{currentModelDetails.capabilities.map((capability) => (
{getCapabilityIcon(capability)}
{capability}
))}
{currentModelDetails.description}
API Version:
{currentModelDetails.apiVersion}
{/* Condensed model details for mobile only */}
{currentModelDetails.capabilities.slice(0, 4).map((capability) => (
{getCapabilityIcon(capability)}
{capability}
))}
{currentModelDetails.capabilities.length > 4 && (
+{currentModelDetails.capabilities.length - 4} more
)}
);
};
```
## /components/project-overview.tsx
```tsx path="/components/project-overview.tsx"
import NextLink from "next/link";
export const ProjectOverview = () => {
return (
Scira MCP Chat
);
};
const Link = ({
children,
href,
}: {
children: React.ReactNode;
href: string;
}) => {
return (
{children}
);
};
```
## /components/suggested-prompts.tsx
```tsx path="/components/suggested-prompts.tsx"
"use client";
import { motion } from "motion/react";
import { Button } from "./ui/button";
import { memo } from "react";
interface SuggestedPromptsProps {
sendMessage: (input: string) => void;
}
function PureSuggestedPrompts({ sendMessage }: SuggestedPromptsProps) {
const suggestedActions = [
{
title: "What are the advantages",
label: "of using Next.js?",
action: "What are the advantages of using Next.js?",
},
{
title: "What is the weather",
label: "in San Francisco?",
action: "What is the weather in San Francisco?",
},
];
return (
{suggestedActions.map((suggestedAction, index) => (
1 ? "hidden sm:block" : "block"}
>
{
sendMessage(suggestedAction.action);
}}
className="text-left border rounded-xl px-4 py-3.5 text-sm flex-1 gap-1 sm:flex-col w-full h-auto justify-start items-start"
>
{suggestedAction.title}
{suggestedAction.label}
))}
);
}
export const SuggestedPrompts = memo(PureSuggestedPrompts, () => true);
```
## /components/textarea.tsx
```tsx path="/components/textarea.tsx"
import { modelID } from "@/ai/providers";
import { Textarea as ShadcnTextarea } from "@/components/ui/textarea";
import { ArrowUp, Loader2 } from "lucide-react";
import { ModelPicker } from "./model-picker";
interface InputProps {
input: string;
handleInputChange: (event: React.ChangeEvent) => void;
isLoading: boolean;
status: string;
stop: () => void;
selectedModel: modelID;
setSelectedModel: (model: modelID) => void;
}
export const Textarea = ({
input,
handleInputChange,
isLoading,
status,
stop,
selectedModel,
setSelectedModel,
}: InputProps) => {
const isStreaming = status === "streaming" || status === "submitted";
return (
{
if (e.key === "Enter" && !e.shiftKey && !isLoading && input.trim()) {
e.preventDefault();
e.currentTarget.form?.requestSubmit();
}
}}
/>
{isStreaming ? (
) : (
)}
);
};
```
## /components/theme-provider.tsx
```tsx path="/components/theme-provider.tsx"
"use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
import { type ThemeProviderProps } from "next-themes"
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return {children}
}
```
## /components/theme-toggle.tsx
```tsx path="/components/theme-toggle.tsx"
"use client"
import * as React from "react"
import { CircleDashed, Flame, Sun } from "lucide-react"
import { useTheme } from "next-themes"
import { Button } from "./ui/button"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./ui/dropdown-menu"
import { cn } from "@/lib/utils"
export function ThemeToggle({ className, ...props }: React.ComponentProps) {
const { setTheme } = useTheme()
return (
Toggle theme
setTheme("dark")}>
Dark
setTheme("light")}>
Light
setTheme("black")}>
Black
{/* sunset theme */}
setTheme("sunset")}>
Sunset
)
}
```
## /components/tool-invocation.tsx
```tsx path="/components/tool-invocation.tsx"
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "motion/react";
import {
ChevronDownIcon,
ChevronUpIcon,
Loader2,
CheckCircle2,
TerminalSquare,
Code,
ArrowRight,
Circle,
} from "lucide-react";
import { cn } from "@/lib/utils";
interface ToolInvocationProps {
toolName: string;
state: string;
args: any;
result: any;
isLatestMessage: boolean;
status: string;
}
export function ToolInvocation({
toolName,
state,
args,
result,
isLatestMessage,
status,
}: ToolInvocationProps) {
const [isExpanded, setIsExpanded] = useState(false);
const variants = {
collapsed: {
height: 0,
opacity: 0,
},
expanded: {
height: "auto",
opacity: 1,
},
};
const getStatusIcon = () => {
if (state === "call") {
if (isLatestMessage && status !== "ready") {
return ;
}
return ;
}
return ;
};
const getStatusClass = () => {
if (state === "call") {
if (isLatestMessage && status !== "ready") {
return "text-primary";
}
return "text-muted-foreground";
}
return "text-primary";
};
const formatContent = (content: any): string => {
try {
if (typeof content === "string") {
try {
const parsed = JSON.parse(content);
return JSON.stringify(parsed, null, 2);
} catch {
return content;
}
}
return JSON.stringify(content, null, 2);
} catch {
return String(content);
}
};
return (
setIsExpanded(!isExpanded)}
>
{toolName}
{state === "call" ? (isLatestMessage && status !== "ready" ? "Running" : "Waiting") : "Completed"}
{getStatusIcon()}
{isExpanded ? (
) : (
)}
{isExpanded && (
{!!args && (
Arguments
{formatContent(args)}
)}
{!!result && (
)}
)}
);
}
```
## /components/ui/accordion.tsx
```tsx path="/components/ui/accordion.tsx"
"use client"
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDownIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Accordion({
...props
}: React.ComponentProps) {
return
}
function AccordionItem({
className,
...props
}: React.ComponentProps) {
return (
)
}
function AccordionTrigger({
className,
children,
...props
}: React.ComponentProps) {
return (
svg]:rotate-180",
className
)}
{...props}
>
{children}
)
}
function AccordionContent({
className,
children,
...props
}: React.ComponentProps) {
return (
{children}
)
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
```
## /components/ui/avatar.tsx
```tsx path="/components/ui/avatar.tsx"
"use client"
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
function Avatar({
className,
...props
}: React.ComponentProps) {
return (
)
}
function AvatarImage({
className,
...props
}: React.ComponentProps) {
return (
)
}
function AvatarFallback({
className,
...props
}: React.ComponentProps) {
return (
)
}
export { Avatar, AvatarImage, AvatarFallback }
```
## /components/ui/badge.tsx
```tsx path="/components/ui/badge.tsx"
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
return (
)
}
export { Badge, badgeVariants }
```
## /components/ui/button.tsx
```tsx path="/components/ui/button.tsx"
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
)
}
export { Button, buttonVariants }
```
## /components/ui/dialog.tsx
```tsx path="/components/ui/dialog.tsx"
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Dialog({
...props
}: React.ComponentProps) {
return
}
function DialogTrigger({
...props
}: React.ComponentProps) {
return
}
function DialogPortal({
...props
}: React.ComponentProps) {
return
}
function DialogClose({
...props
}: React.ComponentProps) {
return
}
function DialogOverlay({
className,
...props
}: React.ComponentProps) {
return (
)
}
function DialogContent({
className,
children,
...props
}: React.ComponentProps) {
return (
{children}
Close
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps) {
return (
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps) {
return (
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}
```
## /components/ui/dropdown-menu.tsx
```tsx path="/components/ui/dropdown-menu.tsx"
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function DropdownMenu({
...props
}: React.ComponentProps) {
return
}
function DropdownMenuPortal({
...props
}: React.ComponentProps) {
return (
)
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps) {
return (
)
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps) {
return (
)
}
function DropdownMenuGroup({
...props
}: React.ComponentProps) {
return (
)
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps) {
return (
{children}
)
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps) {
return (
)
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps) {
return (
{children}
)
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps & {
inset?: boolean
}) {
return (
)
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps) {
return (
)
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
)
}
function DropdownMenuSub({
...props
}: React.ComponentProps) {
return
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps & {
inset?: boolean
}) {
return (
{children}
)
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps) {
return (
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}
```
## /components/ui/input.tsx
```tsx path="/components/ui/input.tsx"
import * as React from "react"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
)
}
export { Input }
```
## /components/ui/label.tsx
```tsx path="/components/ui/label.tsx"
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cn } from "@/lib/utils"
function Label({
className,
...props
}: React.ComponentProps) {
return (
)
}
export { Label }
```
## /components/ui/popover.tsx
```tsx path="/components/ui/popover.tsx"
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
function Popover({
...props
}: React.ComponentProps) {
return
}
function PopoverTrigger({
...props
}: React.ComponentProps) {
return
}
function PopoverContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps) {
return (
)
}
function PopoverAnchor({
...props
}: React.ComponentProps) {
return
}
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
```
## /components/ui/scroll-area.tsx
```tsx path="/components/ui/scroll-area.tsx"
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
function ScrollArea({
className,
children,
...props
}: React.ComponentProps) {
return (
{children}
)
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps) {
return (
)
}
export { ScrollArea, ScrollBar }
```
## /components/ui/select.tsx
```tsx path="/components/ui/select.tsx"
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Select({
...props
}: React.ComponentProps) {
return
}
function SelectGroup({
...props
}: React.ComponentProps) {
return
}
function SelectValue({
...props
}: React.ComponentProps) {
return
}
function SelectTrigger({
className,
children,
...props
}: React.ComponentProps) {
return (
{children}
)
}
function SelectContent({
className,
children,
position = "popper",
...props
}: React.ComponentProps) {
return (
{children}
)
}
function SelectLabel({
className,
...props
}: React.ComponentProps) {
return (
)
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps) {
return (
{children}
)
}
function SelectSeparator({
className,
...props
}: React.ComponentProps) {
return (
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps) {
return (
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps) {
return (
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}
```
## /components/ui/separator.tsx
```tsx path="/components/ui/separator.tsx"
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps) {
return (
)
}
export { Separator }
```
## /components/ui/sheet.tsx
```tsx path="/components/ui/sheet.tsx"
"use client"
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Sheet({ ...props }: React.ComponentProps) {
return
}
function SheetTrigger({
...props
}: React.ComponentProps) {
return
}
function SheetClose({
...props
}: React.ComponentProps) {
return
}
function SheetPortal({
...props
}: React.ComponentProps) {
return
}
function SheetOverlay({
className,
...props
}: React.ComponentProps) {
return (
)
}
function SheetContent({
className,
children,
side = "right",
...props
}: React.ComponentProps & {
side?: "top" | "right" | "bottom" | "left"
}) {
return (
{children}
Close
)
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
)
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
)
}
function SheetTitle({
className,
...props
}: React.ComponentProps) {
return (
)
}
function SheetDescription({
className,
...props
}: React.ComponentProps) {
return (
)
}
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}
```
## /components/ui/sidebar.tsx
```tsx path="/components/ui/sidebar.tsx"
"use client"
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { VariantProps, cva } from "class-variance-authority"
import { PanelLeftIcon } from "lucide-react"
import { useIsMobile } from "@/hooks/use-mobile"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet"
import { Skeleton } from "@/components/ui/skeleton"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
const SIDEBAR_COOKIE_NAME = "sidebar_state"
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
const SIDEBAR_WIDTH = "16rem"
const SIDEBAR_WIDTH_MOBILE = "18rem"
const SIDEBAR_WIDTH_ICON = "3rem"
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
type SidebarContextProps = {
state: "expanded" | "collapsed"
open: boolean
setOpen: (open: boolean) => void
openMobile: boolean
setOpenMobile: (open: boolean) => void
isMobile: boolean
toggleSidebar: () => void
}
const SidebarContext = React.createContext(null)
function useSidebar() {
const context = React.useContext(SidebarContext)
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.")
}
return context
}
function SidebarProvider({
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
className,
style,
children,
...props
}: React.ComponentProps<"div"> & {
defaultOpen?: boolean
open?: boolean
onOpenChange?: (open: boolean) => void
}) {
const isMobile = useIsMobile()
const [openMobile, setOpenMobile] = React.useState(false)
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen)
const open = openProp ?? _open
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value
if (setOpenProp) {
setOpenProp(openState)
} else {
_setOpen(openState)
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
},
[setOpenProp, open]
)
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
}, [isMobile, setOpen, setOpenMobile])
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault()
toggleSidebar()
}
}
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [toggleSidebar])
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed"
const contextValue = React.useMemo(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
)
return (
{children}
)
}
function Sidebar({
side = "left",
variant = "sidebar",
collapsible = "offcanvas",
className,
children,
...props
}: React.ComponentProps<"div"> & {
side?: "left" | "right"
variant?: "sidebar" | "floating" | "inset"
collapsible?: "offcanvas" | "icon" | "none"
}) {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
if (collapsible === "none") {
return (
{children}
)
}
if (isMobile) {
return (
Sidebar
Displays the mobile sidebar.
{children}
)
}
return (
{/* This is what handles the sidebar gap on desktop */}
)
}
function SidebarTrigger({
className,
onClick,
...props
}: React.ComponentProps) {
const { toggleSidebar } = useSidebar()
return (
{
onClick?.(event)
toggleSidebar()
}}
{...props}
>
Toggle Sidebar
)
}
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
const { toggleSidebar } = useSidebar()
return (
)
}
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
return (
)
}
function SidebarInput({
className,
...props
}: React.ComponentProps) {
return (
)
}
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
)
}
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
)
}
function SidebarSeparator({
className,
...props
}: React.ComponentProps) {
return (
)
}
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
return (
)
}
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
)
}
function SidebarGroupLabel({
className,
asChild = false,
...props
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "div"
return (
svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className
)}
{...props}
/>
)
}
function SidebarGroupAction({
className,
asChild = false,
...props
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "button"
return (
svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
}
function SidebarGroupContent({
className,
...props
}: React.ComponentProps<"div">) {
return (
)
}
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
return (
)
}
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
return (
)
}
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
},
size: {
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function SidebarMenuButton({
asChild = false,
isActive = false,
variant = "default",
size = "default",
tooltip,
className,
...props
}: React.ComponentProps<"button"> & {
asChild?: boolean
isActive?: boolean
tooltip?: string | React.ComponentProps
} & VariantProps) {
const Comp = asChild ? Slot : "button"
const { isMobile, state } = useSidebar()
const button = (
)
if (!tooltip) {
return button
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
}
}
return (
{button}
)
}
function SidebarMenuAction({
className,
asChild = false,
showOnHover = false,
...props
}: React.ComponentProps<"button"> & {
asChild?: boolean
showOnHover?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
showOnHover &&
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
className
)}
{...props}
/>
)
}
function SidebarMenuBadge({
className,
...props
}: React.ComponentProps<"div">) {
return (
)
}
function SidebarMenuSkeleton({
className,
showIcon = false,
...props
}: React.ComponentProps<"div"> & {
showIcon?: boolean
}) {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`
}, [])
return (
{showIcon && (
)}
)
}
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
return (
)
}
function SidebarMenuSubItem({
className,
...props
}: React.ComponentProps<"li">) {
return (
)
}
function SidebarMenuSubButton({
asChild = false,
size = "md",
isActive = false,
className,
...props
}: React.ComponentProps<"a"> & {
asChild?: boolean
size?: "sm" | "md"
isActive?: boolean
}) {
const Comp = asChild ? Slot : "a"
return (
svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
}
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
}
```
## /components/ui/skeleton.tsx
```tsx path="/components/ui/skeleton.tsx"
import { cn } from "@/lib/utils"
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
)
}
export { Skeleton }
```
## /components/ui/sonner.tsx
```tsx path="/components/ui/sonner.tsx"
"use client"
import { useTheme } from "next-themes"
import { Toaster as Sonner, ToasterProps } from "sonner"
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
)
}
export { Toaster }
```
## /components/ui/text-morph.tsx
```tsx path="/components/ui/text-morph.tsx"
'use client';
import { cn } from '@/lib/utils';
import { AnimatePresence, motion, Transition, Variants } from 'motion/react';
import { useMemo, useId } from 'react';
export type TextMorphProps = {
children: string;
as?: React.ElementType;
className?: string;
style?: React.CSSProperties;
variants?: Variants;
transition?: Transition;
};
export function TextMorph({
children,
as: Component = 'p',
className,
style,
variants,
transition,
}: TextMorphProps) {
const uniqueId = useId();
const characters = useMemo(() => {
const charCounts: Record = {};
return children.split('').map((char) => {
const lowerChar = char.toLowerCase();
charCounts[lowerChar] = (charCounts[lowerChar] || 0) + 1;
return {
id: `${uniqueId}-${lowerChar}${charCounts[lowerChar]}`,
label: char === ' ' ? '\u00A0' : char,
};
});
}, [children, uniqueId]);
const defaultVariants: Variants = {
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 },
};
const defaultTransition: Transition = {
type: 'spring',
stiffness: 280,
damping: 18,
mass: 0.3,
};
return (
{characters.map((character) => (
{character.label}
))}
);
}
```
## /components/ui/textarea.tsx
```tsx path="/components/ui/textarea.tsx"
import * as React from "react"
import { cn } from "@/lib/utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
)
}
export { Textarea }
```
## /components/ui/tooltip.tsx
```tsx path="/components/ui/tooltip.tsx"
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps) {
return (
)
}
function Tooltip({
...props
}: React.ComponentProps) {
return (
)
}
function TooltipTrigger({
...props
}: React.ComponentProps) {
return
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps) {
return (
{children}
)
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
```
## /drizzle.config.ts
```ts path="/drizzle.config.ts"
import type { Config } from "drizzle-kit";
import dotenv from "dotenv";
// Load environment variables
dotenv.config({ path: ".env.local" });
export default {
schema: "./lib/db/schema.ts",
out: "./drizzle",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
} satisfies Config;
```
## /drizzle/0000_supreme_rocket_raccoon.sql
```sql path="/drizzle/0000_supreme_rocket_raccoon.sql"
CREATE TABLE "chats" (
"id" text PRIMARY KEY NOT NULL,
"title" text DEFAULT 'New Chat' NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "messages" (
"id" text PRIMARY KEY NOT NULL,
"chat_id" text NOT NULL,
"content" text NOT NULL,
"role" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "messages" ADD CONSTRAINT "messages_chat_id_chats_id_fk" FOREIGN KEY ("chat_id") REFERENCES "public"."chats"("id") ON DELETE cascade ON UPDATE no action;
```
## /drizzle/0001_curious_paper_doll.sql
```sql path="/drizzle/0001_curious_paper_doll.sql"
CREATE TABLE "users" (
"id" text PRIMARY KEY NOT NULL,
"client_id" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "users_client_id_unique" UNIQUE("client_id")
);
--> statement-breakpoint
ALTER TABLE "chats" ADD COLUMN "user_id" text;--> statement-breakpoint
ALTER TABLE "chats" ADD CONSTRAINT "chats_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
```
## /drizzle/0002_free_cobalt_man.sql
```sql path="/drizzle/0002_free_cobalt_man.sql"
CREATE TABLE "steps" (
"id" text PRIMARY KEY NOT NULL,
"message_id" text NOT NULL,
"step_type" text NOT NULL,
"text" text,
"reasoning" text,
"finish_reason" text,
"created_at" timestamp DEFAULT now() NOT NULL,
"tool_calls" json,
"tool_results" json
);
--> statement-breakpoint
ALTER TABLE "users" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
DROP TABLE "users" CASCADE;--> statement-breakpoint
ALTER TABLE "chats" DROP CONSTRAINT "chats_user_id_users_id_fk";
--> statement-breakpoint
ALTER TABLE "chats" ALTER COLUMN "user_id" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "messages" ADD COLUMN "reasoning" text;--> statement-breakpoint
ALTER TABLE "messages" ADD COLUMN "tool_calls" json;--> statement-breakpoint
ALTER TABLE "messages" ADD COLUMN "tool_results" json;--> statement-breakpoint
ALTER TABLE "messages" ADD COLUMN "has_tool_use" boolean DEFAULT false;--> statement-breakpoint
ALTER TABLE "steps" ADD CONSTRAINT "steps_message_id_messages_id_fk" FOREIGN KEY ("message_id") REFERENCES "public"."messages"("id") ON DELETE cascade ON UPDATE no action;
```
## /drizzle/0003_oval_energizer.sql
```sql path="/drizzle/0003_oval_energizer.sql"
ALTER TABLE "steps" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
DROP TABLE "steps" CASCADE;--> statement-breakpoint
ALTER TABLE "messages" ALTER COLUMN "tool_calls" SET DATA TYPE jsonb;--> statement-breakpoint
ALTER TABLE "messages" ALTER COLUMN "tool_results" SET DATA TYPE jsonb;--> statement-breakpoint
ALTER TABLE "messages" ADD COLUMN "step_type" text;--> statement-breakpoint
ALTER TABLE "messages" ADD COLUMN "finish_reason" text;--> statement-breakpoint
ALTER TABLE "messages" DROP COLUMN "has_tool_use";
```
## /drizzle/0004_tense_ricochet.sql
```sql path="/drizzle/0004_tense_ricochet.sql"
ALTER TABLE "messages" DROP COLUMN "reasoning";--> statement-breakpoint
ALTER TABLE "messages" DROP COLUMN "tool_calls";--> statement-breakpoint
ALTER TABLE "messages" DROP COLUMN "tool_results";--> statement-breakpoint
ALTER TABLE "messages" DROP COLUMN "step_type";--> statement-breakpoint
ALTER TABLE "messages" DROP COLUMN "finish_reason";
```
## /drizzle/0005_early_payback.sql
```sql path="/drizzle/0005_early_payback.sql"
ALTER TABLE "messages" ADD COLUMN "parts" json NOT NULL;--> statement-breakpoint
ALTER TABLE "messages" DROP COLUMN "content";
```
The content has been capped at 50000 tokens, and files over NaN bytes have been omitted. The user could consider applying other filters to refine the result. The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.