Skip to content
Closed
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
1 change: 1 addition & 0 deletions examples/resumable-stream-chat/.dev.vars.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
OPENAI_API_KEY=your_openai_api_key_here
22 changes: 22 additions & 0 deletions examples/resumable-stream-chat/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Resumable Streams Chat Example

## Setup

1. Copy `.dev.vars.example` to `.dev.vars` and add your OpenAI API key:

```bash
cp .dev.vars.example .dev.vars
# Edit .dev.vars and add your OPENAI_API_KEY
```

2. Install dependencies:

```bash
npm install
```

3. Start the development server:

```bash
npm run start
```
10 changes: 10 additions & 0 deletions examples/resumable-stream-chat/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!doctype html>
<html>
<head>
<title>Resumable Chat</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/client.tsx"></script>
</body>
</html>
13 changes: 13 additions & 0 deletions examples/resumable-stream-chat/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"author": "",
"description": "Resumable Stream Chat Example",
"keywords": [],
"license": "ISC",
"private": true,
"scripts": {
"deploy": "vite build && wrangler deploy",
"start": "vite dev"
},
"type": "module",
"version": "0.0.0"
}
111 changes: 111 additions & 0 deletions examples/resumable-stream-chat/src/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import type { UIMessage as Message } from "ai";
import "./styles.css";
import { useAgentChatHttp } from "agents/use-agent-chat-http";
import { useCallback, useEffect, useRef, useState } from "react";

export default function Chat() {
const [theme, setTheme] = useState<"dark" | "light">("dark");
const [input, setInput] = useState("");
const messagesEndRef = useRef<HTMLDivElement>(null);

const scrollToBottom = useCallback(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, []);

useEffect(() => {
// Set initial theme
document.documentElement.setAttribute("data-theme", theme);
}, [theme]);

// Scroll to bottom on mount
useEffect(() => {
scrollToBottom();
}, [scrollToBottom]);

const toggleTheme = () => {
const newTheme = theme === "dark" ? "light" : "dark";
setTheme(newTheme);
document.documentElement.setAttribute("data-theme", newTheme);
};

const { messages, sendMessage, clearChatHistory } = useAgentChatHttp({
agent: "resumable-chat-agent",
enableResumableStreams: true
});

const handleSubmit = useCallback(
(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (input.trim()) {
sendMessage({ role: "user", parts: [{ type: "text", text: input }] });
setInput("");
}
},
[input, sendMessage]
);

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInput(e.target.value);
};

// Scroll to bottom when messages change
useEffect(() => {
messages.length > 0 && scrollToBottom();
}, [messages, scrollToBottom]);

return (
<>
<div className="controls-container">
<button
type="button"
onClick={toggleTheme}
className="theme-switch"
data-theme={theme}
aria-label={`Switch to ${theme === "dark" ? "light" : "dark"} mode`}
>
<div className="theme-switch-handle" />
</button>
<button
type="button"
onClick={clearChatHistory}
className="clear-history"
>
🗑️ Clear History
</button>
</div>

<div className="chat-container">
<div className="messages-wrapper">
{messages?.map((m: Message) => (
<div key={m.id} className="message">
<strong>{`${m.role}: `}</strong>
{m.parts?.map((part, i) => {
switch (part.type) {
case "text":
return (
<div key={i} className="message-content">
{part.text}
</div>
);
default:
return null;
}
})}
<br />
</div>
))}
<div ref={messagesEndRef} />
</div>

<form onSubmit={handleSubmit}>
<input
className="chat-input"
value={input}
placeholder="Say something..."
onChange={handleInputChange}
/>
</form>
</div>
</>
);
}
7 changes: 7 additions & 0 deletions examples/resumable-stream-chat/src/client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import "./styles.css";
import { createRoot } from "react-dom/client";
import App from "./app";

const root = createRoot(document.getElementById("root")!);

root.render(<App />);
46 changes: 46 additions & 0 deletions examples/resumable-stream-chat/src/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { openai } from "@ai-sdk/openai";
import { routeAgentRequest } from "agents";
import { AIHttpChatAgent } from "agents/ai-chat-agent-http";
import {
convertToModelMessages,
createUIMessageStream,
createUIMessageStreamResponse,
type StreamTextOnFinishCallback,
type ToolSet,
streamText
} from "ai";

type Env = {
OPENAI_API_KEY: string;
};

export class ResumableChatAgent extends AIHttpChatAgent<Env> {
async onChatMessage(
onFinish: StreamTextOnFinishCallback<ToolSet>,
_options?: { streamId?: string }
): Promise<Response | undefined> {
const stream = createUIMessageStream({
execute: async ({ writer }) => {
const result = streamText({
messages: convertToModelMessages(this.messages),
model: openai("gpt-4o"),
onFinish
});

writer.merge(result.toUIMessageStream());
}
});

const response = createUIMessageStreamResponse({ stream });
return response;
}
}

export default {
async fetch(request: Request, env: Env, _ctx: ExecutionContext) {
return (
(await routeAgentRequest(request, env)) ||
new Response("Not found", { status: 404 })
);
}
} satisfies ExportedHandler<Env>;
Loading
Loading