diff --git a/frontend/src/app/components/ChatInterface.tsx b/frontend/src/app/components/ChatInterface.tsx index 3f696ff..c6fdf26 100644 --- a/frontend/src/app/components/ChatInterface.tsx +++ b/frontend/src/app/components/ChatInterface.tsx @@ -1,3 +1,13 @@ +import React, { useRef, useEffect } from 'react'; +import { Bot, Send, CheckCircle2, AlertCircle } from 'lucide-react'; +import type { Message } from '../../types/chat'; + +interface ChatInterfaceProps { + messages: Message[]; + inputState: string; + setInputState: (value: string) => void; + isTyping: boolean; + handleSendMessage: (e: React.FormEvent) => void; "use client"; import React, { useEffect, useRef, useState } from "react"; @@ -20,6 +30,69 @@ export interface ChatInterfaceProps { export function ChatInterface({ messages, + inputState, + setInputState, + isTyping, + handleSendMessage, +}: ChatInterfaceProps) { + const messagesEndRef = useRef(null); + + // Auto scroll + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages, isTyping]); + + return ( +
+
+
+
+ +
+
+

OpenClaw Agent

+
+ Online +
+
+
+ +
+ {messages.map((msg) => ( +
+ {msg.proactive && ( +
+ Proactive Nudge +
+ )} +
{msg.text}
+
+ ))} + {isTyping && ( +
+
+ +
+
+ )} +
+
+ +
+ setInputState(e.target.value)} + /> + +
+
+
+ ); +} isTyping, onSendMessage, placeholder = "Ask Smasage to adjust goals...", diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index f5f6967..d19c6ce 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,3 +1,32 @@ +"use client" +import React, { useState } from 'react'; +import { Target, Activity } from 'lucide-react'; +import { PortfolioStats } from './components/PortfolioStats'; +import { evaluateGoalStatus, getStatusColor, type GoalData } from '../utils/goalProjection'; +import PortfolioChart from './PortfolioChart'; +import { DashboardHeader } from './components/DashboardHeader'; +import { ConnectWalletButton } from './components/ConnectWalletButton'; +import { useWallet } from './components/WalletContext'; +import { ErrorBoundary } from './components/ErrorBoundary'; +import { useChat } from '../hooks/useChat'; +import { ChatInterface } from './components/ChatInterface'; + +export default function Home() { + const { publicKey, setPublicKey } = useWallet(); + const [showInstallModal, setShowInstallModal] = useState(false); + // Connect Wallet logic + const handleConnectWallet = async () => { + if (!window.freighterApi) { + setShowInstallModal(true); + return; + } + try { + const key = await window.freighterApi.getPublicKey(); + setPublicKey(key); + } catch { + // Optionally handle rejection + } + }; "use client"; import React, { useState, useEffect, useMemo } from "react"; import { Activity } from "lucide-react"; @@ -73,6 +102,21 @@ export default function Home() { }; }, [goalData]); + // Calculate goal status + const goalResult = evaluateGoalStatus(goalData); + const progress = goalResult.progressPercentage; + const goalStatus = goalResult.status; + + const { messages, inputState, setInputState, isTyping, handleSendMessage, allocations, wsConnected } = useChat({ + userId: 'user-demo-001', + goalData, + }); + + return ( + + <> + + // WebSocket notifications const { registerGoal } = useNotifications({ userId: "user-demo-001", @@ -233,6 +277,15 @@ export default function Home() {
+ {/* Right Panel - Chat Agent */} + + {/* Right Panel - Chat Agent */}
([ + { 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(getDefaultAllocations()); + + const { registerGoal, isConnected } = useNotifications({ + userId, + onNotification: (notification) => { + if (notification.type === 'agent-message') { + const payload = notification.payload as { text: string; proactive?: boolean; timestamp?: string; }; + const agentMsg: Message = { + id: Date.now(), + sender: 'agent', + text: payload.text, + proactive: payload.proactive, + timestamp: payload.timestamp, + }; + setMessages(prev => [...prev, agentMsg]); + + // Parse allocations if present + const parsedAllocations = parseAllocationsFromMessage(payload.text); + if (parsedAllocations) { + setAllocations(parsedAllocations); + } + } + }, + onError: (error) => { + console.error('[Chat] WebSocket error:', error); + }, + enabled: true, + }); + + // Register goal when connected + useEffect(() => { + if (isConnected) { + registerGoal({ + currentBalance: goalData.currentBalance, + targetAmount: goalData.targetAmount, + targetDate: goalData.targetDate, + expectedAPY: goalData.expectedAPY, + monthlyContribution: goalData.monthlyContribution, + }); + } + }, [isConnected, registerGoal, goalData]); + + const handleSendMessage = useCallback((e: React.FormEvent) => { + e.preventDefault(); + if (!inputState.trim()) return; + + const userMsg: Message = { id: Date.now(), sender: 'user', text: inputState }; + setMessages(prev => [...prev, userMsg]); + setInputState(''); + setIsTyping(true); + + // Mock agent response delay + setTimeout(() => { + setIsTyping(false); + const agentMsg: Message = { + 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]); + + // Parse allocations from agent message + const parsedAllocations = parseAllocationsFromMessage(agentMsg.text); + if (parsedAllocations) { + setAllocations(parsedAllocations); + } + }, 1800); + }, [inputState]); + + return { + messages, + inputState, + setInputState, + isTyping, + handleSendMessage, + allocations, + wsConnected: isConnected, + }; +} \ No newline at end of file diff --git a/frontend/src/types/chat.ts b/frontend/src/types/chat.ts new file mode 100644 index 0000000..ae547bc --- /dev/null +++ b/frontend/src/types/chat.ts @@ -0,0 +1,7 @@ +export interface Message { + id: number; + sender: 'agent' | 'user'; + text: string; + proactive?: boolean; + timestamp?: string; +} \ No newline at end of file