Skip to content
This repository was archived by the owner on Feb 19, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,5 @@ yarn-error.log*
/playwright-report/
/blob-report/
/playwright/*
tsconfig.tsbuildinfo
tsconfig.tsbuildinfo
docs/composio-toolkit-metadata.md
18 changes: 17 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
This is a Next.js AI chatbot application using:

### Core Stack

- **Next.js 15** with App Router and React Server Components
- **AI SDK** for LLM integration with Anthropic Claude models
- **Drizzle ORM** with PostgreSQL for data persistence
- **Auth.js** for authentication
- **shadcn/ui** components with Tailwind CSS

### Project Structure

- `/app/(auth)/` - Authentication routes and components
- `/app/(chat)/` - Main chat interface and API routes
- `/artifacts/` - Different artifact types (code, image, text, sheet)
Expand All @@ -39,25 +41,29 @@ This is a Next.js AI chatbot application using:
- `/tests/` - Playwright e2e tests

### AI Integration

- Default model: `claude-4-sonnet-20250514` for chat, `claude-3-5-haiku-latest` for titles
- Configurable via `/lib/ai/providers.ts`
- Test environment uses mock models from `/lib/ai/models.test.ts`
- AI tools defined in `/lib/ai/tools/`

### Database Schema

- Uses Drizzle ORM with PostgreSQL
- Core tables: User, Chat, Message_v2 (new schema), Document, Vote, Suggestion
- Migration files in `/lib/db/migrations/`
- The Message table is deprecated in favor of Message_v2

### Key Features

- Real-time chat with artifacts (code, documents, images, spreadsheets)
- File upload and document processing
- Chat history with public/private visibility
- User voting and suggestions system
- Multi-modal input support

## Code Quality

- Uses Biome for linting and formatting
- ESLint for Next.js specific rules
- TypeScript with strict configuration
Expand All @@ -66,29 +72,39 @@ This is a Next.js AI chatbot application using:
## Development Best Practices

### Authentication Changes

- **Plan schema completely upfront** - Design all auth-related fields in single migration
- **Exclude auth routes from middleware** - Always exempt `/login`, `/register` from auth checks first
- **Use unique identifiers over emails** - OAuth provider IDs are more reliable than email addresses
- **Test complete auth flows** - Test end-to-end after each major auth change
- **Avoid type safety compromises** - Use proper optional types instead of non-null assertions

### Database Migrations

- **Single comprehensive migration over incremental** - Plan all related fields together
- **Include constraints from start** - Define unique, foreign key constraints in initial schema
- **Clean slate approach for development** - Clear test data when making major auth changes

### Middleware and Routing

- **Auth exclusions first** - Always handle public routes before implementing auth redirects
- **Avoid redirect loops** - Check middleware patterns don't redirect auth pages to themselves

### Error Handling

- **Detailed logging for auth flows** - Log profile data and validation steps for debugging
- **Graceful fallbacks** - Design for missing optional data (emails, names) from OAuth providers

### Code Quality During Development

- **Run `pnpm format` and `pnpm lint`** after implementing any granular part of functionality
- **Avoid type safety compromises** - No ts-ignores or lint ignores, use proper typing instead
- **Test incrementally** - Verify each step works before moving to the next

### Git Workflow
- Use gh cli when working with git stuff. If and when asked, make PRs using it too.

- Use gh cli when working with git stuff. If and when asked, make PRs using it too.

## Composio Documentation

- For composio related documentation, read composio-docs.md to find the relevant URLs to read the documentation from.
19 changes: 16 additions & 3 deletions app/(chat)/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { after } from 'next/server';
import type { Chat } from '@/lib/db/schema';
import { differenceInSeconds } from 'date-fns';
import { ChatSDKError } from '@/lib/errors';
import { getComposioTools } from '@/lib/ai/tools/composio';

export const maxDuration = 60;

Expand Down Expand Up @@ -69,8 +70,13 @@ export async function POST(request: Request) {
}

try {
const { id, message, selectedChatModel, selectedVisibilityType } =
requestBody;
const {
id,
message,
selectedChatModel,
selectedVisibilityType,
enabledToolkits,
} = requestBody;

const session = await auth();

Expand Down Expand Up @@ -142,7 +148,13 @@ export async function POST(request: Request) {
await createStreamId({ streamId, chatId: id });

const stream = createDataStream({
execute: (dataStream) => {
execute: async (dataStream) => {
// Fetch Composio tools if toolkits are enabled
const composioTools = await getComposioTools(
session.user.id,
enabledToolkits || [],
);

const result = streamText({
model: myProvider.languageModel(selectedChatModel),
system: systemPrompt({ selectedChatModel, requestHints }),
Expand All @@ -154,6 +166,7 @@ export async function POST(request: Request) {
experimental_generateMessageId: generateUUID,
tools: {
getWeather,
...composioTools,
},
onFinish: async ({ response }) => {
if (session.user?.id) {
Expand Down
1 change: 1 addition & 0 deletions app/(chat)/api/chat/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const postRequestBodySchema = z.object({
}),
selectedChatModel: z.enum(['chat-model', 'chat-model-reasoning']),
selectedVisibilityType: z.enum(['public', 'private']),
enabledToolkits: z.array(z.string()).optional(),
});

export type PostRequestBody = z.infer<typeof postRequestBodySchema>;
17 changes: 13 additions & 4 deletions app/(chat)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { cookies } from 'next/headers';

import { AppSidebar } from '@/components/app-sidebar';
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
import { ToolBar, ToolbarProvider, ToolbarRail } from '@/components/toolbar';
import { auth } from '../(auth)/auth';
import Script from 'next/script';

Expand All @@ -21,10 +22,18 @@ export default async function Layout({
src="https://cdn.jsdelivr.net/pyodide/v0.23.4/full/pyodide.js"
strategy="beforeInteractive"
/>
<SidebarProvider defaultOpen={!isCollapsed}>
<AppSidebar user={session?.user} />
<SidebarInset>{children}</SidebarInset>
</SidebarProvider>
<ToolbarProvider>
<div className="flex min-h-svh w-full">
<SidebarProvider defaultOpen={!isCollapsed}>
<AppSidebar user={session?.user} />
<SidebarInset className="flex-1">
{children}
<ToolbarRail />
</SidebarInset>
</SidebarProvider>
<ToolBar />
</div>
</ToolbarProvider>
</>
);
}
57 changes: 57 additions & 0 deletions app/api/connections/initiate/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { NextResponse } from 'next/server';
import { auth } from '@/app/(auth)/auth';
import composio from '@/lib/services/composio';
import { initiateConnectionSchema } from './schema';
import { ChatSDKError } from '@/lib/errors';
// ConnectionRequest type is not exported from @composio/core
type ConnectionRequestResponse = {
id: string;
redirectUrl?: string | null;
};

export async function POST(request: Request) {
const session = await auth();

if (!session?.user?.id) {
return new ChatSDKError('unauthorized:chat').toResponse();
}

let requestBody: { authConfigId: string };

try {
const json = await request.json();
requestBody = initiateConnectionSchema.parse(json);
} catch (_) {
return new ChatSDKError(
'bad_request:api',
'Invalid request body',
).toResponse();
}

try {
const { authConfigId } = requestBody;

// Initiate connection with Composio
const connectionRequest = (await composio.connectedAccounts.initiate(
session.user.id,
authConfigId,
)) as ConnectionRequestResponse;

return NextResponse.json({
redirectUrl: connectionRequest.redirectUrl,
connectionId: connectionRequest.id,
});
} catch (error) {
console.error('Failed to initiate connection:', error);

if (error instanceof Error && 'code' in error) {
// Handle Composio specific errors
return NextResponse.json({ error: error.message }, { status: 400 });
}

return NextResponse.json(
{ error: 'Failed to initiate connection' },
{ status: 500 },
);
}
}
9 changes: 9 additions & 0 deletions app/api/connections/initiate/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { z } from 'zod';

export const initiateConnectionSchema = z.object({
authConfigId: z.string().min(1),
});

export type InitiateConnectionRequest = z.infer<
typeof initiateConnectionSchema
>;
93 changes: 93 additions & 0 deletions app/api/toolkits/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { NextResponse } from 'next/server';
import { auth } from '@/app/(auth)/auth';
import composio from '@/lib/services/composio';
import { ChatSDKError } from '@/lib/errors';

// Using custom types since they're not exported from @composio/core
type ToolkitResponse = {
name: string;
slug: string;
meta?: {
description?: string;
logo?: string;
categories?: Array<{
name: string;
slug: string;
}>;
};
};

type ConnectedAccount = {
id: string;
toolkit: {
slug: string;
};
};

// Hardcoded list of supported toolkits
const SUPPORTED_TOOLKITS = [
'GMAIL',
'GOOGLECALENDAR',
'GITHUB',
'NOTION',
'SLACK',
'LINEAR',
];

export async function GET() {
const session = await auth();

if (!session?.user?.id) {
return new ChatSDKError('unauthorized:chat').toResponse();
}

try {
// Fetch connected accounts for the user
const connectedToolkitSlugs: Set<string> = new Set();

try {
const connectedAccounts = await composio.connectedAccounts.list({
userIds: [session.user.id],
});

// Extract toolkit slugs from connected accounts
connectedAccounts.items.forEach((account: ConnectedAccount) => {
if (account.toolkit?.slug) {
connectedToolkitSlugs.add(account.toolkit.slug.toUpperCase());
}
});
} catch (error) {
console.error('Failed to fetch connected accounts:', error);
// Continue without connection status if this fails
}

// Fetch all toolkits in parallel
const toolkitPromises = SUPPORTED_TOOLKITS.map(async (slug) => {
try {
const toolkit = (await composio.toolkits.get(slug)) as ToolkitResponse;
return {
name: toolkit.name,
slug: toolkit.slug,
description: toolkit.meta?.description,
logo: toolkit.meta?.logo,
categories: toolkit.meta?.categories,
isConnected: connectedToolkitSlugs.has(slug.toUpperCase()),
};
} catch (error) {
console.error(`Failed to fetch toolkit ${slug}:`, error);
return null;
}
});

const results = await Promise.all(toolkitPromises);
const toolkits = results.filter((t) => t !== null);

return NextResponse.json({ toolkits });
} catch (error) {
console.error('Failed to fetch toolkits:', error);
return NextResponse.json(
{ error: 'Failed to fetch toolkits' },
{ status: 500 },
);
}
}
7 changes: 7 additions & 0 deletions app/api/toolkits/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { z } from 'zod';

export const toolkitsRequestSchema = z.object({
toolkitSlugs: z.array(z.string()).min(1),
});

export type ToolkitsRequest = z.infer<typeof toolkitsRequestSchema>;
18 changes: 3 additions & 15 deletions components/chat-header.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
'use client';

import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useWindowSize } from 'usehooks-ts';

import { ModelSelector } from '@/components/model-selector';
import { SidebarToggle } from '@/components/sidebar-toggle';
import { Button } from '@/components/ui/button';
import { PlusIcon, VercelIcon } from './icons';
import { PlusIcon } from './icons';
import { useSidebar } from './ui/sidebar';
import { memo } from 'react';
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
import { type VisibilityType, VisibilitySelector } from './visibility-selector';
import type { Session } from 'next-auth';
import { ToolBarTrigger } from './toolbar';

function PureChatHeader({
chatId,
Expand Down Expand Up @@ -71,18 +70,7 @@ function PureChatHeader({
/>
)}

<Button
className="bg-zinc-900 dark:bg-zinc-100 hover:bg-zinc-800 dark:hover:bg-zinc-200 text-zinc-50 dark:text-zinc-900 hidden md:flex py-1.5 px-2 h-fit md:h-[34px] order-4 md:ml-auto"
asChild
>
<Link
href={`https://vercel.com/new/clone?repository-url=https://github.com/vercel/ai-chatbot&env=AUTH_SECRET&envDescription=Learn more about how to get the API Keys for the application&envLink=https://github.com/vercel/ai-chatbot/blob/main/.env.example&demo-title=AI Chatbot&demo-description=An Open-Source AI Chatbot Template Built With Next.js and the AI SDK by Vercel.&demo-url=https://chat.vercel.ai&products=[{"type":"integration","protocol":"ai","productSlug":"grok","integrationSlug":"xai"},{"type":"integration","protocol":"storage","productSlug":"neon","integrationSlug":"neon"},{"type":"integration","protocol":"storage","productSlug":"upstash-kv","integrationSlug":"upstash"},{"type":"blob"}]`}
target="_noblank"
>
<VercelIcon size={16} />
Deploy with Vercel
</Link>
</Button>
<ToolBarTrigger />
</header>
);
}
Expand Down
Loading
Loading