From 198edf585707af665c0b9a46c18f2c35a3336780 Mon Sep 17 00:00:00 2001 From: Xi Zhang Date: Mon, 1 Jun 2026 21:52:19 +0100 Subject: [PATCH] feat: update project structure and enhance UI components with new features and improvements --- .prettierignore | 1 + package-lock.json | 26 ++-- package.json | 3 +- scripts/assemble-standalone.mjs | 14 +- src/app/components/ChatInterface.tsx | 155 ++++++++++++++++++- src/app/components/FileViewDialog.tsx | 4 +- src/app/components/SkillsMarketplace.tsx | 16 +- src/app/components/ThreadList.tsx | 124 +++++++++++---- src/app/components/ToolApprovalInterrupt.tsx | 8 +- src/app/globals.css | 23 ++- src/app/page.tsx | 69 +++++++-- src/components/ui/button.tsx | 2 +- src/components/ui/dialog.tsx | 2 +- 13 files changed, 367 insertions(+), 80 deletions(-) diff --git a/.prettierignore b/.prettierignore index 98a1358..939f29b 100644 --- a/.prettierignore +++ b/.prettierignore @@ -17,6 +17,7 @@ out # production build +dist # vercel .vercel diff --git a/package-lock.json b/package-lock.json index 4883b87..9d82fe9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,13 @@ { - "name": "evoscientist-webui", - "version": "0.1.0", + "name": "@evoscientist/webui", + "version": "0.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "evoscientist-webui", - "version": "0.1.0", + "name": "@evoscientist/webui", + "version": "0.0.2", + "license": "Apache-2.0", "dependencies": { "@langchain/core": "1.1.19", "@langchain/langgraph-sdk": "1.0.3", @@ -19,7 +20,6 @@ "@types/uuid": "^9.0.8", "class-variance-authority": "^0.7.1", "clsx": "^1.2.1", - "date-fns": "^4.1.0", "lucide-react": "^0.539.0", "next": "^16.2.5", "nuqs": "^2.8.8", @@ -35,6 +35,9 @@ "use-stick-to-bottom": "^1.1.1", "uuid": "^9.0.1" }, + "bin": { + "evoscientist-webui": "bin/evoscientist-webui.mjs" + }, "devDependencies": { "@eslint/js": "^9", "@tailwindcss/forms": "^0.5.7", @@ -54,6 +57,9 @@ "tailwindcss-animate": "^1.0.7", "typescript": "^5.9.3", "typescript-eslint": "^8.54.0" + }, + "engines": { + "node": ">=20" } }, "node_modules/@alloc/quick-lru": { @@ -3874,16 +3880,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/date-fns": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.4.0.tgz", - "integrity": "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/kossnocorp" - } - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", diff --git a/package.json b/package.json index 20bdd28..732c866 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@evoscientist/webui", - "version": "0.0.1", + "version": "0.0.2", "description": "Web UI for EvoScientist — a self-evolving AI scientist built on DeepAgents/LangGraph.", "type": "module", "license": "Apache-2.0", @@ -49,7 +49,6 @@ "@types/uuid": "^9.0.8", "class-variance-authority": "^0.7.1", "clsx": "^1.2.1", - "date-fns": "^4.1.0", "lucide-react": "^0.539.0", "next": "^16.2.5", "nuqs": "^2.8.8", diff --git a/scripts/assemble-standalone.mjs b/scripts/assemble-standalone.mjs index b2be1fd..ebc9229 100644 --- a/scripts/assemble-standalone.mjs +++ b/scripts/assemble-standalone.mjs @@ -42,6 +42,18 @@ for (const entry of await readdir(OUT)) { } } +// Strip source maps (*.map): debug-only artifacts the published runtime never +// needs. Removing them keeps the package lean and avoids shipping a stack-trace +// → source mapping. Safe to delete — source maps are never required at runtime. +let strippedMaps = 0; +for (const entry of await readdir(OUT, { recursive: true })) { + if (entry.endsWith(".map")) { + await rm(`${OUT}/${entry}`, { force: true }); + strippedMaps++; + } +} + console.log( - `✓ Assembled standalone server into ${OUT}/ (run: node ${OUT}/server.js)` + `✓ Assembled standalone server into ${OUT}/ ` + + `(${strippedMaps} source map(s) stripped; run: node ${OUT}/server.js)` ); diff --git a/src/app/components/ChatInterface.tsx b/src/app/components/ChatInterface.tsx index 32d78ab..d9e01b4 100644 --- a/src/app/components/ChatInterface.tsx +++ b/src/app/components/ChatInterface.tsx @@ -18,6 +18,7 @@ import { Circle, FileIcon, ShieldCheck, + TriangleAlert, } from "lucide-react"; import { ChatMessage } from "@/app/components/ChatMessage"; import { @@ -36,11 +37,26 @@ import { useChatContext } from "@/providers/ChatProvider"; import { cn } from "@/lib/utils"; import { useStickToBottom } from "use-stick-to-bottom"; import { FilesPopover } from "@/app/components/TasksFilesSidebar"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { useQueryState } from "nuqs"; interface ChatInterfaceProps { assistant: Assistant | null; } +const SUGGESTED_PROMPTS = [ + "Find papers for a research topic", + "Plan an experiment pipeline", + "Brainstorm research directions", +]; + const getStatusIcon = (status: TodoItem["status"], className?: string) => { switch (status) { case "completed": @@ -74,7 +90,11 @@ export const ChatInterface = React.memo(({ assistant }) => { const [input, setInput] = useState(""); const [autoApprove, setAutoApprove] = useState(false); + const [autoApproveDialogOpen, setAutoApproveDialogOpen] = useState(false); const autoApprovedRef = useRef(null); + const [threadId] = useQueryState("threadId"); + const previousThreadIdRef = useRef(threadId); + const preserveAutoApproveForNewThreadRef = useRef(false); const { scrollRef, contentRef } = useStickToBottom(); const { @@ -105,6 +125,27 @@ export const ChatInterface = React.memo(({ assistant }) => { interruptValue.action_requests.length > 0); const submitDisabled = isLoading || !assistant || hasPendingInterrupt; + const turnOffAutoApprove = useCallback(() => { + setAutoApprove(false); + setAutoApproveDialogOpen(false); + autoApprovedRef.current = null; + }, []); + + useEffect(() => { + const previousThreadId = previousThreadIdRef.current; + if (previousThreadId === threadId) return; + + const preserveForNewThread = + previousThreadId === null && + threadId !== null && + preserveAutoApproveForNewThreadRef.current; + + if (!preserveForNewThread) turnOffAutoApprove(); + + preserveAutoApproveForNewThreadRef.current = false; + previousThreadIdRef.current = threadId; + }, [threadId, turnOffAutoApprove]); + const handleSubmit = useCallback( (e?: FormEvent) => { if (e) { @@ -112,10 +153,21 @@ export const ChatInterface = React.memo(({ assistant }) => { } const messageText = input.trim(); if (!messageText || isLoading || submitDisabled) return; + if (!threadId && autoApprove) { + preserveAutoApproveForNewThreadRef.current = true; + } sendMessage(messageText); setInput(""); }, - [input, isLoading, sendMessage, setInput, submitDisabled] + [ + autoApprove, + input, + isLoading, + sendMessage, + setInput, + submitDisabled, + threadId, + ] ); const handleKeyDown = useCallback( @@ -145,6 +197,11 @@ export const ChatInterface = React.memo(({ assistant }) => { }); }, []); + const handleSuggestedPrompt = useCallback((prompt: string) => { + setInput(prompt); + requestAnimationFrame(() => textareaRef.current?.focus()); + }, []); + // Auto-approve: when enabled, approve any pending tool-execution interrupt // for the rest of this conversation (each interrupt is handled once). useEffect(() => { @@ -318,6 +375,48 @@ export const ChatInterface = React.memo(({ assistant }) => { return (
+ + + + Enable Auto-approve? + + EvoScientist will run tool actions in this research without asking + you to review each one. Turn this on only when you trust the + current task and deployment. + + +
+
+ + + + +
+
(({ assistant }) => {
) : ( <> + {processedMessages.length === 0 && !isLoading && !threadId && ( +
+

+ Start a Research Conversation +

+

+ Ask EvoScientist to explore literature, shape an experiment, + or develop a research direction. +

+
+ {SUGGESTED_PROMPTS.map((prompt) => ( + + ))} +
+
+ )} {processedMessages.map((data, index) => { const messageUi = ui?.filter( (u: any) => u.metadata?.message_id === data.message.id @@ -588,6 +710,27 @@ export const ChatInterface = React.memo(({ assistant }) => { )}
)} + {autoApprove && ( +
+ + + +
+ )}
(({ assistant }) => {