Skip to content
Open
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
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ Desktop.ini
.claude/*.local.*
.stripe/

# Wizard-run droppings inside fixture apps — installed skills, the run cache,
# and the setup report a run leaves behind must never be committed as fixture
# state (they bias later runs into skipping install/report steps).
apps/**/.claude/skills/
apps/**/.posthog-wizard-cache/
apps/**/posthog-*-report.md

# Temporary
tmp/
temp/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Dependencies
node_modules/

# Build output
dist/

# Environment variables
.env
.env.local
.env.*.local

# Logs
*.log
npm-debug.log*

# Editor
.vscode/
.idea/
*.swp
*.swo

# OS
.DS_Store
Thumbs.db

# TypeScript cache
*.tsbuildinfo
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Simple Chat App

A minimal chat application demonstrating the Claude Agent SDK.

## Architecture

- **Frontend**: React + Vite + Tailwind CSS
- **Backend**: Node.js + Express + WebSocket (ws)
- **Agent**: Claude Agent SDK integrated directly on the server

## Running the App

```bash
cd simple-chatapp
npm install
npm run dev
```

This starts both:
- Backend server on http://localhost:3001
- Vite dev server on http://localhost:5173

Visit http://localhost:5173

## Project Structure

```
simple-chatapp/
├── client/ # React frontend
│ ├── App.tsx # Main app component
│ ├── index.tsx # Entry point
│ ├── index.html # HTML template
│ ├── globals.css # Tailwind CSS
│ ├── components/
│ │ ├── ChatList.tsx # Left sidebar with chat list
│ │ └── ChatWindow.tsx # Main chat interface
│ └── hooks/
│ └── useWebSocket.ts # WebSocket hook
├── server/
│ ├── server.ts # Express server (REST + WebSocket)
│ ├── ai-client.ts # Claude Agent SDK wrapper
│ ├── session.ts # Chat session management
│ ├── chat-store.ts # In-memory chat storage
│ └── types.ts # TypeScript types
├── package.json
├── tsconfig.json
├── vite.config.ts
├── tailwind.config.js
└── postcss.config.js
```

## API Endpoints

### REST API

- `GET /api/chats` - List all chats
- `POST /api/chats` - Create new chat
- `GET /api/chats/:id` - Get chat details
- `DELETE /api/chats/:id` - Delete chat
- `GET /api/chats/:id/messages` - Get chat messages

### WebSocket (`ws://localhost:3001/ws`)

**Client -> Server:**
- `{ type: "subscribe", chatId: string }` - Subscribe to a chat
- `{ type: "chat", chatId: string, content: string }` - Send message

**Server -> Client:**
- `{ type: "connected" }` - Connection established
- `{ type: "history", messages: [...] }` - Chat history
- `{ type: "assistant_message", content: string }` - AI response
- `{ type: "tool_use", toolName: string, toolInput: {...} }` - Tool being used
- `{ type: "result", success: boolean }` - Query complete
- `{ type: "error", error: string }` - Error occurred

## Notes

- In-memory storage (data lost on restart)
- Agent has access to: Bash, Read, Write, Edit, Glob, Grep, WebSearch, WebFetch
- Uses Vite for frontend development with hot reload
- Uses tsx for TypeScript execution on the backend
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Simple Chat App

A demo chat application using the Claude Agent SDK with a React frontend and Express backend.

![Architecture Diagram](diagram.png)

## Getting Started

### Prerequisites

- Node.js 18+
- Claude Agent SDK credentials (set `ANTHROPIC_API_KEY` environment variable)

### Installation

```bash
npm install
```

### Running

```bash
npm run dev
```

This starts both:
- **Backend** (Express + WebSocket) on http://localhost:3001
- **Frontend** (Vite + React) on http://localhost:5173

Open http://localhost:5173 in your browser.

## Production Considerations

This is an example app for demonstration purposes. For production use, consider:

1. **Isolate the Agent SDK** - Move the SDK into a separate container/service. This provides better security isolation since the agent has access to tools like Bash, file system operations, and web requests.

2. **Persistent storage** - Replace the in-memory `ChatStore` with a database. Currently all chats are lost on server restart.

3. **Transcript syncing** - For Agent Sessions to be persisted across server restarts, you'll need to persist and restore the SDK's conversation transcripts. The SDK maintains internal state for multi-turn conversations that must be synced with your storage.

4. **Authentication** - Add user authentication and authorization. Currently anyone can access any chat.

## Demo

![Demo](demo.gif)
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import { useState, useEffect, useCallback } from "react";
import useWebSocket, { ReadyState } from "react-use-websocket";
import { ChatList } from "./components/ChatList";
import { ChatWindow } from "./components/ChatWindow";

interface Chat {
id: string;
title: string;
createdAt: string;
updatedAt: string;
}

interface Message {
id: string;
role: "user" | "assistant" | "tool_use";
content: string;
timestamp: string;
toolName?: string;
toolInput?: Record<string, any>;
}

// Use relative URLs - Vite will proxy to the backend
const API_BASE = "/api";
const WS_URL = `ws://${window.location.hostname}:3001/ws`;

export default function App() {
const [chats, setChats] = useState<Chat[]>([]);
const [selectedChatId, setSelectedChatId] = useState<string | null>(null);
const [messages, setMessages] = useState<Message[]>([]);
const [isLoading, setIsLoading] = useState(false);

// Handle WebSocket messages
const handleWSMessage = useCallback((message: any) => {
switch (message.type) {
case "connected":
console.log("Connected to server");
break;

case "history":
setMessages(message.messages || []);
break;

case "user_message":
// User message already added locally
break;

case "assistant_message":
setMessages((prev) => [
...prev,
{
id: crypto.randomUUID(),
role: "assistant",
content: message.content,
timestamp: new Date().toISOString(),
},
]);
setIsLoading(false);
break;

case "tool_use":
// Add tool use to messages array so it persists
// Alternative: To show tool uses only while pending, store them in a
// separate `pendingToolUses` state and clear it on "assistant_message" or "result"
setMessages((prev) => [
...prev,
{
id: message.toolId,
role: "tool_use",
content: "",
timestamp: new Date().toISOString(),
toolName: message.toolName,
toolInput: message.toolInput,
},
]);
break;

case "result":
setIsLoading(false);
// Refresh chat list to get updated titles
fetchChats();
break;

case "error":
console.error("Server error:", message.error);
setIsLoading(false);
break;
}
}, []);

const { sendJsonMessage, readyState, lastJsonMessage } = useWebSocket(WS_URL, {
shouldReconnect: () => true,
reconnectAttempts: 10,
reconnectInterval: 3000,
});

const isConnected = readyState === ReadyState.OPEN;

// Handle incoming WebSocket messages
useEffect(() => {
if (lastJsonMessage) {
handleWSMessage(lastJsonMessage);
}
}, [lastJsonMessage, handleWSMessage]);

// Fetch all chats
const fetchChats = async () => {
try {
const res = await fetch(`${API_BASE}/chats`);
const data = await res.json();
setChats(data);
} catch (error) {
console.error("Failed to fetch chats:", error);
}
};

// Create new chat
const createChat = async () => {
try {
const res = await fetch(`${API_BASE}/chats`, {
method: "POST",
headers: { "Content-Type": "application/json" },
});
const chat = await res.json();
setChats((prev) => [chat, ...prev]);
selectChat(chat.id);
} catch (error) {
console.error("Failed to create chat:", error);
}
};

// Delete chat
const deleteChat = async (chatId: string) => {
try {
await fetch(`${API_BASE}/chats/${chatId}`, { method: "DELETE" });
setChats((prev) => prev.filter((c) => c.id !== chatId));
if (selectedChatId === chatId) {
setSelectedChatId(null);
setMessages([]);
}
} catch (error) {
console.error("Failed to delete chat:", error);
}
};

// Select a chat
const selectChat = (chatId: string) => {
setSelectedChatId(chatId);
setMessages([]);
setIsLoading(false);

// Subscribe to chat via WebSocket
sendJsonMessage({ type: "subscribe", chatId });
};

// Send a message
const handleSendMessage = (content: string) => {
if (!selectedChatId || !isConnected) return;

// Add message optimistically
setMessages((prev) => [
...prev,
{
id: crypto.randomUUID(),
role: "user",
content,
timestamp: new Date().toISOString(),
},
]);

setIsLoading(true);

// Send via WebSocket
sendJsonMessage({
type: "chat",
content,
chatId: selectedChatId,
});
};

// Initial fetch
useEffect(() => {
fetchChats();
}, []);

return (
<div className="flex h-screen">
{/* Sidebar */}
<div className="w-64 shrink-0">
<ChatList
chats={chats}
selectedChatId={selectedChatId}
onSelectChat={selectChat}
onNewChat={createChat}
onDeleteChat={deleteChat}
/>
</div>

{/* Main chat area */}
<ChatWindow
chatId={selectedChatId}
messages={messages}
isConnected={isConnected}
isLoading={isLoading}
onSendMessage={handleSendMessage}
/>
</div>
);
}
Loading