Claude
Skills
Sign in
Back

ai-chat

Included with Lifetime
$97 forever

Communication layer — streaming chat UI with Postgres persistence, session management, and composable route/renderer architecture for downstream AI skills. Use this skill when the user says "add chat", "setup AI chat", "add streaming chat", or "setup ai-chat".

Design

What this skill does


# AI Chat

Complete streaming chat with Postgres persistence, session management, and a composable architecture that downstream skills (ai-tools, ai-reasoning, ai-memory, ai-artifacts, ai-tasks, ai-generative-ui) extend via clearly marked comment slots.

## Prerequisites

- Next.js app with `src/` directory and App Router
- `ai-core` skill installed (`getModel()` available at `@/lib/ai`)
- `auth` skill installed (`withAuth` available at `@/lib/auth-guard`, Drizzle DB at `@/lib/db`)
- `docker` skill installed (PostgreSQL running)
- shadcn/ui initialized

## Installation

```bash
bun add @ai-sdk/react @phosphor-icons/react
```

## What Gets Created

```
src/
├── lib/
│   └── db/
│       └── schema/
│           └── chat.ts                      # chatSession + chatMessage tables
├── app/
│   ├── api/
│   │   └── ai/
│   │       ├── chat/
│   │       │   └── route.ts                 # POST — streaming chat (with comment slots)
│   │       └── sessions/
│   │           ├── route.ts                 # GET/POST — list/create sessions
│   │           └── [sessionId]/
│   │               └── route.ts             # GET/PATCH/DELETE — session CRUD
│   └── (app)/
│       └── chat/
│           ├── page.tsx                     # Chat page (protected route)
│           └── loading.tsx                  # Loading state
└── components/
    └── ai/
        ├── chat.tsx                         # Chat UI with useChat + DefaultChatTransport
        ├── chat-container.tsx               # Scrollable message list container
        ├── chat-sidebar.tsx                 # Session history sidebar
        ├── code-block.tsx                   # Code display with language class
        ├── loader.tsx                       # Bouncing dots loading indicator
        ├── markdown.tsx                     # Prose wrapper for markdown content
        ├── message.tsx                      # Message layout + renderer (with comment slots)
        └── prompt-input.tsx                 # Textarea + send button input
```

## Database

After applying this skill, push the schema to create the `chat_session` and `chat_message` tables:

```bash
bunx drizzle-kit push
```

## Setup Steps

### Step 1: Create `src/lib/db/schema/chat.ts`

```typescript
import {
  pgTable,
  text,
  timestamp,
  uuid,
  json,
} from "drizzle-orm/pg-core";

export const chatSession = pgTable("chat_session", {
  id: uuid("id").defaultRandom().primaryKey(),
  userId: text("user_id").notNull(),
  title: text("title").notNull().default("New Chat"),
  createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
  updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
});

export const chatMessage = pgTable("chat_message", {
  id: uuid("id").defaultRandom().primaryKey(),
  sessionId: uuid("session_id")
    .notNull()
    .references(() => chatSession.id, { onDelete: "cascade" }),
  role: text("role", { enum: ["user", "assistant", "system"] }).notNull(),
  parts: json("parts").notNull(),
  createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
});
```

### Step 2: Add chat schema to barrel export

Add the chat schema export to `src/lib/db/schema/index.ts`:

```typescript
export * from "./chat";
```

### Step 3: Create `src/app/api/ai/chat/route.ts`

This is the core streaming endpoint. It includes clearly commented insertion points for downstream skills.

```typescript
import { streamText, convertToModelMessages, type UIMessage, type ToolSet, type JSONValue } from "ai";
import { getModel } from "@/lib/ai";
import { withAuth } from "@/lib/auth-guard";
import { db } from "@/lib/db";
import { chatSession, chatMessage } from "@/lib/db/schema/chat";
import { eq, and } from "drizzle-orm";

export const POST = withAuth(async (request, { user }) => {
  const {
    messages,
    sessionId,
  }: { messages: UIMessage[]; sessionId?: string } = await request.json();

  const userId = user.id;

  // --- Resolve or create session ---
  let activeSessionId = sessionId;

  if (activeSessionId) {
    const existing = await db
      .select({ id: chatSession.id })
      .from(chatSession)
      .where(
        and(
          eq(chatSession.id, activeSessionId),
          eq(chatSession.userId, userId)
        )
      )
      .limit(1);

    if (existing.length === 0) {
      return new Response(
        JSON.stringify({ error: "Session not found" }),
        { status: 404, headers: { "Content-Type": "application/json" } }
      );
    }
  } else {
    const firstUserMessage = messages.find((m) => m.role === "user");
    const title =
      firstUserMessage?.parts
        .filter((p): p is Extract<typeof p, { type: "text" }> => p.type === "text")
        .map((p) => p.text)
        .join(" ")
        .slice(0, 100) || "New Chat";

    const [created] = await db
      .insert(chatSession)
      .values({ userId, title })
      .returning({ id: chatSession.id });
    activeSessionId = created.id;
  }

  // --- SYSTEM PROMPT (ai-chat base, ai-memory appends) ---
  const systemParts: string[] = [
    "You are a helpful assistant. Be concise and clear in your responses.",
  ];
  // [ai-memory]: append memory context here

  // --- TOOLS (ai-tools registers, ai-artifacts/tasks/memory/gen-ui add tools) ---
  const tools: ToolSet = {};
  // [ai-tools]: spread registered tools here
  // [ai-artifacts]: add createArtifact, updateArtifact
  // [ai-tasks]: add createTask, updateTask, listTasks
  // [ai-memory]: add saveMemory, recallMemory

  // --- PROVIDER OPTIONS (ai-reasoning adds thinking config) ---
  let providerOptions: Record<string, Record<string, JSONValue>> | undefined;
  // [ai-reasoning]: add anthropic.thinking config here

  // --- Persist the latest user message ---
  const lastUserMessage = messages.at(-1);
  if (lastUserMessage && lastUserMessage.role === "user") {
    await db.insert(chatMessage).values({
      sessionId: activeSessionId,
      role: "user",
      parts: lastUserMessage.parts,
    });
  }

  // --- Convert UIMessages to ModelMessages for streamText ---
  const modelMessages = await convertToModelMessages(messages);

  // --- Stream the response ---
  const result = streamText({
    model: getModel(),
    system: systemParts.join("\n\n"),
    messages: modelMessages,
    ...(Object.keys(tools).length > 0 && { tools, maxSteps: 5 }),
    ...(providerOptions !== undefined && { providerOptions }),
    async onFinish({ text }) {
      // Persist the assistant's response
      await db.insert(chatMessage).values({
        sessionId: activeSessionId,
        role: "assistant",
        parts: [{ type: "text", text }],
      });

      // Update session timestamp
      await db
        .update(chatSession)
        .set({ updatedAt: new Date() })
        .where(eq(chatSession.id, activeSessionId));
    },
  });

  const response = result.toUIMessageStreamResponse();

  // Append session ID header so client can track the session
  response.headers.set("X-Session-Id", activeSessionId);

  return response;
});
```

### Step 4: Create `src/app/api/ai/sessions/route.ts`

```typescript
import { NextResponse } from "next/server";
import { withAuth } from "@/lib/auth-guard";
import { db } from "@/lib/db";
import { chatSession } from "@/lib/db/schema/chat";
import { eq, desc } from "drizzle-orm";

type SessionListItem = {
  id: string;
  title: string;
  createdAt: Date;
  updatedAt: Date;
};

/** GET /api/ai/sessions — list all sessions for the current user */
export const GET = withAuth(async (_request, { user }) => {
  const sessions = await db
    .select({
      id: chatSession.id,
      title: chatSession.title,
      createdAt: chatSession.createdAt,
      updatedAt: chatSession.updatedAt,
    })
    .from(chatSession)
    .where(eq(chatSession.userId, user.id))
    .orderBy(desc(chatSession.updatedAt));

  return NextResponse.json<SessionListItem[]>(sessions);
});

type CreateSessionBody = { title?: string };

/** POST /api/ai/sessions — create a new session *
Files: 1
Size: 31.6 KB
Complexity: 39/100
Category: Design

Related in Design