Skip to content

Commit 0a644f6

Browse files
reasoning llm streaming
reasoning llm streaming
1 parent 134441f commit 0a644f6

File tree

9 files changed

+1319
-28
lines changed

9 files changed

+1319
-28
lines changed

src/frontend/src/App.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ function App() {
99
<Routes>
1010
<Route path="/" element={<HomePage />} />
1111
<Route path="/plan/:planId" element={<PlanPage />} />
12+
<Route path="/plan/:planId/create" element={<PlanPage />} />
1213
<Route path="*" element={<Navigate to="/" replace />} />
1314
</Routes>
1415
</Router>

src/frontend/src/api/apiService.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
const API_ENDPOINTS = {
1717
INPUT_TASK: '/input_task',
1818
CREATE_PLAN: '/create_plan',
19+
GENERATE_PLAN: '/generate_plan',
1920
PLANS: '/plans',
2021
STEPS: '/steps',
2122
HUMAN_FEEDBACK: '/human_feedback',
@@ -117,6 +118,37 @@ export class APIService {
117118
return apiClient.post(API_ENDPOINTS.CREATE_PLAN, inputTask);
118119
}
119120

121+
/**
122+
* Generate plan details with reasoning stream
123+
* @param planId The plan ID to generate steps for
124+
* @returns ReadableStream for streaming response
125+
*/
126+
async generatePlanStream(planId: string): Promise<ReadableStream<Uint8Array>> {
127+
// Import the config functions
128+
const { headerBuilder, getApiUrl } = await import('./config');
129+
130+
const authHeaders = headerBuilder();
131+
const apiUrl = getApiUrl();
132+
133+
const response = await fetch(`${apiUrl}${API_ENDPOINTS.GENERATE_PLAN}/${planId}`, {
134+
method: 'POST',
135+
headers: {
136+
'Content-Type': 'application/json',
137+
...authHeaders,
138+
},
139+
});
140+
141+
if (!response.ok) {
142+
throw new Error(`HTTP error! status: ${response.status}`);
143+
}
144+
145+
if (!response.body) {
146+
throw new Error('Response body is null');
147+
}
148+
149+
return response.body;
150+
}
151+
120152
/**
121153
* Get all plans, optionally filtered by session ID
122154
* @param sessionId Optional session ID to filter plans

src/frontend/src/components/content/HomeInput.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,10 @@ const HomeInput: React.FC<HomeInputProps> = ({
6767
if (response.plan_id && response.plan_id !== null) {
6868
showToast("Plan created!", "success");
6969
dismissToast(id);
70-
navigate(`/plan/${response.plan_id}`);
70+
// Navigate to the create page to show streaming generation
71+
navigate(`/plan/${response.plan_id}/create`, {
72+
state: { isNewPlan: true, autoStartGeneration: true }
73+
});
7174
} else {
7275
console.log("Invalid plan:", response.status);
7376
showToast("Failed to create plan", "error");
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
import React, { useEffect, useState, useRef } from 'react';
2+
import { useParams, useNavigate } from 'react-router-dom';
3+
import {
4+
Body1,
5+
Title2,
6+
Spinner,
7+
Button,
8+
Card,
9+
CardHeader
10+
} from '@fluentui/react-components';
11+
import { CheckmarkCircle24Regular, DismissCircle24Regular } from '@fluentui/react-icons';
12+
import { apiService } from '../../api/apiService';
13+
import './../../styles/PlanReasoningStream.css';
14+
15+
interface StreamMessage {
16+
type: 'content' | 'processing' | 'success' | 'error' | 'result';
17+
content: string;
18+
timestamp: Date;
19+
}
20+
21+
const PlanReasoningStream: React.FC = () => {
22+
const { planId } = useParams<{ planId: string }>();
23+
const navigate = useNavigate();
24+
const [messages, setMessages] = useState<StreamMessage[]>([]);
25+
const [isStreaming, setIsStreaming] = useState(false);
26+
const [isComplete, setIsComplete] = useState(false);
27+
const [error, setError] = useState<string | null>(null);
28+
const messagesEndRef = useRef<HTMLDivElement>(null);
29+
30+
const scrollToBottom = () => {
31+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
32+
};
33+
34+
useEffect(() => {
35+
scrollToBottom();
36+
}, [messages]);
37+
38+
useEffect(() => {
39+
if (!planId) {
40+
setError('Plan ID is required');
41+
return;
42+
}
43+
44+
startStreaming();
45+
}, [planId]);
46+
47+
const addMessage = (type: StreamMessage['type'], content: string) => {
48+
setMessages(prev => [...prev, {
49+
type,
50+
content,
51+
timestamp: new Date()
52+
}]);
53+
};
54+
55+
const startStreaming = async () => {
56+
if (!planId) return;
57+
58+
try {
59+
setIsStreaming(true);
60+
setError(null);
61+
addMessage('processing', 'Starting plan generation...');
62+
63+
const stream = await apiService.generatePlanStream(planId);
64+
const reader = stream.getReader();
65+
const decoder = new TextDecoder();
66+
67+
let buffer = '';
68+
69+
while (true) {
70+
const { done, value } = await reader.read();
71+
72+
if (done) {
73+
break;
74+
}
75+
76+
buffer += decoder.decode(value, { stream: true });
77+
const lines = buffer.split('\n');
78+
buffer = lines.pop() || '';
79+
80+
for (const line of lines) {
81+
if (line.startsWith('data: ')) {
82+
const data = line.slice(6);
83+
84+
if (data === '[DONE]') {
85+
setIsComplete(true);
86+
setIsStreaming(false);
87+
return;
88+
}
89+
90+
if (data.startsWith('ERROR:')) {
91+
addMessage('error', data.slice(6));
92+
setError(data.slice(6));
93+
setIsStreaming(false);
94+
return;
95+
}
96+
97+
if (data.startsWith('[PROCESSING]')) {
98+
addMessage('processing', data.slice(12));
99+
} else if (data.startsWith('[SUCCESS]')) {
100+
addMessage('success', data.slice(9));
101+
} else if (data.startsWith('[ERROR]')) {
102+
addMessage('error', data.slice(7));
103+
setError(data.slice(7));
104+
} else if (data.startsWith('[RESULT]')) {
105+
try {
106+
const result = JSON.parse(data.slice(8));
107+
addMessage('result', `Plan generation completed! Created ${result.steps_created} steps.`);
108+
setIsComplete(true);
109+
setIsStreaming(false);
110+
} catch (e) {
111+
addMessage('error', 'Failed to parse final result');
112+
}
113+
} else if (data.trim()) {
114+
addMessage('content', data);
115+
}
116+
}
117+
}
118+
}
119+
} catch (error: any) {
120+
const errorMessage = error?.message || 'Failed to generate plan';
121+
setError(errorMessage);
122+
addMessage('error', errorMessage);
123+
setIsStreaming(false);
124+
}
125+
};
126+
127+
const handleViewPlan = () => {
128+
navigate(`/plan/${planId}`);
129+
};
130+
131+
const handleGoHome = () => {
132+
navigate('/');
133+
};
134+
135+
const getMessageIcon = (type: StreamMessage['type']) => {
136+
switch (type) {
137+
case 'success':
138+
return <CheckmarkCircle24Regular style={{ color: '#107C10' }} />;
139+
case 'error':
140+
return <DismissCircle24Regular style={{ color: '#D13438' }} />;
141+
case 'processing':
142+
return <Spinner size="tiny" />;
143+
default:
144+
return null;
145+
}
146+
};
147+
148+
const getMessageClass = (type: StreamMessage['type']) => {
149+
switch (type) {
150+
case 'processing':
151+
return 'reasoning-message processing';
152+
case 'success':
153+
return 'reasoning-message success';
154+
case 'error':
155+
return 'reasoning-message error';
156+
case 'result':
157+
return 'reasoning-message result';
158+
default:
159+
return 'reasoning-message content';
160+
}
161+
};
162+
163+
return (
164+
<div className="plan-reasoning-container">
165+
<div className="plan-reasoning-header">
166+
<Title2>Creating Your Plan</Title2>
167+
<Body1>Watch as the AI reasons through your task and creates a detailed plan.</Body1>
168+
</div>
169+
170+
<Card className="reasoning-stream-card">
171+
<CardHeader
172+
header={
173+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
174+
{isStreaming && <Spinner size="tiny" />}
175+
<span>
176+
{isStreaming ? 'Reasoning in progress...' :
177+
isComplete ? 'Plan generation complete!' :
178+
error ? 'Generation failed' : 'Preparing to generate plan...'}
179+
</span>
180+
</div>
181+
}
182+
/>
183+
184+
<div className="reasoning-content">
185+
<div className="reasoning-messages">
186+
{messages.map((message, index) => (
187+
<div key={index} className={getMessageClass(message.type)}>
188+
<div className="message-icon">
189+
{getMessageIcon(message.type)}
190+
</div>
191+
<div className="message-content">
192+
<pre className="message-text">{message.content}</pre>
193+
<div className="message-timestamp">
194+
{message.timestamp.toLocaleTimeString()}
195+
</div>
196+
</div>
197+
</div>
198+
))}
199+
<div ref={messagesEndRef} />
200+
</div>
201+
</div>
202+
</Card>
203+
204+
<div className="reasoning-actions">
205+
{isComplete && (
206+
<Button
207+
appearance="primary"
208+
onClick={handleViewPlan}
209+
style={{ marginRight: '12px' }}
210+
>
211+
View Plan
212+
</Button>
213+
)}
214+
<Button
215+
appearance="secondary"
216+
onClick={handleGoHome}
217+
>
218+
Create Another Plan
219+
</Button>
220+
</div>
221+
</div>
222+
);
223+
};
224+
225+
export default PlanReasoningStream;

0 commit comments

Comments
 (0)