update working

This commit is contained in:
leehk 2025-04-25 12:05:03 +08:00
parent 896d38e79f
commit 4e5f9f57c2
6 changed files with 1388 additions and 283 deletions

View File

@ -435,10 +435,58 @@ async def websocket_endpoint(websocket: WebSocket):
"question": data_json[0]["content"]
}
async for chunk in app.astream(inputs):
await manager.send_personal_message(
json.dumps({"type": "message", "payload": chunk.get("content", str(chunk))}),
websocket,
)
# Determine if chunk is intermediate or final
if isinstance(chunk, dict):
if len(chunk) == 1:
step_name = list(chunk.keys())[0]
step_value = chunk[step_name]
# Check if this step contains the final answer
if isinstance(step_value, dict) and 'generation' in step_value:
await manager.send_personal_message(
json.dumps({
"type": "final",
"title": "Answer",
"payload": step_value['generation']
}),
websocket,
)
else:
await manager.send_personal_message(
json.dumps({
"type": "intermediate",
"title": step_name.replace('_', ' ').title(),
"payload": str(step_value)
}),
websocket,
)
elif 'generation' in chunk:
await manager.send_personal_message(
json.dumps({
"type": "final",
"title": "Answer",
"payload": chunk['generation']
}),
websocket,
)
else:
await manager.send_personal_message(
json.dumps({
"type": "intermediate",
"title": "Step",
"payload": str(chunk)[:500]
}),
websocket,
)
else:
# Fallback for non-dict chunks
await manager.send_personal_message(
json.dumps({
"type": "intermediate",
"title": "Step",
"payload": str(chunk)[:500]
}),
websocket,
)
# Send a final 'done' message to signal completion
await manager.send_personal_message(
json.dumps({"type": "done"}),

View File

@ -33,6 +33,8 @@ You must not use any information that is not present in the provided context to
If you don't know the answer, just say that you don't know.\n
Provide the answer in a concise and organized manner. \n
Reformat the answer in a human-readable markdown format, include underlined or new lines to improve readability if needed.\n
Question: {question} \n
Context: {context} \n
Answer:

View File

@ -12,7 +12,7 @@ ARG ENV_FILE=.env.test
COPY ${ENV_FILE} /usr/src/app/.env
# Copy dependency files and install dependencies
RUN npm install && npm i --save-dev @types/jest
RUN npm install && npm install --save-dev @types/jest
EXPOSE 80
CMD [ "npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "80" ]

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,8 @@
"dependencies": {
"daisyui": "^5.0.17",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"react-markdown": "^10.1.0"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
@ -37,4 +38,4 @@
"vite": "^6.2.0",
"vitest": "^3.1.1"
}
}
}

View File

@ -1,4 +1,5 @@
import React, { useState, useEffect, useRef } from 'react';
import ReactMarkdown from 'react-markdown';
const BASE_DOMAIN_NAME_PORT = import.meta.env.REACT_APP_DOMAIN_NAME_PORT || 'localhost:8004';
@ -8,58 +9,77 @@ interface Message {
text: string;
}
interface ChatTurn {
question: string;
intermediateMessages: { title: string; payload: string }[];
finalAnswer: string | null;
isLoading: boolean;
showIntermediate: boolean;
}
const App: React.FC = () => {
const [messages, setMessages] = useState<Message[]>([]);
const [intermediateMessages, setIntermediateMessages] = useState<{title: string, payload: string}[]>([]);
const [finalAnswer, setFinalAnswer] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [showIntermediate, setShowIntermediate] = useState(false);
const [newMessage, setNewMessage] = useState('');
const [socket, setSocket] = useState<WebSocket | null>(null);
const [isLoading, setIsLoading] = useState(false);
const mounted = useRef(false);
useEffect(() => {
mounted.current = true;
const ws = new WebSocket(`ws://${BASE_DOMAIN_NAME_PORT}/ws`);
setSocket(ws);
ws.onopen = () => {
console.log('WebSocket connection opened');
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (data.type === 'message' && data.payload && mounted.current) {
setMessages((prevMessages) => {
const lastMessage = prevMessages[prevMessages.length - 1];
if (lastMessage && lastMessage.sender === 'bot') {
return [...prevMessages.slice(0, -1), { ...lastMessage, text: lastMessage.text + data.payload }];
} else {
return [...prevMessages, { sender: 'bot', text: data.payload }];
mounted.current = true;
const ws = new WebSocket(`ws://${BASE_DOMAIN_NAME_PORT}/ws`);
setSocket(ws);
ws.onopen = () => {
console.log('WebSocket connection opened');
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (data.type === 'message' && data.payload && mounted.current) {
// legacy support, treat as final
setMessages((prevMessages) => {
const lastMessage = prevMessages[prevMessages.length - 1];
if (lastMessage && lastMessage.sender === 'bot') {
return [...prevMessages.slice(0, -1), { ...lastMessage, text: lastMessage.text + data.payload }];
} else {
return [...prevMessages, { sender: 'bot', text: data.payload }];
}
});
setFinalAnswer(data.payload);
} else if (data.type === 'intermediate') {
setIntermediateMessages((prev) => [...prev, { title: data.title, payload: data.payload }]);
} else if (data.type === 'final') {
setFinalAnswer(data.payload);
} else if (data.type === 'done') {
setIsLoading(false);
} else {
console.error('Unexpected message format:', data);
}
} catch (error) {
console.error('Error parsing message:', error);
}
});
} else if (data.type === 'done') {
setIsLoading(false);
} else {
console.error('Unexpected message format:', data);
}
} catch (error) {
console.error('Error parsing message:', error);
}
};
ws.onclose = () => {
console.log('WebSocket connection closed');
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
return () => {
mounted.current = false;
ws.close();
};
}, []);
};
ws.onclose = () => {
console.log('WebSocket connection closed');
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
return () => {
mounted.current = false;
ws.close();
};
}, []);
const sendMessage = () => {
if (newMessage.trim() !== '') {
if (newMessage.trim() !== '' && !isLoading) {
setIsLoading(true);
const message = [{ role: 'user', content: newMessage }];
setIntermediateMessages([]);
setFinalAnswer(null);
setMessages((prevMessages) => [...prevMessages, { sender: 'user', text: newMessage }]);
const message = [{ role: 'user', content: newMessage }];
socket?.send(JSON.stringify(message));
setNewMessage('');
}
@ -76,6 +96,52 @@ const App: React.FC = () => {
{msg.text}
</div>
))}
{/* Status box for intermediate steps */}
{intermediateMessages.length > 0 && (
<div className="mb-4">
<div className="bg-blue-50 border border-blue-300 rounded-lg p-3 shadow-sm flex items-center">
{/* Spinner icon */}
{isLoading && (
<svg className="animate-spin h-5 w-5 text-blue-500 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"></path>
</svg>
)}
<span className="font-semibold text-blue-700 mr-2">Working on:</span>
{/* Key steps summary */}
<div className="flex flex-wrap gap-2">
{intermediateMessages.map((msg, idx) => (
<span key={idx} className="bg-blue-100 text-blue-700 px-2 py-1 rounded text-xs font-medium border border-blue-200">
{msg.title}
</span>
))}
</div>
<button
className="ml-auto text-xs text-blue-600 underline focus:outline-none"
onClick={() => setShowIntermediate((v) => !v)}
>
{showIntermediate ? 'Hide details' : 'Show details'}
</button>
</div>
{/* Expanded details */}
{showIntermediate && (
<div className="bg-white border border-blue-200 rounded-b-lg p-3 mt-1 text-xs max-h-64 overflow-y-auto">
{intermediateMessages.map((msg, idx) => (
<div key={idx} className="mb-3">
<div className="font-bold text-blue-700 mb-1">{msg.title}</div>
<pre className="whitespace-pre-wrap break-words text-gray-800">{msg.payload}</pre>
</div>
))}
</div>
)}
</div>
)}
{/* Final answer for this question */}
{finalAnswer && (
<div className="p-4 rounded-lg mb-2 bg-gray-200 text-gray-800 prose max-w-none">
<ReactMarkdown>{finalAnswer}</ReactMarkdown>
</div>
)}
</div>
<div className="p-4 border-t border-gray-300">
<div className="flex">