Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions frontend/src/app/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { ButtonHTMLAttributes, JSX } from "react";

export type ButtonVariant = "primary" | "secondary";

export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
isLoading?: boolean;
loadingLabel?: string;
}

export function Button({
variant = "primary",
isLoading = false,
disabled,
className,
children,
loadingLabel = "Loading",
...rest
}: ButtonProps): JSX.Element {
const classes = ["btn", `btn-${variant}`, className].filter(Boolean).join(" ");

return (
<button
className={classes}
disabled={disabled || isLoading}
aria-busy={isLoading || undefined}
aria-label={isLoading ? loadingLabel : rest["aria-label"]}
{...rest}
>
<span className={`btn-content ${isLoading ? "is-hidden" : ""}`}>{children}</span>
{isLoading ? (
<span className="btn-loader-wrap" aria-hidden="true">
<svg className="btn-loader" viewBox="0 0 24 24" fill="none" focusable="false">
<circle
cx="12"
cy="12"
r="9"
stroke="currentColor"
strokeWidth="3"
strokeLinecap="round"
strokeDasharray="56.5"
strokeDashoffset="18"
/>
</svg>
</span>
) : null}
</button>
);
}
129 changes: 129 additions & 0 deletions frontend/src/app/components/ChatInterface.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>(null);

useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages, isTyping]);

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const trimmed = inputValue.trim();
if (!trimmed) return;

onSendMessage(trimmed);
setInputValue("");
};

return (
<div className="chat-container">
<div className="chat-header">
<div className="agent-avatar" aria-hidden="true">
<Bot size={28} />
</div>
<div>
<h2 style={{ margin: 0, fontSize: "1.25rem" }}>OpenClaw Agent</h2>
<div
style={{
display: "flex",
alignItems: "center",
gap: "6px",
fontSize: "0.85rem",
color: "var(--success)",
}}
>
<CheckCircle2
size={12}
fill="var(--success)"
color="var(--bg-card)"
aria-hidden="true"
/>{" "}
Online
</div>
</div>
</div>

<div
className="chat-messages"
role="log"
aria-label="Chat messages"
aria-live="polite"
aria-relevant="additions"
>
{messages.map((msg) => (
<div key={msg.id} className={`message ${msg.sender}`}>
{msg.proactive && (
<div
style={{
display: "flex",
alignItems: "center",
gap: "6px",
fontSize: "0.75rem",
color: "var(--accent-primary)",
marginBottom: "4px",
fontWeight: 600,
textTransform: "uppercase",
}}
>
<AlertCircle size={12} aria-hidden="true" /> Proactive Nudge
</div>
)}
<div className="message-bubble">{msg.text}</div>
</div>
))}

{isTyping && (
<div className="message agent" role="status">
<span className="sr-only">Agent is typing...</span>
<div className="typing-indicator" aria-hidden="true">
<span></span>
<span></span>
<span></span>
</div>
</div>
)}

<div ref={messagesEndRef} />
</div>

<form onSubmit={handleSubmit} className="chat-input-container">
<input
id="chat-input"
type="text"
placeholder={placeholder}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
aria-label="Message input"
/>
<button type="submit" className="send-button" aria-label="Send message">
<Send size={18} aria-hidden="true" />
</button>
</form>
</div>
);
}
32 changes: 13 additions & 19 deletions frontend/src/app/components/ConnectWalletButton.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { Loader2 } from 'lucide-react';
import { Button } from './Button';

export interface ConnectWalletButtonProps {
onClick: () => void;
Expand All @@ -12,10 +12,10 @@ function truncatePublicKey(key: string) {
return key.slice(0, 4) + '...' + key.slice(-4);
}

export const ConnectWalletButton: React.FC<ConnectWalletButtonProps> = ({
onClick,
publicKey,
isConnecting = false
export const ConnectWalletButton: React.FC<ConnectWalletButtonProps> = ({
onClick,
publicKey,
isConnecting = false
}) => {
const ariaLabel = isConnecting
? 'Connecting wallet, please wait'
Expand All @@ -24,22 +24,16 @@ export const ConnectWalletButton: React.FC<ConnectWalletButtonProps> = ({
: 'Connect Stellar wallet';

return (
<button
onClick={onClick}
className={`connect-wallet-btn ${isConnecting ? 'connecting' : ''}`}
disabled={isConnecting}
<Button
onClick={onClick}
className="connect-wallet-btn"
variant="primary"
isLoading={isConnecting}
aria-label={ariaLabel}
aria-busy={isConnecting}
loadingLabel="Connecting wallet"
>
{isConnecting ? (
<>
<Loader2 className="spinner" size={16} aria-hidden="true" />
<span>Connecting...</span>
</>
) : (
publicKey ? truncatePublicKey(publicKey) : 'Connect Wallet'
)}
</button>
{publicKey ? truncatePublicKey(publicKey) : 'Connect Wallet'}
</Button>
);
};

6 changes: 4 additions & 2 deletions frontend/src/app/components/WalletModal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useEffect, useRef } from 'react';
import { X } from 'lucide-react';
import { Button } from './Button';

interface WalletModalProps {
isOpen: boolean;
Expand Down Expand Up @@ -114,12 +115,13 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
Install Freighter
</a>

<button
<Button
onClick={onClose}
className="close-modal"
variant="secondary"
>
Close
</button>
</Button>
</div>
</div>
</div>
Expand Down
Loading
Loading