diff --git a/.env.example b/.env.example index 76d0139..c08b730 100644 --- a/.env.example +++ b/.env.example @@ -32,3 +32,6 @@ AGENT_MODEL_BASE_URL=https://ai.example.com/v1 AGENT_MODEL_API_KEY=replace-with-provider-api-key AGENT_MODEL_CONTEXT_WINDOW=262000 AGENT_MODEL_MAX_TOKENS=131072 + +# Zopu lean server (standalone chat + work actor) +VITE_ZOPU_SERVER_URL=http://localhost:3590 diff --git a/apps/web/src/components/standalone-chat/standalone-chat-page.tsx b/apps/web/src/components/standalone-chat/standalone-chat-page.tsx new file mode 100644 index 0000000..9c21fc2 --- /dev/null +++ b/apps/web/src/components/standalone-chat/standalone-chat-page.tsx @@ -0,0 +1,186 @@ +import { env } from "@code/env/web"; +import { FlueProvider, useFlueAgent } from "@flue/react"; +import { createFlueClient } from "@flue/sdk"; +import { ArrowUp, Loader2, MessageSquare } from "lucide-react"; +import { useEffect, useMemo, useRef, useState } from "react"; + +const STANDALONE_CHAT_ID = "zopu-standalone"; + +const StandaloneFlueProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const client = useMemo( + () => + createFlueClient({ + baseUrl: env.VITE_ZOPU_SERVER_URL, + }), + [] + ); + return {children}; +}; + +const extractText = (parts: { type: string; text?: string }[]): string => + parts + .filter((p) => p.type === "text" && p.text) + .map((p) => p.text ?? "") + .join(""); + +const ChatMessage = ({ + parts, + role, +}: { + parts: { type: string; text?: string }[]; + role: string; +}) => { + const isUser = role === "user"; + return ( +
+ {!isUser && ( +
+ +
+ )} +
+ {extractText(parts) || ...} +
+
+ ); +}; + +const ChatContent = () => { + const agent = useFlueAgent({ + id: STANDALONE_CHAT_ID, + name: "zopu", + }); + const [input, setInput] = useState(""); + const scrollRef = useRef(null); + + const hasMessages = agent.messages.length > 0; + const isBusy = agent.status === "submitted" || agent.status === "streaming"; + + useEffect(() => { + const el = scrollRef.current; + if (el) { + el.scrollTop = el.scrollHeight; + } + }, [agent.messages, isBusy]); + + const submit = async () => { + const message = input.trim(); + if (!message || isBusy) { + return; + } + setInput(""); + await agent.sendMessage(message); + }; + + return ( +
+
+
+ Z +
+
+

Zopu

+

+ {isBusy ? "working..." : "ready"} +

+
+
+ +
+ {!hasMessages && !agent.historyReady && ( +
+ +
+ )} + {!hasMessages && agent.historyReady && ( +
+
+ +
+
+

+ Chat with Zopu +

+

+ Ask about issues, trigger work, or check PR status. +

+
+
+ {[ + "List recent pull requests", + "Create an issue for adding a settings page", + ].map((suggestion) => ( + + ))} +
+
+ )} + {agent.messages.map((message) => ( + + ))} +
+ +
+
+