diff --git a/src/app/components/ChatInterface.tsx b/src/app/components/ChatInterface.tsx index c3691a9..3d5fa1f 100644 --- a/src/app/components/ChatInterface.tsx +++ b/src/app/components/ChatInterface.tsx @@ -110,6 +110,19 @@ export const ChatInterface = React.memo(({ assistant }) => { [handleSubmit, submitDisabled] ); + // Pull a previous user message back into the composer to edit/resend it, + // placing the cursor at the end so the user can keep typing. + const handleEditMessage = useCallback((content: string) => { + setInput(content); + requestAnimationFrame(() => { + const el = textareaRef.current; + if (el) { + el.focus(); + el.setSelectionRange(content.length, content.length); + } + }); + }, []); + // TODO: can we make this part of the hook? const processedMessages = useMemo(() => { /* @@ -253,7 +266,7 @@ export const ChatInterface = React.memo(({ assistant }) => { > {isThreadLoading ? (
-

Loading...

+

Loading…

) : ( <> @@ -278,6 +291,7 @@ export const ChatInterface = React.memo(({ assistant }) => { stream={stream} onResumeInterrupt={resumeInterrupt} graphId={assistant?.graph_id} + onEditMessage={handleEditMessage} /> ); })} @@ -290,7 +304,8 @@ export const ChatInterface = React.memo(({ assistant }) => {
{(hasTasks || hasFiles) && ( @@ -393,7 +408,7 @@ export const ChatInterface = React.memo(({ assistant }) => { > Files (State) - + {Object.keys(files).length} @@ -439,7 +454,7 @@ export const ChatInterface = React.memo(({ assistant }) => { aria-expanded={metaOpen === "files"} > Files (State) - + {Object.keys(files).length} @@ -509,7 +524,8 @@ export const ChatInterface = React.memo(({ assistant }) => { value={input} onChange={(e) => setInput(e.target.value)} onKeyDown={handleKeyDown} - placeholder={isLoading ? "Running..." : "Write your message..."} + aria-label="Message" + placeholder={isLoading ? "Running…" : "Write your message…"} className="font-inherit field-sizing-content flex-1 resize-none border-0 bg-transparent px-[18px] pb-[13px] pt-[14px] text-sm leading-7 text-primary outline-none placeholder:text-tertiary" rows={1} /> diff --git a/src/app/components/ChatMessage.tsx b/src/app/components/ChatMessage.tsx index 48eadde..a9a8c5b 100644 --- a/src/app/components/ChatMessage.tsx +++ b/src/app/components/ChatMessage.tsx @@ -11,6 +11,7 @@ import type { ReviewConfig, } from "@/app/types/types"; import { Message } from "@langchain/langgraph-sdk"; +import { Brain, Check, ChevronRight, Copy, Pencil } from "lucide-react"; import { extractSubAgentContent, extractStringFromMessageContent, @@ -27,6 +28,7 @@ interface ChatMessageProps { stream?: any; onResumeInterrupt?: (value: any) => void; graphId?: string; + onEditMessage?: (content: string) => void; } export const ChatMessage = React.memo( @@ -40,11 +42,19 @@ export const ChatMessage = React.memo( stream, onResumeInterrupt, graphId, + onEditMessage, }) => { const isUser = message.type === "human"; const messageContent = extractStringFromMessageContent(message); const hasContent = messageContent && messageContent.trim() !== ""; const hasToolCalls = toolCalls.length > 0; + // Extended-thinking / reasoning text (Anthropic & friends store it here). + const reasoning = useMemo(() => { + const r = ( + message.additional_kwargs as Record | undefined + )?.reasoning_content; + return typeof r === "string" && r.trim() ? r.trim() : null; + }, [message.additional_kwargs]); const subAgents = useMemo(() => { return toolCalls .filter((toolCall: ToolCall) => { @@ -70,6 +80,15 @@ export const ChatMessage = React.memo( }); }, [toolCalls]); + const [thinkingOpen, setThinkingOpen] = useState(false); + const [copied, setCopied] = useState(false); + const handleCopy = useCallback(() => { + if (!messageContent) return; + navigator.clipboard?.writeText(messageContent).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }); + }, [messageContent]); const [expandedSubAgents, setExpandedSubAgents] = useState< Record >({}); @@ -87,7 +106,7 @@ export const ChatMessage = React.memo( return (
@@ -97,6 +116,34 @@ export const ChatMessage = React.memo( isUser ? "max-w-[70%]" : "w-full" )} > + {!isUser && reasoning && ( +
+ + {thinkingOpen && ( +
+ {reasoning} +
+ )} +
+ )} {hasContent && (
(
)} + {!isUser && hasContent && ( +
+ +
+ )} + {isUser && hasContent && ( +
+ + +
+ )} {hasToolCalls && (
{toolCalls.map((toolCall: ToolCall) => { diff --git a/src/app/components/ConfigDialog.tsx b/src/app/components/ConfigDialog.tsx index 04b85f4..41880cc 100644 --- a/src/app/components/ConfigDialog.tsx +++ b/src/app/components/ConfigDialog.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { Dialog, DialogContent, @@ -30,21 +30,33 @@ export function ConfigDialog({ const [deploymentUrl, setDeploymentUrl] = useState( initialConfig?.deploymentUrl || "http://127.0.0.1:6174" ); + const [error, setError] = useState(null); + const inputRef = useRef(null); useEffect(() => { - if (open && initialConfig) { - setDeploymentUrl(initialConfig.deploymentUrl); + if (open) { + setError(null); + if (initialConfig) setDeploymentUrl(initialConfig.deploymentUrl); } }, [open, initialConfig]); const handleSave = () => { - if (!deploymentUrl) { - alert("Please enter the deployment URL"); + const url = deploymentUrl.trim(); + if (!url) { + setError("Enter your deployment URL to continue."); + inputRef.current?.focus(); + return; + } + try { + new URL(url); + } catch { + setError("Enter a valid URL, e.g. http://127.0.0.1:6174"); + inputRef.current?.focus(); return; } onSave({ - deploymentUrl, + deploymentUrl: url, // Fixed to the EvoScientist main agent (see DEFAULT_ASSISTANT_ID). assistantId: DEFAULT_ASSISTANT_ID, }); @@ -69,11 +81,36 @@ export function ConfigDialog({
setDeploymentUrl(e.target.value)} + onChange={(e) => { + setDeploymentUrl(e.target.value); + if (error) setError(null); + }} + onKeyDown={(e) => { + if (e.key === "Enter") handleSave(); + }} + aria-invalid={error ? true : undefined} + aria-describedby={error ? "deploymentUrl-error" : undefined} /> + {error && ( + + )}
diff --git a/src/app/globals.css b/src/app/globals.css index 5a3f4bb..409a78e 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -36,6 +36,12 @@ --color-file-button: #ffffff; --color-file-button-hover: #e5e7eb; + /* Brand accent — single source of truth. Change this to re-theme + (e.g. EvoScientist blue) in one place. */ + --brand: #2f6868; + --brand-hover: #275757; + --brand-foreground: #ffffff; + /* Dark theme colors */ --color-primary-dark: #2dd4bf; --color-user-message-dark: #076699; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e8de5e3..e2ccd98 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,4 +1,4 @@ -import type { Metadata } from "next"; +import type { Metadata, Viewport } from "next"; import { Inter } from "next/font/google"; import { NuqsAdapter } from "nuqs/adapters/next/app"; import { Toaster } from "sonner"; @@ -12,6 +12,11 @@ export const metadata: Metadata = { "Web UI for EvoScientist — a self-evolving AI scientist built on DeepAgents/LangGraph.", }; +export const viewport: Viewport = { + themeColor: "#f9f9f9", + colorScheme: "light", +}; + export default function RootLayout({ children, }: { diff --git a/src/app/page.tsx b/src/app/page.tsx index 792f127..64582d4 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -131,7 +131,10 @@ function HomePageInner({ onClick={() => setSidebar("1")} className="rounded-md border border-border bg-card p-3 text-foreground hover:bg-accent" > - +
diff --git a/src/lib/config.ts b/src/lib/config.ts index ea859af..9a5b4e9 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -19,6 +19,10 @@ export function getConfig(): DeploymentConfig | null { try { const parsed = JSON.parse(stored) as DeploymentConfig; + // A stored config with no usable deployment URL would make the SDK send + // requests to the app's own origin (404s). Treat it as unconfigured so + // the config dialog reappears instead. + if (!parsed.deploymentUrl?.trim()) return null; // Always pin the assistant to the EvoScientist main agent. return { ...parsed, assistantId: DEFAULT_ASSISTANT_ID }; } catch {