Skip to content

Commit 7db4252

Browse files
committed
refactor(agent-streaming): Mount React app
- refactor: Add React entry point - refactor: Update index.html with React root - chore: Delete unused script.ts
1 parent 5a0537b commit 7db4252

4 files changed

Lines changed: 202 additions & 22 deletions

File tree

agent-streaming/index.html

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,28 +12,8 @@
1212
</head>
1313

1414
<body>
15-
<div class="chat-container">
16-
<div class="chat-messages" id="chat-messages">
17-
<!-- Welcome Message -->
18-
<div class="message system-message">
19-
<div class="message-content">
20-
Agent will think out loud and show reasoning
21-
</div>
22-
</div>
23-
</div>
24-
25-
<form class="chat-input-form" id="chat-form">
26-
<input type="text" id="prompt-input" placeholder="Type a message..." autocomplete="off" required />
27-
<button type="submit" id="send-button" aria-label="Send message">
28-
<!-- SVG Send Icon -->
29-
<svg viewBox="0 0 24 24" width="24" height="24">
30-
<path fill="currentColor" d="M2,21L23,12L2,3V10L17,12L2,14V21Z" />
31-
</svg>
32-
</button>
33-
</form>
34-
</div>
35-
36-
<script type="module" src="/src/client/script.ts"></script>
15+
<div id="root" class="chat-container"></div>
16+
<script type="module" src="/src/client/main.tsx"></script>
3717
</body>
3818

3919
</html>

agent-streaming/src/client/App.tsx

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import React, { useState, useEffect, useRef } from 'react';
2+
import { ChatInput } from './components/ChatInput.js';
3+
import { ThoughtBox } from './components/ThoughtBox.js';
4+
import { MessageBubble } from './components/MessageBubble.js';
5+
import { Message, StreamChunk } from './types.js';
6+
7+
export const App: React.FC = () => {
8+
const [messages, setMessages] = useState<Message[]>([]);
9+
const [loading, setLoading] = useState(false);
10+
const messagesEndRef = useRef<HTMLDivElement>(null);
11+
12+
// Auto-scroll to bottom on messages update
13+
useEffect(() => {
14+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
15+
}, [messages]);
16+
17+
const handlePromptSubmit = async (prompt: string) => {
18+
setLoading(true);
19+
20+
const userMessage: Message = {
21+
id: crypto.randomUUID(), // Generate unique message IDs
22+
role: 'user',
23+
type: 'text',
24+
content: prompt,
25+
};
26+
27+
setMessages((prev) => [...prev, userMessage]);
28+
29+
try {
30+
const response = await fetch('/api/chat', {
31+
method: 'POST',
32+
headers: {
33+
'Content-Type': 'application/json',
34+
},
35+
body: JSON.stringify({ prompt }),
36+
});
37+
38+
if (!response.ok) {
39+
throw new Error('Network response was not ok');
40+
}
41+
if (!response.body) {
42+
throw new Error('Response body is null');
43+
}
44+
45+
const reader = response.body.getReader();
46+
const decoder = new TextDecoder();
47+
let buffer = '';
48+
49+
while (true) {
50+
const { value, done } = await reader.read();
51+
if (done) break;
52+
53+
buffer += decoder.decode(value, { stream: true });
54+
const lines = buffer.split('\n');
55+
56+
// Keep the last partial line in the buffer
57+
buffer = lines.pop() || '';
58+
59+
for (const line of lines) {
60+
const trimmed = line.trim();
61+
if (trimmed.startsWith('data: ')) {
62+
const jsonStr = trimmed.slice(6);
63+
try {
64+
const chunk = JSON.parse(jsonStr) as StreamChunk;
65+
handleStreamChunk(chunk);
66+
} catch (err) {
67+
console.error('Failed to parse SSE JSON:', err);
68+
}
69+
}
70+
}
71+
}
72+
} catch (error) {
73+
console.error('Streaming error:', error);
74+
const errorMessage: Message = {
75+
id: crypto.randomUUID(),
76+
role: 'system',
77+
type: 'error',
78+
content: 'Failed to connect to the agent. Please try again.',
79+
};
80+
setMessages((prev) => [...prev, errorMessage]);
81+
} finally {
82+
setLoading(false);
83+
}
84+
};
85+
86+
const handleStreamChunk = (chunk: StreamChunk) => {
87+
if (chunk.type === 'thought') {
88+
const stepName = chunk.currentStep || 'Thinking';
89+
const content = chunk.content || '';
90+
91+
setMessages((prev) => {
92+
const last = prev[prev.length - 1];
93+
if (last && last.role === 'model' && last.type === 'thought') {
94+
// Update the active thought message
95+
const updated: Message = {
96+
...last,
97+
content: last.content + content,
98+
stepName: chunk.currentStep || last.stepName,
99+
};
100+
return [...prev.slice(0, -1), updated];
101+
} else {
102+
// Create a new thought message
103+
const newThought: Message = {
104+
id: crypto.randomUUID(),
105+
role: 'model',
106+
type: 'thought',
107+
content: content,
108+
stepName: stepName,
109+
};
110+
return [...prev, newThought];
111+
}
112+
});
113+
} else if (chunk.type === 'text') {
114+
const content = chunk.content || '';
115+
116+
setMessages((prev) => {
117+
const last = prev[prev.length - 1];
118+
if (last && last.role === 'model' && last.type === 'text') {
119+
// Update the active text response
120+
const updated: Message = {
121+
...last,
122+
content: last.content + content,
123+
};
124+
return [...prev.slice(0, -1), updated];
125+
} else {
126+
// Create a new text response
127+
const newText: Message = {
128+
id: crypto.randomUUID(),
129+
role: 'model',
130+
type: 'text',
131+
content: content,
132+
};
133+
return [...prev, newText];
134+
}
135+
});
136+
} else if (chunk.type === 'error') {
137+
const errorMessage: Message = {
138+
id: crypto.randomUUID(),
139+
role: 'system',
140+
type: 'error',
141+
content: chunk.content,
142+
};
143+
setMessages((prev) => [...prev, errorMessage]);
144+
}
145+
};
146+
147+
return (
148+
<>
149+
<div className="chat-messages">
150+
{/* Welcome Message */}
151+
<div className="message system-message">
152+
<div className="message-content">
153+
Agent will think out loud and show reasoning
154+
</div>
155+
</div>
156+
157+
{/* Conversation list */}
158+
{messages.map((msg) => {
159+
if (msg.type === 'thought') {
160+
return (
161+
<ThoughtBox
162+
key={msg.id}
163+
content={msg.content}
164+
stepName={msg.stepName}
165+
/>
166+
);
167+
} else {
168+
return (
169+
<MessageBubble
170+
key={msg.id}
171+
role={msg.role}
172+
type={msg.type}
173+
content={msg.content}
174+
/>
175+
);
176+
}
177+
})}
178+
<div ref={messagesEndRef} />
179+
</div>
180+
181+
<ChatInput onSubmit={handlePromptSubmit} disabled={loading} />
182+
</>
183+
);
184+
};
185+
186+
export default App;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { StrictMode } from 'react';
2+
import { createRoot } from 'react-dom/client';
3+
import './style.css';
4+
import App from './App.js';
5+
6+
createRoot(document.getElementById('root')!).render(
7+
<StrictMode>
8+
<App />
9+
</StrictMode>
10+
);
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Declares Vite client-side type definitions.
2+
// This allows TypeScript to recognize side-effect imports of static assets (like CSS, images, etc.)
3+
// which are compiled and resolved at build-time by Vite.
4+
/// <reference types="vite/client" />

0 commit comments

Comments
 (0)