diff --git a/frontend/src/app/components/ChatInterface.tsx b/frontend/src/app/components/ChatInterface.tsx new file mode 100644 index 0000000..3f696ff --- /dev/null +++ b/frontend/src/app/components/ChatInterface.tsx @@ -0,0 +1,129 @@ +"use client"; + +import React, { useEffect, useRef, useState } from "react"; +import { AlertCircle, Bot, CheckCircle2, Send } from "lucide-react"; + +export interface ChatMessage { + id: number; + sender: "agent" | "user"; + text: string; + proactive?: boolean; + timestamp?: string; +} + +export interface ChatInterfaceProps { + messages: ChatMessage[]; + isTyping: boolean; + onSendMessage: (message: string) => void; + placeholder?: string; +} + +export function ChatInterface({ + messages, + isTyping, + onSendMessage, + placeholder = "Ask Smasage to adjust goals...", +}: ChatInterfaceProps) { + const [inputValue, setInputValue] = useState(""); + const messagesEndRef = useRef(null); + + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages, isTyping]); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const trimmed = inputValue.trim(); + if (!trimmed) return; + + onSendMessage(trimmed); + setInputValue(""); + }; + + return ( +
+
+ +
+

OpenClaw Agent

+
+
+
+
+ +
+ {messages.map((msg) => ( +
+ {msg.proactive && ( +
+
+ )} +
{msg.text}
+
+ ))} + + {isTyping && ( +
+ Agent is typing... + +
+ )} + +
+
+ +
+ setInputValue(e.target.value)} + aria-label="Message input" + /> + +
+
+ ); +} diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index e23dfc7..deee427 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -472,7 +472,9 @@ h2 { .agent .message-bubble { background: rgba(255, 255, 255, 0.05); - border: 1px solid var(--border); + border: 1px solid rgba(255, 255, 255, 0.1); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); border-bottom-left-radius: 4px; color: var(--text-main); } @@ -501,7 +503,7 @@ h2 { border-radius: 12px; border-bottom-left-radius: 4px; width: fit-content; - border: 1px solid var(--border); + border: 1px solid rgba(255, 255, 255, 0.1); } .typing-indicator span { @@ -621,7 +623,7 @@ h2 { outline: none; /* replaced by explicit ring below */ border-color: var(--accent-primary); background: rgba(0, 0, 0, 0.45); - box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.15), inset 0 0 8px rgba(139, 92, 246, 0.1); + box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.15); } .chat-input-container input:focus-visible { diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 26add5a..c3daaf1 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,13 +1,6 @@ "use client"; -import React, { useState, useEffect, useRef, useMemo } from "react"; -import { - Bot, - Send, - Target, - Activity, - CheckCircle2, - AlertCircle, -} from "lucide-react"; +import React, { useState, useEffect, useMemo } from "react"; +import { Target, Activity } from "lucide-react"; import { PortfolioStats } from "./components/PortfolioStats"; import { evaluateGoalStatus, @@ -35,14 +28,7 @@ import { PortfolioChartSkeleton, } from "./components/SkeletonLoader"; import { WalletModal } from "./components/WalletModal"; - -interface Message { - id: number; - sender: "agent" | "user"; - text: string; - proactive?: boolean; - timestamp?: string; -} +import { ChatInterface, type ChatMessage } from "./components/ChatInterface"; export default function Home() { const { @@ -53,14 +39,13 @@ export default function Home() { isConnecting } = useFreighter(); - const [messages, setMessages] = useState([ + const [messages, setMessages] = useState([ { id: 1, sender: "agent", text: "Welcome to Smasage! 👋 I'm OpenClaw, your personal AI savings assistant natively built on Stellar. What financial goal can we crush today?", }, ]); - const [inputState, setInputState] = useState(""); const [isTyping, setIsTyping] = useState(false); const [allocations, setAllocations] = useState( @@ -69,7 +54,6 @@ export default function Home() { const [wsConnected, setWsConnected] = useState(false); const [isLoading, setIsLoading] = useState(true); - const messagesEndRef = useRef(null); // Goal data (Memoized to avoid unnecessary effect triggers) const goalData = useMemo(() => ({ @@ -100,14 +84,14 @@ export default function Home() { } else if (isAgentMessageNotification(notification)) { // payload is fully typed as AgentMessagePayload — no cast needed const { text, proactive, timestamp } = notification.payload; - const agentMsg: Message = { + const agentMsg: ChatMessage = { id: Date.now(), sender: "agent", text, proactive, timestamp, }; - setMessages((prev) => [...prev, agentMsg]); + setMessages((prev: ChatMessage[]) => [...prev, agentMsg]); // Parse allocations if present const parsedAllocations = parseAllocationsFromMessage(text); @@ -128,11 +112,6 @@ export default function Home() { return () => clearTimeout(t); }, []); - // Auto scroll - useEffect(() => { - messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); - }, [messages, isTyping]); - // Register goal with notification server on mount useEffect(() => { if (wsConnected) { @@ -146,28 +125,27 @@ export default function Home() { } }, [wsConnected, registerGoal, goalData]); - const handleSendMessage = (e: React.FormEvent) => { - e.preventDefault(); - if (!inputState.trim()) return; + const handleSendMessage = (message: string) => { + const trimmed = message.trim(); + if (!trimmed) return; - const userMsg: Message = { + const userMsg: ChatMessage = { id: Date.now(), sender: "user", - text: inputState, + text: trimmed, }; - setMessages((prev) => [...prev, userMsg]); - setInputState(""); + setMessages((prev: ChatMessage[]) => [...prev, userMsg]); setIsTyping(true); // Mock agent response delay setTimeout(() => { setIsTyping(false); - const agentMsg: Message = { + const agentMsg: ChatMessage = { id: Date.now() + 1, sender: "agent", text: "That's a great goal. I'll allocate 60% to Stellar Blend for safe yield, and the rest to Soroswap XLM/USDC LP to accelerate returns. Does that sound good?", }; - setMessages((prev) => [...prev, agentMsg]); + setMessages((prev: ChatMessage[]) => [...prev, agentMsg]); // Parse allocations from agent message const parsedAllocations = parseAllocationsFromMessage(agentMsg.text); @@ -299,97 +277,11 @@ export default function Home() { {/* Right Panel - Chat Agent */}
-
-
- -
-

- OpenClaw Agent -

-
-
-
-
- -
- {messages.map((msg) => ( -
- {msg.proactive && ( -
-
- )} -
{msg.text}
-
- ))} - {isTyping && ( -
- Agent is typing… - -
- )} -
-
- -
- setInputState(e.target.value)} - aria-label="Message input" - /> - -
-
+