s4um1l commited on
Commit
fb8177a
·
1 Parent(s): 0af2618

including streaming response and vibe coded star wars theme

Browse files
Files changed (2) hide show
  1. backend/main.py +22 -14
  2. frontend/src/App.js +119 -12
backend/main.py CHANGED
@@ -1,19 +1,20 @@
1
  from fastapi import FastAPI, File, UploadFile, HTTPException, BackgroundTasks
2
  from fastapi.middleware.cors import CORSMiddleware
3
  from fastapi.staticfiles import StaticFiles
4
- from fastapi.responses import FileResponse
5
  from pydantic import BaseModel
6
  import uvicorn
7
  import os
8
  import tempfile
9
  import shutil
10
- from typing import List, Optional, Dict, Any
11
  import pathlib
12
  import asyncio
13
  import logging
14
  import time
15
  import traceback
16
  import uuid
 
17
 
18
  # Configure logging
19
  logging.basicConfig(level=logging.INFO,
@@ -256,7 +257,7 @@ async def upload_file(background_tasks: BackgroundTasks, file: UploadFile = File
256
  logger.error(traceback.format_exc()) # Log the full error traceback
257
  raise HTTPException(status_code=500, detail=f"Error processing file: {str(e)}")
258
 
259
- @app.post("/query/", response_model=QueryResponse)
260
  async def process_query(request: QueryRequest):
261
  logger.info(f"Received query request for session: {request.session_id}")
262
 
@@ -285,18 +286,25 @@ async def process_query(request: QueryRequest):
285
  start_time = time.time()
286
  result = await session_data.arun_pipeline(request.query)
287
 
288
- # In a streaming setup, we'd handle this differently
289
- # For simplicity, we're collecting the entire response
290
- response_text = ""
291
- async for chunk in result["response"]:
292
- response_text += chunk
293
-
294
- logger.info(f"Generated response of length: {len(response_text)} (took {time.time() - start_time:.2f}s)")
 
 
 
 
 
 
295
 
296
- return {
297
- "response": response_text,
298
- "session_id": request.session_id
299
- }
 
300
 
301
  except Exception as e:
302
  logger.error(f"Error processing query for session {request.session_id}: {str(e)}")
 
1
  from fastapi import FastAPI, File, UploadFile, HTTPException, BackgroundTasks
2
  from fastapi.middleware.cors import CORSMiddleware
3
  from fastapi.staticfiles import StaticFiles
4
+ from fastapi.responses import FileResponse, StreamingResponse
5
  from pydantic import BaseModel
6
  import uvicorn
7
  import os
8
  import tempfile
9
  import shutil
10
+ from typing import List, Optional, Dict, Any, Iterator
11
  import pathlib
12
  import asyncio
13
  import logging
14
  import time
15
  import traceback
16
  import uuid
17
+ import json
18
 
19
  # Configure logging
20
  logging.basicConfig(level=logging.INFO,
 
257
  logger.error(traceback.format_exc()) # Log the full error traceback
258
  raise HTTPException(status_code=500, detail=f"Error processing file: {str(e)}")
259
 
260
+ @app.post("/query/")
261
  async def process_query(request: QueryRequest):
262
  logger.info(f"Received query request for session: {request.session_id}")
263
 
 
286
  start_time = time.time()
287
  result = await session_data.arun_pipeline(request.query)
288
 
289
+ # Stream the response - this is key for the Star Wars effect
290
+ async def stream_response():
291
+ try:
292
+ async for chunk in result["response"]:
293
+ # Add a small delay between chunks for dramatic effect
294
+ await asyncio.sleep(0.01)
295
+ # Stream each chunk as JSON with proper encoding
296
+ yield chunk
297
+
298
+ logger.info(f"Completed streaming response (took {time.time() - start_time:.2f}s)")
299
+ except Exception as e:
300
+ logger.error(f"Error in streaming: {str(e)}")
301
+ yield f"Error during streaming: {str(e)}"
302
 
303
+ # Return streaming response
304
+ return StreamingResponse(
305
+ stream_response(),
306
+ media_type="text/plain",
307
+ )
308
 
309
  except Exception as e:
310
  logger.error(f"Error processing query for session {request.session_id}: {str(e)}")
frontend/src/App.js CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useState, useRef } from 'react';
2
  import {
3
  ChakraProvider,
4
  Box,
@@ -121,7 +121,42 @@ axios.interceptors.response.use(
121
  }
122
  );
123
 
124
- function ChatMessage({ message, isUser }) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  return (
126
  <Box
127
  bg={isUser ? 'rebel.500' : 'imperial.500'}
@@ -133,11 +168,38 @@ function ChatMessage({ message, isUser }) {
133
  maxW="80%"
134
  boxShadow="0 0 5px"
135
  color={isUser ? 'dark.500' : 'light.500'}
 
 
136
  >
137
  <Text fontWeight="bold" fontSize="sm" mb={1}>
138
  {isUser ? 'Rebel Commander' : 'Jedi Archives'}
139
  </Text>
140
- <ReactMarkdown>{message}</ReactMarkdown>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  </Box>
142
  );
143
  }
@@ -423,6 +485,7 @@ function App() {
423
  const [inputText, setInputText] = useState('');
424
  const [isProcessing, setIsProcessing] = useState(false);
425
  const [isDocProcessing, setIsDocProcessing] = useState(false);
 
426
  const messagesEndRef = useRef(null);
427
  const toast = useToast();
428
 
@@ -442,23 +505,67 @@ function App() {
442
 
443
  const userMessage = inputText;
444
  setInputText('');
 
445
  setMessages(prev => [...prev, { text: userMessage, isUser: true }]);
 
 
 
 
 
446
  setIsProcessing(true);
447
 
448
  try {
449
- // Either use the API_URL or direct backend based on environment
450
  const queryUrl = `${API_URL}/query/`;
451
  console.log('Sending query to:', queryUrl);
452
 
453
- const response = await axios.post(queryUrl, {
454
- session_id: sessionId,
455
- query: userMessage
 
 
 
 
 
 
 
456
  });
457
 
458
- console.log('Query response:', response.data);
459
- setMessages(prev => [...prev, { text: response.data.response, isUser: false }]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
460
  } catch (error) {
461
  console.error('Error sending message:', error);
 
462
 
463
  // Handle specific errors
464
  if (error.response?.status === 409) {
@@ -471,7 +578,7 @@ function App() {
471
  isClosable: true,
472
  });
473
 
474
- setMessages(prev => [...prev, {
475
  text: "The Jedi Council is still analyzing this document. Patience, young Padawan.",
476
  isUser: false
477
  }]);
@@ -485,12 +592,11 @@ function App() {
485
  isClosable: true,
486
  });
487
 
488
- setMessages(prev => [...prev, {
489
  text: "I find your lack of network connectivity disturbing. Please try again.",
490
  isUser: false
491
  }]);
492
  }
493
- } finally {
494
  setIsProcessing(false);
495
  }
496
  };
@@ -568,6 +674,7 @@ function App() {
568
  key={idx}
569
  message={msg.text}
570
  isUser={msg.isUser}
 
571
  />
572
  ))}
573
  {isDocProcessing && (
 
1
+ import React, { useState, useRef, useEffect } from 'react';
2
  import {
3
  ChakraProvider,
4
  Box,
 
121
  }
122
  );
123
 
124
+ function ChatMessage({ message, isUser, isStreaming }) {
125
+ const [displayedText, setDisplayedText] = useState('');
126
+ const [charIndex, setCharIndex] = useState(0);
127
+ const messageRef = useRef(null);
128
+
129
+ // Star Wars-style typewriter effect for streamed responses
130
+ useEffect(() => {
131
+ if (isUser || !isStreaming) {
132
+ setDisplayedText(message);
133
+ return;
134
+ }
135
+
136
+ // Reset if message changes
137
+ if (charIndex === 0) {
138
+ setDisplayedText('');
139
+ }
140
+
141
+ // Implement the typing effect with randomized speeds for Star Wars terminal feel
142
+ if (charIndex < message.length) {
143
+ const delay = Math.random() * 20 + 10; // Random delay between 10-30ms
144
+ const timer = setTimeout(() => {
145
+ setDisplayedText(prev => prev + message[charIndex]);
146
+ setCharIndex(prevIndex => prevIndex + 1);
147
+ }, delay);
148
+
149
+ return () => clearTimeout(timer);
150
+ }
151
+ }, [message, charIndex, isUser, isStreaming]);
152
+
153
+ // Auto-scroll to the bottom of the message as it streams
154
+ useEffect(() => {
155
+ if (isStreaming && messageRef.current) {
156
+ messageRef.current.scrollIntoView({ behavior: 'smooth', block: 'end' });
157
+ }
158
+ }, [displayedText, isStreaming]);
159
+
160
  return (
161
  <Box
162
  bg={isUser ? 'rebel.500' : 'imperial.500'}
 
168
  maxW="80%"
169
  boxShadow="0 0 5px"
170
  color={isUser ? 'dark.500' : 'light.500'}
171
+ ref={messageRef}
172
+ position="relative"
173
  >
174
  <Text fontWeight="bold" fontSize="sm" mb={1}>
175
  {isUser ? 'Rebel Commander' : 'Jedi Archives'}
176
  </Text>
177
+ {isStreaming && !isUser ? (
178
+ <Box position="relative">
179
+ <ReactMarkdown>{displayedText}</ReactMarkdown>
180
+ {charIndex < message.length && (
181
+ <Box
182
+ as="span"
183
+ display="inline-block"
184
+ w="10px"
185
+ h="16px"
186
+ bg="brand.500"
187
+ position="absolute"
188
+ ml="2px"
189
+ opacity={0.8}
190
+ animation="blink 1s step-end infinite"
191
+ sx={{
192
+ '@keyframes blink': {
193
+ '0%, 100%': { opacity: 0 },
194
+ '50%': { opacity: 1 }
195
+ }
196
+ }}
197
+ />
198
+ )}
199
+ </Box>
200
+ ) : (
201
+ <ReactMarkdown>{message}</ReactMarkdown>
202
+ )}
203
  </Box>
204
  );
205
  }
 
485
  const [inputText, setInputText] = useState('');
486
  const [isProcessing, setIsProcessing] = useState(false);
487
  const [isDocProcessing, setIsDocProcessing] = useState(false);
488
+ const [streamingIndex, setStreamingIndex] = useState(-1); // Track which message is streaming
489
  const messagesEndRef = useRef(null);
490
  const toast = useToast();
491
 
 
505
 
506
  const userMessage = inputText;
507
  setInputText('');
508
+ // Add user message right away
509
  setMessages(prev => [...prev, { text: userMessage, isUser: true }]);
510
+
511
+ // Add empty response message that will be filled via streaming
512
+ const messageIndex = messages.length + 1; // +1 since we just added user message
513
+ setMessages(prev => [...prev, { text: '', isUser: false }]);
514
+ setStreamingIndex(messageIndex);
515
  setIsProcessing(true);
516
 
517
  try {
 
518
  const queryUrl = `${API_URL}/query/`;
519
  console.log('Sending query to:', queryUrl);
520
 
521
+ // We need to handle the streaming response from the backend
522
+ const response = await fetch(queryUrl, {
523
+ method: 'POST',
524
+ headers: {
525
+ 'Content-Type': 'application/json',
526
+ },
527
+ body: JSON.stringify({
528
+ session_id: sessionId,
529
+ query: userMessage
530
+ })
531
  });
532
 
533
+ if (!response.ok) {
534
+ throw new Error(`HTTP error! status: ${response.status}`);
535
+ }
536
+
537
+ // Get the response reader for streaming
538
+ const reader = response.body.getReader();
539
+ const decoder = new TextDecoder();
540
+ let accumulatedResponse = '';
541
+
542
+ try {
543
+ // Process the stream as it comes in
544
+ while (true) {
545
+ const { value, done } = await reader.read();
546
+
547
+ // If the stream is done, break out of the loop
548
+ if (done) break;
549
+
550
+ // Decode the chunk and add it to our response
551
+ const chunk = decoder.decode(value, { stream: true });
552
+ accumulatedResponse += chunk;
553
+
554
+ // Update the current message with the accumulated response
555
+ setMessages(prev => prev.map((msg, idx) =>
556
+ idx === messageIndex ? { ...msg, text: accumulatedResponse } : msg
557
+ ));
558
+ }
559
+ } catch (streamError) {
560
+ console.error('Stream processing error:', streamError);
561
+ } finally {
562
+ // We're done streaming, clean up
563
+ setStreamingIndex(-1);
564
+ setIsProcessing(false);
565
+ }
566
  } catch (error) {
567
  console.error('Error sending message:', error);
568
+ setStreamingIndex(-1);
569
 
570
  // Handle specific errors
571
  if (error.response?.status === 409) {
 
578
  isClosable: true,
579
  });
580
 
581
+ setMessages(prev => [...prev.slice(0, -1), {
582
  text: "The Jedi Council is still analyzing this document. Patience, young Padawan.",
583
  isUser: false
584
  }]);
 
592
  isClosable: true,
593
  });
594
 
595
+ setMessages(prev => [...prev.slice(0, -1), {
596
  text: "I find your lack of network connectivity disturbing. Please try again.",
597
  isUser: false
598
  }]);
599
  }
 
600
  setIsProcessing(false);
601
  }
602
  };
 
674
  key={idx}
675
  message={msg.text}
676
  isUser={msg.isUser}
677
+ isStreaming={idx === streamingIndex}
678
  />
679
  ))}
680
  {isDocProcessing && (