refactor using Cursor

This commit is contained in:
Steve Androulakis
2025-01-07 13:19:34 -08:00
parent 99b11099af
commit 1b8b9c9906
8 changed files with 534 additions and 283 deletions

View File

@@ -1,74 +1,153 @@
import React, { useEffect, useState, useRef } from "react";
import React, { useEffect, useState, useRef, useCallback } from "react";
import NavBar from "../components/NavBar";
import ChatWindow from "../components/ChatWindow";
import { apiService } from "../services/api";
const POLL_INTERVAL = 500; // 0.5 seconds
const INITIAL_ERROR_STATE = { visible: false, message: '' };
const DEBOUNCE_DELAY = 300; // 300ms debounce for user input
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
export default function App() {
const containerRef = useRef(null);
const inputRef = useRef(null);
const pollingRef = useRef(null);
const scrollTimeoutRef = useRef(null);
const [conversation, setConversation] = useState([]);
const [lastMessage, setLastMessage] = useState(null); // New state for tracking the last message
const [lastMessage, setLastMessage] = useState(null);
const [userInput, setUserInput] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState(INITIAL_ERROR_STATE);
const [done, setDone] = useState(true);
useEffect(() => {
// Poll /get-conversation-history every 0.5 seconds
const intervalId = setInterval(async () => {
try {
const res = await fetch("http://127.0.0.1:8000/get-conversation-history");
if (res.ok) {
const data = await res.json();
const newConversation = data.messages || [];
setConversation(newConversation);
const debouncedUserInput = useDebounce(userInput, DEBOUNCE_DELAY);
if (newConversation.length > 0) {
const lastMsg = newConversation[newConversation.length - 1];
setLoading(lastMsg.actor !== "agent");
setDone(lastMsg.response.next === "done");
// Error handling utility with auto-dismiss
const handleError = useCallback((error, context) => {
console.error(`${context}:`, error);
const errorMessage = error.status === 400
? error.message
: `Error ${context.toLowerCase()}. Please try again.`;
setError({
visible: true,
message: errorMessage
});
const timer = setTimeout(() => setError(INITIAL_ERROR_STATE), 3000);
return () => clearTimeout(timer);
}, []);
// Only scroll if the last message changes
if (!lastMessage || lastMsg.response.response !== lastMessage.response.response) {
setLastMessage(lastMsg); // Update the last message
}
} else {
setLoading(false);
setDone(true);
setLastMessage(null); // Clear last message if no messages
}
const fetchConversationHistory = useCallback(async () => {
try {
const data = await apiService.getConversationHistory();
const newConversation = data.messages || [];
setConversation(prevConversation => {
// Only update if there are actual changes
if (JSON.stringify(prevConversation) !== JSON.stringify(newConversation)) {
return newConversation;
}
} catch (err) {
console.error("Error fetching conversation history:", err);
return prevConversation;
});
if (newConversation.length > 0) {
const lastMsg = newConversation[newConversation.length - 1];
const isAgentMessage = lastMsg.actor === "agent";
setLoading(!isAgentMessage);
setDone(lastMsg.response.next === "done");
setLastMessage(prevLastMessage => {
if (!prevLastMessage || lastMsg.response.response !== prevLastMessage.response.response) {
return lastMsg;
}
return prevLastMessage;
});
} else {
setLoading(false);
setDone(true);
setLastMessage(null);
}
}, POLL_INTERVAL);
} catch (err) {
handleError(err, "fetching conversation");
}
}, [handleError]);
return () => clearInterval(intervalId);
}, [lastMessage]);
// Setup polling with cleanup
useEffect(() => {
pollingRef.current = setInterval(fetchConversationHistory, POLL_INTERVAL);
return () => {
if (pollingRef.current) {
clearInterval(pollingRef.current);
}
};
}, [fetchConversationHistory]);
const scrollToBottom = useCallback(() => {
if (containerRef.current) {
if (scrollTimeoutRef.current) {
clearTimeout(scrollTimeoutRef.current);
}
scrollTimeoutRef.current = setTimeout(() => {
const element = containerRef.current;
element.scrollTop = element.scrollHeight;
scrollTimeoutRef.current = null;
}, 100);
}
}, []);
const handleContentChange = useCallback(() => {
scrollToBottom();
}, [scrollToBottom]);
useEffect(() => {
if (containerRef.current && lastMessage) {
containerRef.current.scrollTop = containerRef.current.scrollHeight;
if (lastMessage) {
scrollToBottom();
}
}, [lastMessage]); // Scroll only when the last message changes
}, [lastMessage, scrollToBottom]);
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus(); // Ensure the input box retains focus
if (inputRef.current && !loading && !done) {
inputRef.current.focus();
}
}, [userInput, loading, done]);
return () => {
if (scrollTimeoutRef.current) {
clearTimeout(scrollTimeoutRef.current);
}
};
}, [loading, done]);
const handleSendMessage = async () => {
if (!userInput.trim()) return;
const trimmedInput = userInput.trim();
if (!trimmedInput) return;
try {
setLoading(true);
await fetch(
`http://127.0.0.1:8000/send-prompt?prompt=${encodeURIComponent(userInput)}`,
{ method: "POST" }
);
setError(INITIAL_ERROR_STATE);
await apiService.sendMessage(trimmedInput);
setUserInput("");
} catch (err) {
console.error("Error sending prompt:", err);
handleError(err, "sending message");
setLoading(false);
}
};
@@ -76,29 +155,25 @@ export default function App() {
const handleConfirm = async () => {
try {
setLoading(true);
await fetch("http://127.0.0.1:8000/confirm", { method: "POST" });
setError(INITIAL_ERROR_STATE);
await apiService.confirm();
} catch (err) {
console.error("Confirm error:", err);
handleError(err, "confirming action");
setLoading(false);
}
};
const handleStartNewChat = async () => {
try {
await fetch(
`http://127.0.0.1:8000/send-prompt?prompt=${encodeURIComponent("I'd like to travel for an event.")}`,
{ method: "POST" }
);
setConversation([]); // Clear local state
setLastMessage(null); // Reset last message
setError(INITIAL_ERROR_STATE);
setLoading(true);
await apiService.sendMessage("I'd like to travel for an event.");
setConversation([]);
setLastMessage(null);
} catch (err) {
console.error("Error ending chat:", err);
}
};
const handleKeyPress = (e) => {
if (e.key === "Enter") {
handleSendMessage();
handleError(err, "starting new chat");
} finally {
setLoading(false);
}
};
@@ -106,19 +181,28 @@ export default function App() {
<div className="flex flex-col h-screen">
<NavBar title="Temporal AI Agent 🤖" />
{/* Centered content, but no manual bottom margin */}
{error.visible && (
<div className="fixed top-16 left-1/2 transform -translate-x-1/2
bg-red-500 text-white px-4 py-2 rounded shadow-lg z-50
transition-opacity duration-300">
{error.message}
</div>
)}
<div className="flex-grow flex justify-center px-4 py-2 overflow-hidden">
<div className="w-full max-w-lg bg-white dark:bg-gray-900 p-8 px-3 rounded shadow-md
flex flex-col overflow-hidden">
{/* Scrollable chat area */}
<div ref={containerRef} className="flex-grow overflow-y-auto pb-20 pt-10">
flex flex-col overflow-hidden">
<div ref={containerRef}
className="flex-grow overflow-y-auto pb-20 pt-10 scroll-smooth">
<ChatWindow
conversation={conversation}
loading={loading}
onConfirm={handleConfirm}
onContentChange={handleContentChange}
/>
{done && (
<div className="text-center text-sm text-gray-500 dark:text-gray-400 mt-4">
<div className="text-center text-sm text-gray-500 dark:text-gray-400 mt-4
animate-fade-in">
Chat ended
</div>
)}
@@ -126,41 +210,49 @@ export default function App() {
</div>
</div>
{/* Fixed bottom input */}
<div
className="fixed bottom-0 left-1/2 transform -translate-x-1/2
<div className="fixed bottom-0 left-1/2 transform -translate-x-1/2
w-full max-w-lg bg-white dark:bg-gray-900 p-4
border-t border-gray-300 dark:border-gray-700"
style={{ zIndex: 10 }}
>
<div className="flex items-center">
border-t border-gray-300 dark:border-gray-700 shadow-lg
transition-all duration-200"
style={{ zIndex: 10 }}>
<form onSubmit={(e) => {
e.preventDefault();
handleSendMessage();
}} className="flex items-center">
<input
ref={inputRef}
type="text"
className={`flex-grow rounded-l px-3 py-2 border border-gray-300
dark:bg-gray-700 dark:border-gray-600 focus:outline-none
${loading || done ? "opacity-50 cursor-not-allowed" : ""}`}
dark:bg-gray-700 dark:border-gray-600 focus:outline-none
transition-opacity duration-200
${loading || done ? "opacity-50 cursor-not-allowed" : ""}`}
placeholder="Type your message..."
value={userInput}
onChange={(e) => setUserInput(e.target.value)}
onKeyPress={handleKeyPress}
disabled={loading || done}
aria-label="Type your message"
/>
<button
onClick={handleSendMessage}
type="submit"
className={`bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-r
${loading || done ? "opacity-50 cursor-not-allowed" : ""}`}
transition-all duration-200
${loading || done ? "opacity-50 cursor-not-allowed" : ""}`}
disabled={loading || done}
aria-label="Send message"
>
Send
</button>
</div>
</form>
<div className="text-right mt-3">
<button
onClick={handleStartNewChat}
className={`text-sm underline text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200
${!done ? "opacity-0 cursor-not-allowed" : ""}`}
className={`text-sm underline text-gray-600 dark:text-gray-400
hover:text-gray-800 dark:hover:text-gray-200
transition-all duration-200
${!done ? "opacity-0 cursor-not-allowed" : ""}`}
disabled={!done}
aria-label="Start new chat"
>
Start New Chat
</button>