basic react API

This commit is contained in:
Steve Androulakis
2025-01-02 18:05:28 -08:00
parent 9b139ee479
commit 93ec96a406
21 changed files with 3498 additions and 24 deletions

View File

@@ -0,0 +1,66 @@
import React from "react";
import LLMResponse from "./LLMResponse";
import MessageBubble from "./MessageBubble";
import LoadingIndicator from "./LoadingIndicator";
function safeParse(str) {
try {
return JSON.parse(str);
} catch (err) {
console.error("safeParse error:", err, "Original string:", str);
return {};
}
}
export default function ChatWindow({ conversation, loading, onConfirm }) {
if (!Array.isArray(conversation)) {
console.error("ChatWindow expected conversation to be an array, got:", conversation);
return null;
}
const filtered = conversation.filter((msg) => {
const { actor, response } = msg;
if (actor === "user") {
return true;
}
if (actor === "response") {
const parsed = typeof response === "string" ? safeParse(response) : response;
// Keep if next is "question", "confirm", or "confirmed".
// Only skip if next is "done" (or something else).
return !["done"].includes(parsed.next);
}
return false;
});
return (
<div className="flex-grow overflow-y-auto space-y-4">
{filtered.map((msg, idx) => {
const { actor, response } = msg;
if (actor === "user") {
return (
<MessageBubble key={idx} message={{ response }} isUser />
);
} else if (actor === "response") {
const data =
typeof response === "string" ? safeParse(response) : response;
return <LLMResponse key={idx} data={data} onConfirm={onConfirm} />;
}
return null;
})}
{/* If loading = true, show the spinner at the bottom */}
{loading && (
<div className="flex justify-center">
<LoadingIndicator />
</div>
)}
{conversation.length > 0 && conversation[conversation.length - 1].actor === "user" && (
<div className="flex justify-center">
<LoadingIndicator />
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,59 @@
import React from "react";
import LoadingIndicator from "./LoadingIndicator";
export default function ConfirmInline({ data, confirmed, onConfirm }) {
const { args, tool } = data || {};
console.log("ConfirmInline rendered with confirmed:", confirmed);
if (confirmed) {
// Once confirmed, show "Running..." state in the same container
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="text-sm text-gray-600 dark:text-gray-300">
<div>
<strong>Tool:</strong> {tool ?? "Unknown"}
</div>
{args && (
<div className="mt-1">
<strong>Args:</strong>
<pre className="bg-gray-100 dark:bg-gray-700 p-1 rounded text-xs whitespace-pre-wrap">
{JSON.stringify(args, null, 2)}
</pre>
</div>
)}
</div>
<div className="mt-2 text-green-600 dark:text-green-400 font-medium">
Running {tool}...
</div>
</div>
);
}
// Not confirmed yet → show confirmation UI
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="text-sm text-gray-600 dark:text-gray-300">
<div>
<strong>Tool:</strong> {tool ?? "Unknown"}
</div>
{args && (
<div className="mt-1">
<strong>Args:</strong>
<pre className="bg-gray-100 dark:bg-gray-700 p-1 rounded text-xs whitespace-pre-wrap">
{JSON.stringify(args, null, 2)}
</pre>
</div>
)}
</div>
<div className="text-right mt-2">
<button
onClick={onConfirm}
className="bg-green-600 hover:bg-green-700 text-white px-3 py-1 rounded"
>
Confirm
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,34 @@
import React, { useState } from "react";
import MessageBubble from "./MessageBubble";
import ConfirmInline from "./ConfirmInline";
export default function LLMResponse({ data, onConfirm }) {
const [isConfirmed, setIsConfirmed] = useState(false);
const handleConfirm = async () => {
if (onConfirm) {
await onConfirm();
}
setIsConfirmed(true); // Update state after confirmation
};
const requiresConfirm = data.next === "confirm";
let displayText = (data.response || "").trim();
if (!displayText && requiresConfirm) {
displayText = `Agent is ready to run "${data.tool}". Please confirm.`;
}
return (
<div className="space-y-2">
<MessageBubble message={{ response: displayText }} />
{requiresConfirm && (
<ConfirmInline
data={data}
confirmed={isConfirmed}
onConfirm={handleConfirm}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,11 @@
import React from "react";
export default function LoadingIndicator() {
return (
<div className="flex items-center justify-center space-x-2">
<div className="w-2 h-2 rounded-full bg-blue-600 animate-ping"></div>
<div className="w-2 h-2 rounded-full bg-blue-600 animate-ping delay-100"></div>
<div className="w-2 h-2 rounded-full bg-blue-600 animate-ping delay-200"></div>
</div>
);
}

View File

@@ -0,0 +1,27 @@
import React from "react";
export default function MessageBubble({ message, fallback = "", isUser = false }) {
// Use isUser directly instead of message.user
const bubbleStyle = isUser
? "bg-blue-600 text-white self-end"
: "bg-gray-300 text-gray-900 dark:bg-gray-600 dark:text-gray-100";
// If message.response is empty or whitespace, use fallback text
const displayText = message.response?.trim() ? message.response : fallback;
// Skip display entirely if text starts with ###
if (displayText.startsWith("###")) {
return null;
}
return (
<div
className={`max-w-xs md:max-w-sm px-4 py-2 mb-1 rounded-lg ${
isUser ? "ml-auto" : "mr-auto"
} ${bubbleStyle}`}
style={{ whiteSpace: "pre-wrap" }}
>
{displayText}
</div>
);
}

View File

@@ -0,0 +1,9 @@
import React from "react";
export default function NavBar({ title }) {
return (
<div className="bg-gray-200 dark:bg-gray-700 p-4 shadow-sm">
<h1 className="text-xl font-bold">{title}</h1>
</div>
);
}