Skip to content

feat: add embed route #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 0 additions & 23 deletions chat/next.config.mjs

This file was deleted.

18 changes: 17 additions & 1 deletion chat/next.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
import type { NextConfig } from "next";
const basePath = process.env.BASE_PATH ?? "/chat";

const nextConfig: NextConfig = {
/* config options here */
// Enable static exports
output: "export",

// Disable image optimization since it's not supported in static exports
images: {
unoptimized: true,
},

// Configure base path for GitHub Pages (repo/chat)
basePath,

// Configure asset prefix for GitHub Pages - helps with static asset loading
assetPrefix: `${basePath}/`,

// Configure trailing slashes (recommended for static exports)
trailingSlash: true,
};

export default nextConfig;
19 changes: 19 additions & 0 deletions chat/src/app/embed/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Chat } from "@/components/chat";
import { ChatProvider } from "@/components/chat-provider";
import { Suspense } from "react";

export default function EmbedPage() {
return (
<Suspense
fallback={
<div className="text-center p-4 text-sm">Loading chat interface...</div>
}
>
<ChatProvider>
<div className="flex flex-col h-svh">
<Chat />
</div>
</ChatProvider>
</Suspense>
);
}
6 changes: 3 additions & 3 deletions chat/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
}

.dark {
--background: oklch(0.129 0.042 264.695);
--background: oklch(0.14 0 0);
--foreground: oklch(0.984 0.003 247.858);
--card: oklch(0.208 0.042 265.755);
--card-foreground: oklch(0.984 0.003 247.858);
Expand All @@ -90,11 +90,11 @@
--secondary: oklch(0.279 0.041 260.031);
--secondary-foreground: oklch(0.984 0.003 247.858);
--muted: oklch(0.279 0.041 260.031);
--muted-foreground: oklch(0.704 0.04 256.788);
--muted-foreground: oklch(0.7118 0.0129 286.07);
--accent: oklch(0.279 0.041 260.031);
--accent-foreground: oklch(0.984 0.003 247.858);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--border: oklch(0.27 0.01 0);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.551 0.027 264.364);
--chart-1: oklch(0.488 0.243 264.376);
Expand Down
31 changes: 31 additions & 0 deletions chat/src/app/header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"use client";

import { useChat } from "@/components/chat-provider";
import { ModeToggle } from "../components/mode-toggle";

export function Header() {
const { serverStatus } = useChat();

return (
<header className="p-4 flex items-center justify-between border-b">
<span className="font-bold">AgentAPI Chat</span>

<div className="flex items-center gap-4">
{serverStatus !== "unknown" && (
<div className="flex items-center gap-2 text-sm font-medium">
<span
className={`text-secondary w-2 h-2 rounded-full ${
["offline", "unknown"].includes(serverStatus)
? "bg-red-500 ring-2 ring-red-500/35"
: "bg-green-500 ring-2 ring-green-500/35"
}`}
/>
<span className="sr-only">Status:</span>
<span className="first-letter:uppercase">{serverStatus}</span>
</div>
)}
<ModeToggle />
</div>
</header>
);
}
13 changes: 10 additions & 3 deletions chat/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import { Chat } from "@/components/chat";
import { ChatProvider } from "@/components/chat-provider";
import { Header } from "./header";
import { Suspense } from "react";
import ChatInterface from "@/components/ChatInterface";

export default function Home() {
return (
<Suspense
fallback={
<div className="text-center p-4">Loading chat interface...</div>
<div className="text-center p-4 text-sm">Loading chat interface...</div>
}
>
<ChatInterface />
<ChatProvider>
<div className="flex flex-col h-svh">
<Header />
<Chat />
</div>
</ChatProvider>
</Suspense>
);
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
"use client";

import { useState, useEffect, useRef, useCallback } from "react";
import MessageList from "./MessageList";
import MessageInput from "./MessageInput";
import { useSearchParams } from "next/navigation";
import {
useState,
useEffect,
useRef,
createContext,
PropsWithChildren,
useContext,
} from "react";
import { toast } from "sonner";
import { Button } from "./ui/button";
import { TriangleAlertIcon } from "lucide-react";
import { Alert, AlertTitle, AlertDescription } from "./ui/alert";
import { ModeToggle } from "./mode-toggle";

interface Message {
id: number;
Expand All @@ -33,42 +34,30 @@ interface StatusChangeEvent {
status: string;
}

const isDraftMessage = (message: Message | DraftMessage): boolean => {
function isDraftMessage(message: Message | DraftMessage): boolean {
return message.id === undefined;
};
}

export default function ChatInterface() {
const [messages, setMessages] = useState<(Message | DraftMessage)[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [serverStatus, setServerStatus] = useState<string>("unknown");
const searchParams = useSearchParams();
type MessageType = "user" | "raw";

const getAgentApiUrl = useCallback(() => {
const apiUrlFromParam = searchParams.get("url");
if (apiUrlFromParam) {
try {
// Validate if it's a proper URL
new URL(apiUrlFromParam);
return apiUrlFromParam;
} catch (e) {
console.warn("Invalid url parameter, defaulting...", e);
// Fallback if parsing fails or it's not a valid URL.
// Ensure window is defined (for SSR/Node.js environments during build)
return typeof window !== "undefined" ? window.location.origin : "";
}
}
// Ensure window is defined
return typeof window !== "undefined" ? window.location.origin : "";
}, [searchParams]);
type ServerStatus = "online" | "offline" | "unknown";

const [agentAPIUrl, setAgentAPIUrl] = useState<string>(getAgentApiUrl());
interface ChatContextValue {
messages: (Message | DraftMessage)[];
loading: boolean;
serverStatus: ServerStatus;
sendMessage: (message: string, type?: MessageType) => void;
}

const eventSourceRef = useRef<EventSource | null>(null);
const ChatContext = createContext<ChatContextValue | undefined>(undefined);

// Update agentAPIUrl when searchParams change (e.g. url is added/removed)
useEffect(() => {
setAgentAPIUrl(getAgentApiUrl());
}, [getAgentApiUrl, searchParams]);
export function ChatProvider({ children }: PropsWithChildren) {
const [messages, setMessages] = useState<(Message | DraftMessage)[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [serverStatus, setServerStatus] = useState<ServerStatus>("unknown");
const eventSourceRef = useRef<EventSource | null>(null);
const searchParams = useSearchParams();
const agentAPIUrl = searchParams.get("url");

// Set up SSE connection to the events endpoint
useEffect(() => {
Expand Down Expand Up @@ -132,7 +121,7 @@ export default function ChatInterface() {
// Handle status changes
eventSource.addEventListener("status_change", (event) => {
const data: StatusChangeEvent = JSON.parse(event.data);
setServerStatus(data.status);
setServerStatus(data.status as ServerStatus);
});

// Handle connection open (server is online)
Expand Down Expand Up @@ -240,55 +229,23 @@ export default function ChatInterface() {
};

return (
<div className="flex flex-col h-svh">
<header className="p-4 flex items-center justify-between border-b">
<span className="font-bold">AgentAPI Chat</span>

<div className="flex items-center gap-4">
{serverStatus !== "unknown" && (
<div className="flex items-center gap-2 text-sm font-medium">
<span
className={`text-secondary w-2 h-2 rounded-full ${
["offline", "unknown"].includes(serverStatus)
? "bg-red-500 ring-2 ring-red-500/35"
: "bg-green-500 ring-2 ring-green-500/35"
}`}
/>
<span className="sr-only">Status:</span>
<span className="first-letter:uppercase">{serverStatus}</span>
</div>
)}
<ModeToggle />
</div>
</header>

<main className="flex flex-1 flex-col w-full overflow-auto">
{serverStatus === "offline" && (
<div className="p-4 w-full max-w-4xl mx-auto">
<Alert className="flex border-yellow-500">
<TriangleAlertIcon className="h-4 w-4 stroke-yellow-600" />
<div>
<AlertTitle>API server is offline</AlertTitle>
<AlertDescription>
Please start the AgentAPI server. Attempting to connect to:{" "}
{agentAPIUrl || "N/A"}.
</AlertDescription>
</div>
<Button
variant="ghost"
size="sm"
className="ml-auto"
onClick={() => window.location.reload()}
>
Retry
</Button>
</Alert>
</div>
)}

<MessageList messages={messages} />
<MessageInput onSendMessage={sendMessage} disabled={loading} />
</main>
</div>
<ChatContext.Provider
value={{
messages,
loading,
sendMessage,
serverStatus,
}}
>
{children}
</ChatContext.Provider>
);
}

export function useChat() {
const context = useContext(ChatContext);
if (!context) {
throw new Error("useChat must be used within a ChatProvider");
}
return context;
}
16 changes: 16 additions & 0 deletions chat/src/components/chat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"use client";

import { useChat } from "./chat-provider";
import MessageInput from "./message-input";
import MessageList from "./message-list";

export function Chat() {
const { messages, loading, sendMessage } = useChat();

return (
<>
<MessageList messages={messages} />
<MessageInput onSendMessage={sendMessage} disabled={loading} />
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ export default function MessageInput({
onKeyDown={handleKeyDown as any}
onFocus={() => setControlAreaFocused(true)}
onBlur={() => setControlAreaFocused(false)}
className="cursor-text p-4 h-20 text-muted-foreground flex items-center justify-center w-full outline-none"
className="cursor-text p-4 h-20 text-muted-foreground flex items-center justify-center w-full outline-none text-sm"
>
{controlAreaFocused
? "Press any key to send to terminal (arrows, Ctrl+C, Ctrl+R, etc.)"
Expand All @@ -175,7 +175,7 @@ export default function MessageInput({
</div>

<div className="flex items-center justify-between p-4">
<TabsList>
<TabsList className="bg-transparent">
<TabsTrigger
value="text"
onClick={() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export default function MessageList({ messages }: MessageListProps) {
} ${message.id === undefined ? "animate-pulse" : ""}`}
>
<div
className={`whitespace-pre-wrap break-words text-left text-sm ${
className={`whitespace-pre-wrap break-words text-left text-xs md:text-sm leading-relaxed md:leading-normal ${
message.role === "user" ? "" : "font-mono"
}`}
>
Expand Down