mirror of
https://github.com/temporal-community/temporal-ai-agent.git
synced 2026-03-15 14:08:08 +01:00
mucho UI fixes
This commit is contained in:
@@ -48,10 +48,8 @@ TODO: Document /frontend react app running instructions.
|
|||||||
- Note the mapping in `tools/__init__.py` to each tool
|
- Note the mapping in `tools/__init__.py` to each tool
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
- The LLM prompts move through 3 mock tools (FindEvents, SearchFlights, CreateInvoice) but I should make them contact real APIs.
|
- Code GenerateInvoice against the Stripe API
|
||||||
- I should prove this out with other tool definitions (take advantage of my nice DSL).
|
- I should prove this out with other tool definitions outside of the event/flight search case (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).
|
|
||||||
- Currently hardcoded to the Temporal dev server at localhost:7233. Need to support options incl Temporal Cloud.
|
- Currently hardcoded to the Temporal dev server at localhost:7233. Need to support options incl Temporal Cloud.
|
||||||
- UI: Make prettier
|
- UI: Make prettier
|
||||||
- UI: Tool Confirmed state could be better represented
|
- UI: Tool Confirmed state could be better represented
|
||||||
- UI: 'Start new chat' button needs to handle better
|
|
||||||
@@ -19,7 +19,6 @@ export default function ChatWindow({ conversation, loading, onConfirm }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const filtered = conversation.filter((msg) => {
|
const filtered = conversation.filter((msg) => {
|
||||||
|
|
||||||
const { actor, response } = msg;
|
const { actor, response } = msg;
|
||||||
|
|
||||||
if (actor === "user") {
|
if (actor === "user") {
|
||||||
@@ -27,38 +26,43 @@ export default function ChatWindow({ conversation, loading, onConfirm }) {
|
|||||||
}
|
}
|
||||||
if (actor === "agent") {
|
if (actor === "agent") {
|
||||||
const parsed = typeof response === "string" ? safeParse(response) : response;
|
const parsed = typeof response === "string" ? safeParse(response) : response;
|
||||||
// Keep if next is "question", "confirm", or "user_confirmed_tool_run".
|
return true; // Adjust this logic based on your "next" field.
|
||||||
// Only skip if next is "done" (or something else).
|
|
||||||
// return !["done"].includes(parsed.next);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-grow overflow-y-auto space-y-4">
|
<div className="flex-grow flex flex-col">
|
||||||
{filtered.map((msg, idx) => {
|
{/* Main message container */}
|
||||||
|
<div className="flex-grow flex flex-col justify-end overflow-y-auto space-y-3">
|
||||||
|
{filtered.map((msg, idx) => {
|
||||||
|
const { actor, response } = msg;
|
||||||
|
|
||||||
const { actor, response } = msg;
|
if (actor === "user") {
|
||||||
|
return <MessageBubble key={idx} message={{ response }} isUser />;
|
||||||
if (actor === "user") {
|
} else if (actor === "agent") {
|
||||||
return (
|
const data =
|
||||||
<MessageBubble key={idx} message={{ response }} isUser />
|
typeof response === "string" ? safeParse(response) : response;
|
||||||
);
|
const isLastMessage = idx === filtered.length - 1;
|
||||||
} else if (actor === "agent") {
|
return (
|
||||||
const data =
|
<LLMResponse
|
||||||
typeof response === "string" ? safeParse(response) : response;
|
key={idx}
|
||||||
return <LLMResponse key={idx} data={data} onConfirm={onConfirm} />;
|
data={data}
|
||||||
}
|
onConfirm={onConfirm}
|
||||||
return null;
|
isLastMessage={isLastMessage}
|
||||||
})}
|
/>
|
||||||
|
);
|
||||||
{/* If loading = true, show the spinner at the bottom */}
|
}
|
||||||
{loading && (
|
return null; // Fallback for unsupported actors.
|
||||||
<div className="flex justify-center">
|
})}
|
||||||
<LoadingIndicator />
|
{/* Loading indicator */}
|
||||||
</div>
|
{loading && (
|
||||||
)}
|
<div className="pt-2 flex justify-center">
|
||||||
|
<LoadingIndicator />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export default function ConfirmInline({ data, confirmed, onConfirm }) {
|
|||||||
{args && (
|
{args && (
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<strong>Args:</strong>
|
<strong>Args:</strong>
|
||||||
<pre className="bg-gray-100 dark:bg-gray-700 p-1 rounded text-xs whitespace-pre-wrap">
|
<pre className="bg-gray-100 dark:bg-gray-700 p-1 rounded text-sm whitespace-pre-wrap">
|
||||||
{JSON.stringify(args, null, 2)}
|
{JSON.stringify(args, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
@@ -31,14 +31,14 @@ export default function ConfirmInline({ data, confirmed, onConfirm }) {
|
|||||||
// Not confirmed yet → show confirmation UI
|
// Not confirmed yet → show confirmation UI
|
||||||
return (
|
return (
|
||||||
<div className="mt-2 p-2 border border-gray-400 dark:border-gray-600 rounded bg-gray-50 dark:bg-gray-800">
|
<div className="mt-2 p-2 border border-gray-400 dark:border-gray-600 rounded bg-gray-50 dark:bg-gray-800">
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-300">
|
<div className="text-gray-600 dark:text-gray-300">
|
||||||
<div>
|
<div>
|
||||||
<strong>Tool:</strong> {tool ?? "Unknown"}
|
<strong>Tool:</strong> {tool ?? "Unknown"}
|
||||||
</div>
|
</div>
|
||||||
{args && (
|
{args && (
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<strong>Args:</strong>
|
<strong>Args:</strong>
|
||||||
<pre className="bg-gray-100 dark:bg-gray-700 p-1 rounded text-xs whitespace-pre-wrap">
|
<pre className="bg-gray-100 dark:bg-gray-700 p-1 rounded text-sm whitespace-pre-wrap">
|
||||||
{JSON.stringify(args, null, 2)}
|
{JSON.stringify(args, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,17 +2,16 @@ import React, { useState } from "react";
|
|||||||
import MessageBubble from "./MessageBubble";
|
import MessageBubble from "./MessageBubble";
|
||||||
import ConfirmInline from "./ConfirmInline";
|
import ConfirmInline from "./ConfirmInline";
|
||||||
|
|
||||||
export default function LLMResponse({ data, onConfirm }) {
|
export default function LLMResponse({ data, onConfirm, isLastMessage }) {
|
||||||
const [isConfirmed, setIsConfirmed] = useState(false);
|
const [isConfirmed, setIsConfirmed] = useState(false);
|
||||||
|
|
||||||
const handleConfirm = async () => {
|
const handleConfirm = async () => {
|
||||||
if (onConfirm) {
|
if (onConfirm) await onConfirm();
|
||||||
await onConfirm();
|
setIsConfirmed(true);
|
||||||
}
|
|
||||||
setIsConfirmed(true); // Update state after confirmation
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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") {
|
if (typeof data.response === "object") {
|
||||||
data.response = data.response.response;
|
data.response = data.response.response;
|
||||||
|
|||||||
@@ -16,11 +16,14 @@ export default function MessageBubble({ message, fallback = "", isUser = false }
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`max-w-xs md:max-w-sm px-4 py-2 mb-1 rounded-lg ${
|
className={`inline-block px-4 py-2 mb-1 rounded-lg ${
|
||||||
isUser ? "ml-auto" : "mr-auto"
|
isUser ? "ml-auto bg-blue-100" : "mr-auto bg-gray-200"
|
||||||
} ${bubbleStyle}`}
|
} break-words`}
|
||||||
style={{ whiteSpace: "pre-wrap" }}
|
style={{
|
||||||
>
|
whiteSpace: "pre-wrap",
|
||||||
|
maxWidth: "75%", // or '80%'
|
||||||
|
}}
|
||||||
|
>
|
||||||
{displayText}
|
{displayText}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ import React from "react";
|
|||||||
|
|
||||||
export default function NavBar({ title }) {
|
export default function NavBar({ title }) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-gray-200 dark:bg-gray-700 p-4 shadow-sm">
|
<header className="fixed top-0 left-0 w-full p-4 bg-white/70 dark:bg-gray-800/70
|
||||||
<h1 className="text-xl font-bold">{title}</h1>
|
backdrop-blur-md shadow-md z-10 flex justify-center">
|
||||||
</div>
|
<h1 className="text-xl font-bold font-poppins">{title}</h1>
|
||||||
|
{/* ...any additional nav items... */}
|
||||||
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,23 +8,26 @@ export default function App() {
|
|||||||
const [conversation, setConversation] = useState([]);
|
const [conversation, setConversation] = useState([]);
|
||||||
const [userInput, setUserInput] = useState("");
|
const [userInput, setUserInput] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [done, setDone] = useState(true); // New `done` state
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Poll /get-conversation-history once per second
|
// Poll /get-conversation-history every 0.5 seconds
|
||||||
const intervalId = setInterval(async () => {
|
const intervalId = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("http://127.0.0.1:8000/get-conversation-history");
|
const res = await fetch("http://127.0.0.1:8000/get-conversation-history");
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
// data is now an object like { messages: [ ... ] }
|
// Update conversation
|
||||||
|
|
||||||
if (data.messages && data.messages.length > 0 && (data.messages[data.messages.length - 1].actor === "agent")) {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
setLoading(true);
|
|
||||||
}
|
|
||||||
setConversation(data.messages || []);
|
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) {
|
} catch (err) {
|
||||||
console.error("Error fetching conversation history:", err);
|
console.error("Error fetching conversation history:", err);
|
||||||
@@ -37,7 +40,7 @@ export default function App() {
|
|||||||
const handleSendMessage = async () => {
|
const handleSendMessage = async () => {
|
||||||
if (!userInput.trim()) return;
|
if (!userInput.trim()) return;
|
||||||
try {
|
try {
|
||||||
setLoading(true); // <--- Mark as loading
|
setLoading(true); // Mark as loading
|
||||||
await fetch(
|
await fetch(
|
||||||
`http://127.0.0.1:8000/send-prompt?prompt=${encodeURIComponent(userInput)}`,
|
`http://127.0.0.1:8000/send-prompt?prompt=${encodeURIComponent(userInput)}`,
|
||||||
{ method: "POST" }
|
{ 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.")}`,
|
`http://127.0.0.1:8000/send-prompt?prompt=${encodeURIComponent("I'd like to travel for an event.")}`,
|
||||||
{ method: "POST" }
|
{ method: "POST" }
|
||||||
);
|
);
|
||||||
setConversation([]); // clear local state
|
setConversation([]); // Clear local state
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error ending chat:", err);
|
console.error("Error ending chat:", err);
|
||||||
}
|
}
|
||||||
@@ -78,42 +81,67 @@ export default function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col min-h-screen">
|
<div className="flex flex-col h-screen">
|
||||||
<NavBar title="Temporal AI Agent" />
|
<NavBar title="Temporal AI Agent" />
|
||||||
<div className="flex-grow flex justify-center px-4 py-6">
|
|
||||||
<div className="w-full max-w-lg bg-white dark:bg-gray-900 p-4 rounded shadow-md flex flex-col">
|
|
||||||
{/* Pass down the array of messages to ChatWindow */}
|
|
||||||
<ChatWindow
|
|
||||||
conversation={conversation}
|
|
||||||
loading={loading}
|
|
||||||
onConfirm={handleConfirm}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex items-center mt-4">
|
{/* Centered content, but no manual bottom margin */}
|
||||||
<input
|
<div className="flex-grow flex justify-center px-4 py-2 overflow-hidden">
|
||||||
type="text"
|
<div className="w-full max-w-lg bg-white dark:bg-gray-900 p-8 px-3 rounded shadow-md
|
||||||
className="flex-grow rounded-l px-3 py-2 border border-gray-300
|
flex flex-col overflow-hidden">
|
||||||
dark:bg-gray-700 dark:border-gray-600 focus:outline-none"
|
{/* Scrollable chat area */}
|
||||||
placeholder="Type your message..."
|
<div className="flex-grow overflow-y-auto pb-20 pt-10">
|
||||||
value={userInput}
|
<ChatWindow
|
||||||
onChange={(e) => setUserInput(e.target.value)}
|
conversation={conversation}
|
||||||
onKeyPress={handleKeyPress}
|
loading={loading}
|
||||||
|
onConfirm={handleConfirm}
|
||||||
/>
|
/>
|
||||||
<button
|
{done && (
|
||||||
onClick={handleSendMessage}
|
<div className="text-center text-sm text-gray-500 dark:text-gray-400 mt-4">
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-r"
|
Chat ended
|
||||||
>
|
</div>
|
||||||
Send
|
)}
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right mt-3">
|
</div>
|
||||||
<button
|
|
||||||
onClick={handleStartNewChat}
|
{/* Floating Input Section */}
|
||||||
className="text-sm underline text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200"
|
{/* Fixed bottom input */}
|
||||||
>
|
<div
|
||||||
Start New Chat
|
className="fixed bottom-0 left-1/2 transform -translate-x-1/2
|
||||||
</button>
|
w-full max-w-lg bg-white dark:bg-gray-900 p-4
|
||||||
</div>
|
border-t border-gray-300 dark:border-gray-700"
|
||||||
|
style={{ zIndex: 10 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<input
|
||||||
|
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" : ""}`}
|
||||||
|
placeholder="Type your message..."
|
||||||
|
value={userInput}
|
||||||
|
onChange={(e) => setUserInput(e.target.value)}
|
||||||
|
onKeyPress={handleKeyPress}
|
||||||
|
disabled={loading || done}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleSendMessage}
|
||||||
|
className={`bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-r
|
||||||
|
${loading || done ? "opacity-50 cursor-not-allowed" : ""}`}
|
||||||
|
disabled={loading || done}
|
||||||
|
>
|
||||||
|
Send
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<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" : ""}`}
|
||||||
|
disabled={!done}
|
||||||
|
>
|
||||||
|
Start New Chat
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: [
|
content: [
|
||||||
"./public/index.html",
|
"./index.html",
|
||||||
"./src/**/*.{js,jsx,ts,tsx}",
|
"./src/**/*.{js,jsx,ts,tsx}",
|
||||||
],
|
],
|
||||||
darkMode: "class", // enable dark mode by toggling a .dark class
|
darkMode: "class", // enable dark mode by toggling a .dark class
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ def generate_genai_prompt(
|
|||||||
"1) If any required argument is missing, set next='question' and ask the user.\n"
|
"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"
|
"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"
|
" 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."
|
"4) response should be short and user-friendly."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user