Skip to content

Commit b8f53c2

Browse files
whoiskatrinthreepointone
authored andcommitted
working streaming draft (not syncing tabs yet)
1 parent ac3bd43 commit b8f53c2

File tree

15 files changed

+1154
-717
lines changed

15 files changed

+1154
-717
lines changed

examples/http-reusable-chat/index.html

Lines changed: 4 additions & 487 deletions
Large diffs are not rendered by default.

examples/http-reusable-chat/package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,20 @@
44
"private": true,
55
"type": "module",
66
"scripts": {
7-
"dev": "wrangler dev --port 5175",
8-
"deploy": "wrangler deploy",
9-
"typecheck": "tsc --noEmit"
7+
"deploy": "vite build && wrangler deploy",
8+
"start": "vite dev"
109
},
1110
"dependencies": {
1211
"agents": "workspace:*",
1312
"ai": "^4.0.0",
1413
"@ai-sdk/openai": "^1.0.0",
15-
"hono": "^4.0.0"
14+
"hono": "^4.0.0",
15+
"nanoid": "^5.0.0",
16+
"react": "^18.0.0",
17+
"react-dom": "^18.0.0"
1618
},
1719
"devDependencies": {
1820
"@cloudflare/workers-types": "^4.20241106.0",
19-
"@types/react": "^18.0.0",
20-
"@types/react-dom": "^18.0.0",
2121
"typescript": "^5.0.0",
2222
"vite": "^5.0.0",
2323
"wrangler": "^3.0.0"
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import type { UIMessage as Message } from "ai";
2+
import "./styles.css";
3+
import { useAgentChatHttp } from "agents/use-agent-chat-http";
4+
import { useCallback, useEffect, useRef, useState } from "react";
5+
6+
export default function Chat() {
7+
const [theme, setTheme] = useState<"dark" | "light">("dark");
8+
const [input, setInput] = useState("");
9+
const messagesEndRef = useRef<HTMLDivElement>(null);
10+
11+
const scrollToBottom = useCallback(() => {
12+
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
13+
}, []);
14+
15+
useEffect(() => {
16+
// Set initial theme
17+
document.documentElement.setAttribute("data-theme", theme);
18+
}, [theme]);
19+
20+
// Scroll to bottom on mount
21+
useEffect(() => {
22+
scrollToBottom();
23+
}, [scrollToBottom]);
24+
25+
const toggleTheme = () => {
26+
const newTheme = theme === "dark" ? "light" : "dark";
27+
setTheme(newTheme);
28+
document.documentElement.setAttribute("data-theme", newTheme);
29+
};
30+
31+
const { messages, sendMessage, clearChatHistory } = useAgentChatHttp({
32+
agent: "resumable-chat-agent",
33+
enableResumableStreams: true
34+
});
35+
36+
const handleSubmit = useCallback(
37+
(e: React.FormEvent<HTMLFormElement>) => {
38+
e.preventDefault();
39+
if (input.trim()) {
40+
sendMessage({ role: "user", parts: [{ type: "text", text: input }] });
41+
setInput("");
42+
}
43+
},
44+
[input, sendMessage]
45+
);
46+
47+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
48+
setInput(e.target.value);
49+
};
50+
51+
// Scroll to bottom when messages change
52+
useEffect(() => {
53+
messages.length > 0 && scrollToBottom();
54+
}, [messages, scrollToBottom]);
55+
56+
return (
57+
<>
58+
<div className="controls-container">
59+
<button
60+
type="button"
61+
onClick={toggleTheme}
62+
className="theme-switch"
63+
data-theme={theme}
64+
aria-label={`Switch to ${theme === "dark" ? "light" : "dark"} mode`}
65+
>
66+
<div className="theme-switch-handle" />
67+
</button>
68+
<button
69+
type="button"
70+
onClick={clearChatHistory}
71+
className="clear-history"
72+
>
73+
🗑️ Clear History
74+
</button>
75+
</div>
76+
77+
<div className="chat-container">
78+
<div className="messages-wrapper">
79+
{messages?.map((m: Message) => (
80+
<div key={m.id} className="message">
81+
<strong>{`${m.role}: `}</strong>
82+
{m.parts?.map((part, i) => {
83+
switch (part.type) {
84+
case "text":
85+
return (
86+
// biome-ignore lint/suspicious/noArrayIndexKey: vibes
87+
<div key={i} className="message-content">
88+
{part.text}
89+
</div>
90+
);
91+
}
92+
})}
93+
<br />
94+
</div>
95+
))}
96+
<div ref={messagesEndRef} />
97+
</div>
98+
99+
<form onSubmit={handleSubmit}>
100+
<input
101+
className="chat-input"
102+
value={input}
103+
placeholder="Say something..."
104+
onChange={handleInputChange}
105+
/>
106+
</form>
107+
</div>
108+
</>
109+
);
110+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import "./styles.css";
2+
import { createRoot } from "react-dom/client";
3+
import App from "./app";
4+
5+
const root = createRoot(document.getElementById("root")!);
6+
7+
root.render(<App />);

examples/http-reusable-chat/src/http-chat-agent.ts

Lines changed: 0 additions & 37 deletions
This file was deleted.

examples/http-reusable-chat/src/index.ts

Lines changed: 0 additions & 18 deletions
This file was deleted.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { openai } from "@ai-sdk/openai";
2+
import { routeAgentRequest } from "agents";
3+
import { AIHttpChatAgent } from "agents/ai-chat-agent-http";
4+
import {
5+
convertToModelMessages,
6+
createUIMessageStream,
7+
createUIMessageStreamResponse,
8+
type StreamTextOnFinishCallback,
9+
type ToolSet,
10+
streamText
11+
} from "ai";
12+
13+
type Env = {
14+
OPENAI_API_KEY: string;
15+
};
16+
17+
export class ResumableChatAgent extends AIHttpChatAgent<Env> {
18+
async onChatMessage(
19+
onFinish: StreamTextOnFinishCallback<ToolSet>,
20+
options?: { streamId?: string }
21+
): Promise<Response | undefined> {
22+
const stream = createUIMessageStream({
23+
execute: async ({ writer }) => {
24+
const result = streamText({
25+
messages: convertToModelMessages(this.messages),
26+
model: openai("gpt-4o"),
27+
onFinish
28+
});
29+
30+
writer.merge(result.toUIMessageStream());
31+
}
32+
});
33+
34+
return createUIMessageStreamResponse({ stream });
35+
}
36+
}
37+
38+
export default {
39+
async fetch(request: Request, env: Env, _ctx: ExecutionContext) {
40+
return (
41+
(await routeAgentRequest(request, env)) ||
42+
new Response("Not found", { status: 404 })
43+
);
44+
}
45+
} satisfies ExportedHandler<Env>;

0 commit comments

Comments
 (0)