From 1fa11495e9b4128fd8f91ad642a2ff320f427d7a Mon Sep 17 00:00:00 2001 From: Steve Androulakis Date: Fri, 3 Jan 2025 23:19:48 -0800 Subject: [PATCH] mucho UI fixes --- README.md | 8 +- frontend/src/components/ChatWindow.jsx | 62 ++++++----- frontend/src/components/ConfirmInline.jsx | 6 +- frontend/src/components/LLMResponse.jsx | 11 +- frontend/src/components/MessageBubble.jsx | 13 ++- frontend/src/components/NavBar.jsx | 8 +- frontend/src/index.css | 52 ++++++++- frontend/src/pages/App.jsx | 122 +++++++++++++--------- frontend/tailwind.config.js | 2 +- prompts/agent_prompt_generators.py | 2 +- 10 files changed, 185 insertions(+), 101 deletions(-) diff --git a/README.md b/README.md index e9a52ce..4a558ad 100644 --- a/README.md +++ b/README.md @@ -48,10 +48,8 @@ TODO: Document /frontend react app running instructions. - Note the mapping in `tools/__init__.py` to each tool ## TODO -- The LLM prompts move through 3 mock tools (FindEvents, SearchFlights, CreateInvoice) but I should make them contact real APIs. -- I should prove this out with other tool definitions (take advantage of my nice DSL). -- Might need to abstract the json example in the prompt generator to be part of a ToolDefinition (prevent overfitting to the example). +- Code GenerateInvoice against the Stripe API +- I should prove this out with other tool definitions outside of the event/flight search case (take advantage of my nice DSL). - Currently hardcoded to the Temporal dev server at localhost:7233. Need to support options incl Temporal Cloud. - UI: Make prettier -- UI: Tool Confirmed state could be better represented -- UI: 'Start new chat' button needs to handle better \ No newline at end of file +- UI: Tool Confirmed state could be better represented \ No newline at end of file diff --git a/frontend/src/components/ChatWindow.jsx b/frontend/src/components/ChatWindow.jsx index a82931b..ec97c2a 100644 --- a/frontend/src/components/ChatWindow.jsx +++ b/frontend/src/components/ChatWindow.jsx @@ -19,46 +19,50 @@ export default function ChatWindow({ conversation, loading, onConfirm }) { } const filtered = conversation.filter((msg) => { - const { actor, response } = msg; - + if (actor === "user") { return true; } if (actor === "agent") { const parsed = typeof response === "string" ? safeParse(response) : response; - // Keep if next is "question", "confirm", or "user_confirmed_tool_run". - // Only skip if next is "done" (or something else). - // return !["done"].includes(parsed.next); - return true; + return true; // Adjust this logic based on your "next" field. } return false; }); return ( -
- {filtered.map((msg, idx) => { - - const { actor, response } = msg; - - if (actor === "user") { - return ( - - ); - } else if (actor === "agent") { - const data = - typeof response === "string" ? safeParse(response) : response; - return ; - } - return null; - })} - - {/* If loading = true, show the spinner at the bottom */} - {loading && ( -
- -
- )} +
+ {/* Main message container */} +
+ {filtered.map((msg, idx) => { + const { actor, response } = msg; + + if (actor === "user") { + return ; + } else if (actor === "agent") { + const data = + typeof response === "string" ? safeParse(response) : response; + const isLastMessage = idx === filtered.length - 1; + return ( + + ); + } + return null; // Fallback for unsupported actors. + })} + {/* Loading indicator */} + {loading && ( +
+ +
+ )} +
); + } diff --git a/frontend/src/components/ConfirmInline.jsx b/frontend/src/components/ConfirmInline.jsx index 3cdbf7c..878d817 100644 --- a/frontend/src/components/ConfirmInline.jsx +++ b/frontend/src/components/ConfirmInline.jsx @@ -15,7 +15,7 @@ export default function ConfirmInline({ data, confirmed, onConfirm }) { {args && (
Args: -
+              
                 {JSON.stringify(args, null, 2)}
               
@@ -31,14 +31,14 @@ export default function ConfirmInline({ data, confirmed, onConfirm }) { // Not confirmed yet → show confirmation UI return (
-
+
Tool: {tool ?? "Unknown"}
{args && (
Args: -
+            
               {JSON.stringify(args, null, 2)}
             
diff --git a/frontend/src/components/LLMResponse.jsx b/frontend/src/components/LLMResponse.jsx index dee8fbb..654dc21 100644 --- a/frontend/src/components/LLMResponse.jsx +++ b/frontend/src/components/LLMResponse.jsx @@ -2,17 +2,16 @@ import React, { useState } from "react"; import MessageBubble from "./MessageBubble"; import ConfirmInline from "./ConfirmInline"; -export default function LLMResponse({ data, onConfirm }) { +export default function LLMResponse({ data, onConfirm, isLastMessage }) { const [isConfirmed, setIsConfirmed] = useState(false); const handleConfirm = async () => { - if (onConfirm) { - await onConfirm(); - } - setIsConfirmed(true); // Update state after confirmation + if (onConfirm) await onConfirm(); + setIsConfirmed(true); }; - const requiresConfirm = data.next === "confirm"; + // Only requires confirm if data.next === "confirm" AND it's the last message + const requiresConfirm = data.next === "confirm" && isLastMessage; if (typeof data.response === "object") { data.response = data.response.response; diff --git a/frontend/src/components/MessageBubble.jsx b/frontend/src/components/MessageBubble.jsx index 0ee06c3..0356c2d 100644 --- a/frontend/src/components/MessageBubble.jsx +++ b/frontend/src/components/MessageBubble.jsx @@ -16,11 +16,14 @@ export default function MessageBubble({ message, fallback = "", isUser = false } return (
+ className={`inline-block px-4 py-2 mb-1 rounded-lg ${ + isUser ? "ml-auto bg-blue-100" : "mr-auto bg-gray-200" + } break-words`} + style={{ + whiteSpace: "pre-wrap", + maxWidth: "75%", // or '80%' + }} +> {displayText}
); diff --git a/frontend/src/components/NavBar.jsx b/frontend/src/components/NavBar.jsx index 49dfae9..693f313 100644 --- a/frontend/src/components/NavBar.jsx +++ b/frontend/src/components/NavBar.jsx @@ -2,8 +2,10 @@ import React from "react"; export default function NavBar({ title }) { return ( -
-

{title}

-
+
+

{title}

+ {/* ...any additional nav items... */} +
); } diff --git a/frontend/src/index.css b/frontend/src/index.css index bd6213e..0838cb3 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,3 +1,53 @@ +@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@100;200;300;400;500;600;700;800;900&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap'); + @tailwind base; @tailwind components; -@tailwind utilities; \ No newline at end of file +@tailwind utilities; + +*, +html, +body { + box-sizing: border-box; +} + +html { + height: 100vh; + width: 100vw; +} + +body { + font-family: Inter, sans-serif; + position: relative; + height: 100%; + width: 100%; +} + +h1, +h2, +h3, +h4, +h5, +h6, +titles, +labels { + font-family: Poppins, sans-serif; +} + +/* example if you want it in index.css or a separate .css */ +.corner { + width: 3em; + height: 3em; +} +.corner a { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; +} +.corner img { + width: 2em; + height: 2em; + object-fit: contain; +} diff --git a/frontend/src/pages/App.jsx b/frontend/src/pages/App.jsx index f68b0a1..a0244f3 100644 --- a/frontend/src/pages/App.jsx +++ b/frontend/src/pages/App.jsx @@ -8,23 +8,26 @@ export default function App() { const [conversation, setConversation] = useState([]); const [userInput, setUserInput] = useState(""); const [loading, setLoading] = useState(false); + const [done, setDone] = useState(true); // New `done` state useEffect(() => { - // Poll /get-conversation-history once per second + // 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(); - // data is now an object like { messages: [ ... ] } - - if (data.messages && data.messages.length > 0 && (data.messages[data.messages.length - 1].actor === "agent")) { - setLoading(false); - } - else { - setLoading(true); - } + // Update conversation setConversation(data.messages || []); + + if (data.messages && data.messages.length > 0) { + const lastMessage = data.messages[data.messages.length - 1]; + setLoading(lastMessage.actor !== "agent"); + setDone(lastMessage.response.next === "done"); + } else { + setLoading(false); + setDone(true); // Default to `done` if no messages + } } } catch (err) { console.error("Error fetching conversation history:", err); @@ -37,7 +40,7 @@ export default function App() { const handleSendMessage = async () => { if (!userInput.trim()) return; try { - setLoading(true); // <--- Mark as loading + setLoading(true); // Mark as loading await fetch( `http://127.0.0.1:8000/send-prompt?prompt=${encodeURIComponent(userInput)}`, { method: "POST" } @@ -65,7 +68,7 @@ export default function App() { `http://127.0.0.1:8000/send-prompt?prompt=${encodeURIComponent("I'd like to travel for an event.")}`, { method: "POST" } ); - setConversation([]); // clear local state + setConversation([]); // Clear local state } catch (err) { console.error("Error ending chat:", err); } @@ -78,44 +81,69 @@ export default function App() { }; return ( -
- -
-
- {/* Pass down the array of messages to ChatWindow */} - - -
- setUserInput(e.target.value)} - onKeyPress={handleKeyPress} +
+ + + {/* Centered content, but no manual bottom margin */} +
+
+ {/* Scrollable chat area */} +
+ - -
-
- -
+ {done && ( +
+ Chat ended +
+ )} +
+
+
+ + {/* Floating Input Section */} + {/* Fixed bottom input */} +
+
+ setUserInput(e.target.value)} + onKeyPress={handleKeyPress} + disabled={loading || done} + /> + +
+
+
); -} +} \ No newline at end of file diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 0764d15..4a7513e 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -1,7 +1,7 @@ /** @type {import('tailwindcss').Config} */ module.exports = { content: [ - "./public/index.html", + "./index.html", "./src/**/*.{js,jsx,ts,tsx}", ], darkMode: "class", // enable dark mode by toggling a .dark class diff --git a/prompts/agent_prompt_generators.py b/prompts/agent_prompt_generators.py index 76290d6..00cc634 100644 --- a/prompts/agent_prompt_generators.py +++ b/prompts/agent_prompt_generators.py @@ -81,7 +81,7 @@ def generate_genai_prompt( "1) If any required argument is missing, set next='question' and ask the user.\n" "2) If all required arguments are known, set next='confirm' and specify the tool.\n" " The user will confirm before the tool is run.\n" - "3) If no more tools are needed, set next='done' and tool=null.\n" + "3) If no more tools are needed (user_confirmed_tool_run has been run for all), set next='done' and tool=null.\n" "4) response should be short and user-friendly." )