refactor(web): normalize advanced theme surfaces

This commit is contained in:
root 2026-05-06 04:18:48 +00:00
parent 76d326c793
commit 69e1b12d63
7 changed files with 546 additions and 742 deletions

View File

@ -5,9 +5,11 @@ import { getPlatformAccessToken } from '../lib/authSession';
import { createRequestId } from '../../../shared/request-id.js'; import { createRequestId } from '../../../shared/request-id.js';
import { import {
Send, X, Bot, User, Send, X, Bot, User,
Check, Loader2, Check, Loader2,
Zap, Copy Zap, Copy
} from 'lucide-react'; } from 'lucide-react';
import { Button } from './ui/button';
import { cn } from '../lib/utils';
interface ChatMessage { interface ChatMessage {
id: number; id: number;
@ -297,17 +299,31 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
navigator.clipboard.writeText(JSON.stringify(data, null, 2)); navigator.clipboard.writeText(JSON.stringify(data, null, 2));
}; };
const handleKeyDown = (e: React.KeyboardEvent) => { const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) { if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault(); e.preventDefault();
sendMessage(); sendMessage();
} }
}; };
// Floating robot button - bottom right corner (portaled to body to avoid parent CSS issues) const assistantTint = 'var(--accent-soft)';
if (!isOpen) { const panelStyle: React.CSSProperties = {
return createPortal( background: 'var(--card)',
<button border: '1px solid var(--border)',
boxShadow: '0 25px 80px rgba(0,0,0,0.22)',
};
const inputStyle: React.CSSProperties = {
background: 'var(--input)',
border: '1px solid var(--border)',
color: 'var(--foreground)',
boxShadow: 'none',
caretColor: 'var(--ring)',
};
// Floating robot button - bottom right corner (portaled to body to avoid parent CSS issues)
if (!isOpen) {
return createPortal(
<button
onClick={() => setIsOpen(true)} onClick={() => setIsOpen(true)}
style={{ style={{
position: 'fixed', position: 'fixed',
@ -322,46 +338,46 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
}} }}
className="group" className="group"
> >
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
{/* Glow ring */} {/* Glow ring */}
<div style={{ <div style={{
position: 'absolute', position: 'absolute',
inset: '-8px', inset: '-8px',
borderRadius: '50%', borderRadius: '50%',
opacity: 0.4, opacity: 0.4,
background: 'radial-gradient(circle, rgba(0,255,136,0.25), transparent 70%)', background: 'radial-gradient(circle, color-mix(in oklab, var(--ring) 30%, transparent), transparent 70%)',
transition: 'opacity 0.3s', transition: 'opacity 0.3s',
}} /> }} />
{/* Robot container */} {/* Robot container */}
<div style={{ <div style={{
position: 'relative', position: 'relative',
width: '56px', width: '56px',
height: '56px', height: '56px',
borderRadius: '16px', borderRadius: '16px',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
background: 'linear-gradient(145deg, #1a1b2e, #0f1017)', background: 'var(--card)',
border: '1.5px solid rgba(0,255,136,0.25)', border: '1.5px solid var(--border)',
boxShadow: '0 8px 32px rgba(0,0,0,0.5), 0 0 20px rgba(0,255,136,0.1), inset 0 1px 0 rgba(255,255,255,0.06)', boxShadow: '0 8px 32px rgba(0,0,0,0.18), 0 0 20px color-mix(in oklab, var(--ring) 16%, transparent)',
transition: 'transform 0.2s', transition: 'transform 0.2s',
}}> }}>
<RobotIcon size={34} /> <RobotIcon size={34} />
</div> </div>
{/* Pulse dot */} {/* Pulse dot */}
<div style={{ <div style={{
position: 'absolute', position: 'absolute',
top: '-2px', top: '-2px',
right: '-2px', right: '-2px',
width: '14px', width: '14px',
height: '14px', height: '14px',
borderRadius: '50%', borderRadius: '50%',
background: '#00ff88', background: 'var(--primary)',
border: '2px solid #0a0b10', border: '2px solid var(--card)',
boxShadow: '0 0 8px rgba(0,255,136,0.5)', boxShadow: '0 0 8px color-mix(in oklab, var(--primary) 45%, transparent)',
animation: 'pulseDot 2s ease-in-out infinite', animation: 'pulseDot 2s ease-in-out infinite',
}} /> }} />
</div> </div>
<style>{` <style>{`
@keyframes robotFloat { @keyframes robotFloat {
0%, 100% { transform: translateY(0px); } 0%, 100% { transform: translateY(0px); }
@ -380,17 +396,17 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
return createPortal( return createPortal(
<> <>
{/* Backdrop */} {/* Backdrop */}
<div <div
onClick={() => setIsOpen(false)} onClick={() => setIsOpen(false)}
style={{ style={{
position: 'fixed', position: 'fixed',
inset: 0, inset: 0,
zIndex: 999998, zIndex: 999998,
background: 'rgba(0,0,0,0.6)', background: 'rgba(15, 23, 42, 0.45)',
backdropFilter: 'blur(6px)', backdropFilter: 'blur(6px)',
animation: 'fadeIn 0.15s ease-out', animation: 'fadeIn 0.15s ease-out',
}} }}
/> />
<div style={{ <div style={{
position: 'fixed', position: 'fixed',
bottom: '24px', bottom: '24px',
@ -399,80 +415,75 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
width: '460px', width: '460px',
maxWidth: 'calc(100vw - 48px)', maxWidth: 'calc(100vw - 48px)',
height: '640px', height: '640px',
maxHeight: 'calc(100vh - 48px)', maxHeight: 'calc(100vh - 48px)',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
borderRadius: '20px', borderRadius: '20px',
overflow: 'hidden', overflow: 'hidden',
border: '1px solid rgba(0,255,136,0.12)', animation: 'chatSlideUp 0.25s ease-out',
boxShadow: '0 25px 80px rgba(0,0,0,0.6), 0 0 40px rgba(0,255,136,0.06)', ...panelStyle,
animation: 'chatSlideUp 0.25s ease-out', }}>
}}> {/* Header */}
{/* Header */} <div style={{
<div style={{ background: 'var(--hero-gradient)',
background: 'linear-gradient(135deg, #14151f 0%, #0f1017 100%)', borderBottom: '1px solid var(--border)',
borderBottom: '1px solid rgba(255,255,255,0.06)', padding: '14px 18px',
padding: '14px 18px', display: 'flex',
display: 'flex', alignItems: 'center',
alignItems: 'center', justifyContent: 'space-between',
justifyContent: 'space-between', }}>
}}> <div className="flex items-center gap-3">
<div className="flex items-center gap-3"> <div className="w-10 h-10 rounded-xl flex items-center justify-center" style={{
<div className="w-10 h-10 rounded-xl flex items-center justify-center" style={{ background: 'var(--card)',
background: 'linear-gradient(145deg, #1a1b2e, #0f1017)', border: '1px solid var(--border)',
border: '1px solid rgba(0,255,136,0.2)', boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
boxShadow: '0 2px 8px rgba(0,0,0,0.3), 0 0 10px rgba(0,255,136,0.06)', }}>
}}> <RobotIcon size={26} />
<RobotIcon size={26} /> </div>
</div> <div>
<div> <h3 className="text-[13px] font-bold text-[var(--foreground)] leading-none">AI Strategy Assistant</h3>
<h3 className="text-[13px] font-bold text-white leading-none">AI Strategy Assistant</h3> <p className="mt-1 text-[10px] text-[var(--muted-foreground)]">Create & manage profiles with natural language</p>
<p className="text-[10px] text-zinc-500 mt-1">Create & manage profiles with natural language</p> </div>
</div> </div>
</div> <Button
<button onClick={() => setIsOpen(false)}
onClick={() => setIsOpen(false)} variant="ghost"
className="w-8 h-8 rounded-lg flex items-center justify-center text-zinc-500 hover:text-white transition-all" size="icon"
style={{ className="h-8 w-8 rounded-lg"
background: 'rgba(255,255,255,0.04)', >
border: '1px solid rgba(255,255,255,0.06)', <X size={14} />
}} </Button>
> </div>
<X size={14} />
</button> {/* Messages */}
</div> <div className="flex-1 overflow-y-auto px-4 py-4 space-y-4" style={{
background: 'var(--background)',
{/* Messages */} scrollbarWidth: 'thin',
<div className="flex-1 overflow-y-auto px-4 py-4 space-y-4" style={{ scrollbarColor: 'var(--border) transparent',
background: '#0a0b10', }}>
scrollbarWidth: 'thin', {messages.map(msg => (
scrollbarColor: 'rgba(0,255,136,0.06) transparent', <div key={msg.id} className={`flex gap-2.5 ${msg.role === 'user' ? 'flex-row-reverse' : ''}`}>
}}> {/* Avatar */}
{messages.map(msg => ( <div className={cn('shrink-0 flex h-7 w-7 items-center justify-center rounded-lg border', msg.role === 'user' ? 'bg-blue-500/12 border-blue-500/20' : '')}
<div key={msg.id} className={`flex gap-2.5 ${msg.role === 'user' ? 'flex-row-reverse' : ''}`}> style={msg.role === 'user' ? undefined : { background: assistantTint, borderColor: 'var(--border)' }}>
{/* Avatar */} {msg.role === 'user'
<div className={`shrink-0 w-7 h-7 rounded-lg flex items-center justify-center ${msg.role === 'user' ? <User size={12} className="text-blue-400" />
? 'bg-blue-500/15 border border-blue-500/20' : <Bot size={12} className="text-[var(--primary)]" />
: 'bg-[#00ff88]/10 border border-[#00ff88]/20' }
}`}> </div>
{msg.role === 'user'
? <User size={12} className="text-blue-400" />
: <Bot size={12} className="text-[#00ff88]" />
}
</div>
{/* Bubble */} {/* Bubble */}
<div className={`max-w-[85%] ${msg.role === 'user' ? 'text-right' : ''}`}> <div className={`max-w-[85%] ${msg.role === 'user' ? 'text-right' : ''}`}>
<div className="rounded-xl px-3.5 py-2.5 text-[12px] leading-relaxed" style={{ <div className="rounded-xl px-3.5 py-2.5 text-[12px] leading-relaxed" style={{
background: msg.role === 'user' background: msg.role === 'user'
? 'linear-gradient(135deg, rgba(59,130,246,0.15), rgba(59,130,246,0.08))' ? 'linear-gradient(135deg, rgba(59,130,246,0.15), rgba(59,130,246,0.08))'
: 'linear-gradient(135deg, rgba(255,255,255,0.04), rgba(255,255,255,0.015))', : 'var(--card)',
border: `1px solid ${msg.role === 'user' ? 'rgba(59,130,246,0.2)' : 'rgba(255,255,255,0.06)'}`, border: `1px solid ${msg.role === 'user' ? 'rgba(59,130,246,0.2)' : 'var(--border)'}`,
color: msg.role === 'user' ? '#93c5fd' : '#d4d4d8', color: msg.role === 'user' ? '#93c5fd' : 'var(--foreground)',
whiteSpace: 'pre-wrap', whiteSpace: 'pre-wrap',
}}> }}>
{msg.content} {msg.content}
</div> </div>
{/* Profile preview card */} {/* Profile preview card */}
{msg.profileData && msg.action !== 'explain' && (() => { {msg.profileData && msg.action !== 'explain' && (() => {
@ -484,23 +495,23 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
return ( return (
<div className="mt-2 rounded-xl overflow-hidden" style={{ <div className="mt-2 rounded-xl overflow-hidden" style={{
background: 'linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01))', background: 'var(--card-elevated)',
border: '1px solid rgba(255,255,255,0.06)', border: '1px solid var(--border)',
boxShadow: '0 2px 8px rgba(0,0,0,0.2)', boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
}}> }}>
<div className="px-3.5 py-2 flex items-center justify-between" style={{ <div className="px-3.5 py-2 flex items-center justify-between" style={{
background: 'linear-gradient(90deg, rgba(0,255,136,0.06), transparent)', background: 'var(--accent-soft)',
borderBottom: '1px solid rgba(255,255,255,0.04)', borderBottom: '1px solid var(--border)',
}}> }}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Zap size={10} className="text-[#00ff88]" /> <Zap size={10} className="text-[var(--primary)]" />
<span className="text-[10px] font-bold text-zinc-400 uppercase tracking-wider"> <span className="text-[10px] font-bold text-[var(--muted-foreground)] uppercase tracking-wider">
{msg.action === 'create_profile' ? 'New Profile' : 'Update Profile'} {msg.action === 'create_profile' ? 'New Profile' : 'Update Profile'}
</span> </span>
</div> </div>
<button <button
onClick={() => copyJson(activeProfileData)} onClick={() => copyJson(activeProfileData)}
className="text-zinc-600 hover:text-zinc-400 transition-colors" className="text-[var(--muted-foreground)] hover:text-[var(--foreground)] transition-colors"
title="Copy JSON" title="Copy JSON"
> >
<Copy size={11} /> <Copy size={11} />
@ -509,31 +520,31 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
<div className="px-3.5 py-2.5 space-y-1.5"> <div className="px-3.5 py-2.5 space-y-1.5">
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-[10px] text-zinc-600">Name</span> <span className="text-[10px] text-[var(--muted-foreground)]">Name</span>
<span className="text-[11px] font-bold text-white">{activeProfileData?.name}</span> <span className="text-[11px] font-bold text-[var(--foreground)]">{activeProfileData?.name}</span>
</div> </div>
{activeProfileData?.allocated_capital ? ( {activeProfileData?.allocated_capital ? (
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-[10px] text-zinc-600">Capital</span> <span className="text-[10px] text-[var(--muted-foreground)]">Capital</span>
<span className="text-[11px] font-bold text-blue-400 font-mono">${activeProfileData.allocated_capital}</span> <span className="text-[11px] font-bold text-blue-400 font-mono">${activeProfileData.allocated_capital}</span>
</div> </div>
) : null} ) : null}
{activeProfileData?.risk_per_trade_percent ? ( {activeProfileData?.risk_per_trade_percent ? (
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-[10px] text-zinc-600">Risk / Trade</span> <span className="text-[10px] text-[var(--muted-foreground)]">Risk / Trade</span>
<span className="text-[11px] font-bold text-amber-400 font-mono">{activeProfileData.risk_per_trade_percent}%</span> <span className="text-[11px] font-bold text-amber-400 font-mono">{activeProfileData.risk_per_trade_percent}%</span>
</div> </div>
) : null} ) : null}
{activeProfileData?.symbols ? ( {activeProfileData?.symbols ? (
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-[10px] text-zinc-600">Symbols</span> <span className="text-[10px] text-[var(--muted-foreground)]">Symbols</span>
<span className="text-[10px] font-mono text-zinc-400">{activeProfileData.symbols}</span> <span className="text-[10px] font-mono text-[var(--foreground)]">{activeProfileData.symbols}</span>
</div> </div>
) : null} ) : null}
{activeRules > 0 ? ( {activeRules > 0 ? (
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-[10px] text-zinc-600">Rules</span> <span className="text-[10px] text-[var(--muted-foreground)]">Rules</span>
<span className="text-[10px] font-mono text-[#00ff88]"> <span className="text-[10px] font-mono text-[var(--primary)]">
{activeRules} active {activeRules} active
</span> </span>
</div> </div>
@ -542,12 +553,13 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
{isEditing ? ( {isEditing ? (
<div className="px-3.5 pb-3 space-y-2"> <div className="px-3.5 pb-3 space-y-2">
<div className="text-[10px] text-zinc-500 uppercase tracking-wider font-bold">Edit Parameters Before Apply</div> <div className="text-[10px] text-[var(--muted-foreground)] uppercase tracking-wider font-bold">Edit Parameters Before Apply</div>
<input <input
value={activeProfileData?.name || ''} value={activeProfileData?.name || ''}
onChange={(e) => updateDraftField(msg.id, 'name', e.target.value)} onChange={(e) => updateDraftField(msg.id, 'name', e.target.value)}
placeholder="Profile Name" placeholder="Profile Name"
className="w-full rounded-lg px-2.5 py-1.5 text-[11px] bg-[#161722] border border-white/10 text-white outline-none" className="w-full rounded-lg px-2.5 py-1.5 text-[11px] outline-none"
style={inputStyle}
/> />
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
<input <input
@ -557,7 +569,8 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
value={activeProfileData?.allocated_capital ?? ''} value={activeProfileData?.allocated_capital ?? ''}
onChange={(e) => updateDraftField(msg.id, 'allocated_capital', e.target.value)} onChange={(e) => updateDraftField(msg.id, 'allocated_capital', e.target.value)}
placeholder="Capital" placeholder="Capital"
className="w-full rounded-lg px-2.5 py-1.5 text-[11px] bg-[#161722] border border-white/10 text-white outline-none" className="w-full rounded-lg px-2.5 py-1.5 text-[11px] outline-none"
style={inputStyle}
/> />
<input <input
type="number" type="number"
@ -566,45 +579,50 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
value={activeProfileData?.risk_per_trade_percent ?? ''} value={activeProfileData?.risk_per_trade_percent ?? ''}
onChange={(e) => updateDraftField(msg.id, 'risk_per_trade_percent', e.target.value)} onChange={(e) => updateDraftField(msg.id, 'risk_per_trade_percent', e.target.value)}
placeholder="Risk %" placeholder="Risk %"
className="w-full rounded-lg px-2.5 py-1.5 text-[11px] bg-[#161722] border border-white/10 text-white outline-none" className="w-full rounded-lg px-2.5 py-1.5 text-[11px] outline-none"
style={inputStyle}
/> />
</div> </div>
<input <input
value={activeProfileData?.symbols || ''} value={activeProfileData?.symbols || ''}
onChange={(e) => updateDraftField(msg.id, 'symbols', e.target.value)} onChange={(e) => updateDraftField(msg.id, 'symbols', e.target.value)}
placeholder="Symbols (e.g. BTC/USDT,ETH/USDT)" placeholder="Symbols (e.g. BTC/USDT,ETH/USDT)"
className="w-full rounded-lg px-2.5 py-1.5 text-[11px] bg-[#161722] border border-white/10 text-white outline-none" className="w-full rounded-lg px-2.5 py-1.5 text-[11px] outline-none"
style={inputStyle}
/> />
<div className="flex items-center justify-between rounded-lg px-2.5 py-1.5 bg-[#161722] border border-white/10"> <div className="flex items-center justify-between rounded-lg px-2.5 py-1.5" style={inputStyle}>
<span className="text-[10px] text-zinc-400 uppercase tracking-wider">Auto Trading</span> <span className="text-[10px] text-[var(--muted-foreground)] uppercase tracking-wider">Auto Trading</span>
<select <select
value={activeProfileData?.is_active === false ? 'false' : 'true'} value={activeProfileData?.is_active === false ? 'false' : 'true'}
onChange={(e) => updateDraftField(msg.id, 'is_active', e.target.value === 'true')} onChange={(e) => updateDraftField(msg.id, 'is_active', e.target.value === 'true')}
className="rounded px-2 py-1 text-[10px] bg-[#0f1017] border border-white/10 text-zinc-300 outline-none" className="rounded px-2 py-1 text-[10px] outline-none"
style={inputStyle}
> >
<option value="true">Active</option> <option value="true">Active</option>
<option value="false">Paused</option> <option value="false">Paused</option>
</select> </select>
</div> </div>
<div className="flex justify-end"> <div className="flex justify-end">
<button <Button
onClick={() => resetDraft(msg)} onClick={() => resetDraft(msg)}
className="px-2.5 py-1 rounded border border-white/10 text-[9px] font-bold uppercase tracking-wider text-zinc-300 hover:bg-white/5 transition-colors" variant="outline"
size="sm"
className="h-8 px-2.5 text-[9px] uppercase tracking-wider"
> >
Reset Reset
</button> </Button>
</div> </div>
</div> </div>
) : null} ) : null}
{!appliedIds.has(msg.id) && !cancelledIds.has(msg.id) ? ( {!appliedIds.has(msg.id) && !cancelledIds.has(msg.id) ? (
<div className="flex" style={{ borderTop: '1px solid rgba(255,255,255,0.04)' }}> <div className="flex" style={{ borderTop: '1px solid var(--border)' }}>
<button <button
onClick={() => handleCancel(msg.id)} onClick={() => handleCancel(msg.id)}
className="flex-1 py-2.5 flex items-center justify-center gap-1.5 text-[11px] font-bold transition-all hover:bg-white/[0.03]" className="flex-1 py-2.5 flex items-center justify-center gap-1.5 text-[11px] font-bold transition-all hover:bg-white/[0.03]"
style={{ style={{
color: '#ef4444', color: 'var(--destructive)',
borderRight: '1px solid rgba(255,255,255,0.04)', borderRight: '1px solid var(--border)',
}} }}
> >
<X size={11} /> <X size={11} />
@ -615,7 +633,7 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
className="flex-1 py-2.5 flex items-center justify-center gap-1.5 text-[11px] font-bold transition-all hover:bg-white/[0.03]" className="flex-1 py-2.5 flex items-center justify-center gap-1.5 text-[11px] font-bold transition-all hover:bg-white/[0.03]"
style={{ style={{
color: '#fbbf24', color: '#fbbf24',
borderRight: '1px solid rgba(255,255,255,0.04)', borderRight: '1px solid var(--border)',
}} }}
> >
<Copy size={11} /> <Copy size={11} />
@ -625,8 +643,8 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
onClick={() => handleApply(msg)} onClick={() => handleApply(msg)}
className="flex-[2] py-2.5 flex items-center justify-center gap-1.5 text-[11px] font-bold transition-all hover:brightness-110" className="flex-[2] py-2.5 flex items-center justify-center gap-1.5 text-[11px] font-bold transition-all hover:brightness-110"
style={{ style={{
background: 'linear-gradient(90deg, rgba(0,255,136,0.12), rgba(0,255,136,0.06))', background: 'var(--accent-soft)',
color: '#00ff88', color: 'var(--primary)',
}} }}
> >
<Zap size={11} /> <Zap size={11} />
@ -635,17 +653,17 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
</div> </div>
) : cancelledIds.has(msg.id) ? ( ) : cancelledIds.has(msg.id) ? (
<div className="w-full py-2 flex items-center justify-center gap-1.5 text-[10px] font-semibold" style={{ <div className="w-full py-2 flex items-center justify-center gap-1.5 text-[10px] font-semibold" style={{
borderTop: '1px solid rgba(255,255,255,0.03)', borderTop: '1px solid var(--border)',
color: '#71717a', color: 'var(--muted-foreground)',
}}> }}>
<X size={10} /> <X size={10} />
Cancelled Cancelled
</div> </div>
) : ( ) : (
<div className="w-full py-2.5 flex items-center justify-center gap-2 text-[11px] font-bold" style={{ <div className="w-full py-2.5 flex items-center justify-center gap-2 text-[11px] font-bold" style={{
background: 'rgba(0,255,136,0.05)', background: 'var(--accent-soft)',
borderTop: '1px solid rgba(0,255,136,0.1)', borderTop: '1px solid var(--border)',
color: 'rgba(0,255,136,0.5)', color: 'var(--primary)',
}}> }}>
<Check size={12} /> <Check size={12} />
Applied Applied
@ -654,108 +672,99 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
</div> </div>
); );
})()} })()}
<span className="text-[9px] text-zinc-700 mt-1 block"> <span className="mt-1 block text-[9px] text-[var(--muted-foreground)]">
{msg.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} {msg.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</span> </span>
</div> </div>
</div> </div>
))} ))}
{/* Suggested quick actions - shown when only welcome message exists */} {/* Suggested quick actions - shown when only welcome message exists */}
{messages.length <= 1 && !isLoading && ( {messages.length <= 1 && !isLoading && (
<div className="mt-2"> <div className="mt-2">
<p style={{ fontSize: '9px', color: '#a1a1aa', textTransform: 'uppercase', letterSpacing: '0.1em', fontWeight: 700, marginBottom: '10px', paddingLeft: '4px' }}>Quick Actions</p> <p style={{ fontSize: '9px', color: 'var(--muted-foreground)', textTransform: 'uppercase', letterSpacing: '0.1em', fontWeight: 700, marginBottom: '10px', paddingLeft: '4px' }}>Quick Actions</p>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
{quickActions.map((action, i) => ( {quickActions.map((action, i) => (
<button <button
key={i} key={i}
onClick={() => sendMessage(action.prompt)} onClick={() => sendMessage(action.prompt)}
className="text-left px-3.5 py-3 rounded-xl transition-all" className="text-left px-3.5 py-3 rounded-xl transition-all"
style={{ style={{
background: '#161722', background: 'var(--card)',
border: '1px solid rgba(255,255,255,0.08)', border: '1px solid var(--border)',
cursor: 'pointer', cursor: 'pointer',
}} }}
onMouseEnter={e => { onMouseEnter={e => {
e.currentTarget.style.borderColor = 'rgba(0,255,136,0.25)'; e.currentTarget.style.borderColor = 'var(--ring)';
e.currentTarget.style.background = '#1a1b2a'; e.currentTarget.style.background = 'var(--accent-soft)';
}} }}
onMouseLeave={e => { onMouseLeave={e => {
e.currentTarget.style.borderColor = 'rgba(255,255,255,0.08)'; e.currentTarget.style.borderColor = 'var(--border)';
e.currentTarget.style.background = '#161722'; e.currentTarget.style.background = 'var(--card)';
}} }}
> >
<span style={{ fontSize: '13px', display: 'block', marginBottom: '3px' }}>{action.label}</span> <span style={{ fontSize: '13px', display: 'block', marginBottom: '3px', color: 'var(--foreground)' }}>{action.label}</span>
<span style={{ fontSize: '10px', color: '#a1a1aa', lineHeight: '1.4', display: 'block' }}>{action.prompt.slice(0, 55)}...</span> <span style={{ fontSize: '10px', color: 'var(--muted-foreground)', lineHeight: '1.4', display: 'block' }}>{action.prompt.slice(0, 55)}...</span>
</button> </button>
))} ))}
</div> </div>
</div>
)}
{isLoading && (
<div className="flex gap-2.5">
<div className="w-7 h-7 rounded-lg bg-[#00ff88]/10 border border-[#00ff88]/20 flex items-center justify-center">
<Bot size={12} className="text-[#00ff88]" />
</div>
<div className="rounded-xl px-3.5 py-2.5 flex items-center gap-2" style={{
background: 'rgba(255,255,255,0.03)',
border: '1px solid rgba(255,255,255,0.06)',
}}>
<Loader2 size={12} className="text-[#00ff88] animate-spin" />
<span className="text-[11px] text-zinc-500">Generating configuration...</span>
</div>
</div> </div>
)} )}
{isLoading && (
<div className="flex gap-2.5">
<div className="flex h-7 w-7 items-center justify-center rounded-lg border" style={{ background: assistantTint, borderColor: 'var(--border)' }}>
<Bot size={12} className="text-[var(--primary)]" />
</div>
<div className="rounded-xl px-3.5 py-2.5 flex items-center gap-2" style={{
background: 'var(--card)',
border: '1px solid var(--border)',
}}>
<Loader2 size={12} className="animate-spin text-[var(--primary)]" />
<span className="text-[11px] text-[var(--muted-foreground)]">Generating configuration...</span>
</div>
</div>
)}
<div ref={messagesEndRef} /> <div ref={messagesEndRef} />
</div> </div>
{/* Input area */} {/* Input area */}
<div style={{ <div style={{
background: '#0e0f18', background: 'var(--card)',
borderTop: '1px solid rgba(0,255,136,0.1)', borderTop: '1px solid var(--border)',
padding: '14px 16px', padding: '14px 16px',
}}> }}>
<div className="flex items-end gap-2.5"> <div className="flex items-end gap-2.5">
<div className="flex-1 relative"> <div className="flex-1 relative">
<textarea <textarea
ref={inputRef} ref={inputRef}
value={input} value={input}
onChange={e => setInput(e.target.value)} onChange={e => setInput(e.target.value)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
placeholder="Describe a strategy profile..." placeholder="Describe a strategy profile..."
disabled={isLoading} disabled={isLoading}
rows={2} rows={2}
className="w-full rounded-xl py-3 pl-4 pr-12 outline-none disabled:opacity-50 transition-all resize-none" className="w-full rounded-xl py-3 pl-4 pr-12 outline-none disabled:opacity-50 transition-all resize-none"
style={{ style={{ ...inputStyle, lineHeight: '1.5', fontFamily: 'inherit', fontSize: '13px' }}
background: '#161722', />
border: '1px solid rgba(255,255,255,0.12)', <button
boxShadow: 'inset 0 2px 6px rgba(0,0,0,0.3)', onClick={() => sendMessage()}
lineHeight: '1.5', disabled={!input.trim() || isLoading}
fontFamily: 'inherit', className="absolute right-2.5 bottom-2.5 w-8 h-8 rounded-lg flex items-center justify-center transition-all disabled:opacity-20 hover:scale-105"
fontSize: '13px', style={{
color: '#ffffff', background: input.trim() ? 'var(--primary)' : 'var(--accent-soft)',
caretColor: '#00ff88', color: input.trim() ? 'var(--primary-foreground)' : 'var(--muted-foreground)',
}} boxShadow: input.trim() ? '0 2px 10px color-mix(in oklab, var(--primary) 25%, transparent)' : 'none',
/> }}
<button >
onClick={() => sendMessage()} <Send size={14} />
disabled={!input.trim() || isLoading} </button>
className="absolute right-2.5 bottom-2.5 w-8 h-8 rounded-lg flex items-center justify-center transition-all disabled:opacity-20 hover:scale-105" </div>
style={{ </div>
background: input.trim() ? 'linear-gradient(135deg, #00ff88, #00cc6a)' : 'rgba(255,255,255,0.05)', <p className="mt-1.5 ml-1 text-[9px] text-[var(--muted-foreground)]">Enter to send · Shift+Enter new line</p>
color: input.trim() ? '#000' : '#52525b', </div>
boxShadow: input.trim() ? '0 2px 10px rgba(0,255,136,0.3)' : 'none',
}}
>
<Send size={14} />
</button>
</div>
</div>
<p className="text-[9px] text-zinc-700 mt-1.5 ml-1">Enter to send · Shift+Enter new line</p>
</div>
<style>{` <style>{`
@keyframes chatSlideUp { @keyframes chatSlideUp {

View File

@ -1,248 +1,157 @@
import React, { useState, useEffect } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { fetchMarketplacePresets } from '../lib/marketplaceApi'; import { fetchMarketplacePresets } from '../lib/marketplaceApi';
import { import {
Activity, Activity,
ArrowUpRight, ArrowUpRight,
Shield, CheckCircle,
Zap, Cpu,
Scale, Fingerprint,
CheckCircle, Info,
TrendingUp, LineChart,
Info, Scale,
Dna, Shield,
Cpu, TrendingUp,
Fingerprint, Users,
Users, Zap,
LineChart } from 'lucide-react';
} from 'lucide-react'; import type { StrategyPreset } from '../lib/PresetRegistry';
import type { StrategyPreset } from '../lib/PresetRegistry'; import { STRATEGY_PRESETS } from '../lib/PresetRegistry';
import { STRATEGY_PRESETS } from '../lib/PresetRegistry'; import { Button } from './ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card';
interface PresetMarketplaceProps { import { PageHeader } from './ui/page-header';
onSelect: (preset: StrategyPreset) => void;
onClose?: () => void; interface PresetMarketplaceProps {
} onSelect: (preset: StrategyPreset) => void;
onClose?: () => void;
const StrategyMarketplaceCard: React.FC<{ }
preset: StrategyPreset,
onSelect: (preset: StrategyPreset) => void, const themeByRisk: Record<string, { tone: 'safe' | 'balanced' | 'aggressive'; label: string }> = {
index: number safe: { tone: 'safe', label: 'Low Volatility' },
}> = ({ preset, onSelect, index }) => { balanced: { tone: 'balanced', label: 'Balanced' },
const isSafe = preset.riskStyleId === 'safe'; aggressive: { tone: 'aggressive', label: 'High Conviction' },
const isBalanced = preset.riskStyleId === 'balanced'; };
const isAggressive = preset.riskStyleId === 'aggressive';
function metricForPreset(preset: StrategyPreset) {
const themeColor = isSafe ? '#00ff88' : isBalanced ? '#3498db' : '#ff3366'; if (preset.riskStyleId === 'aggressive') {
return { performance: '+14.2%', volatility: 'High', icon: Zap, accent: 'var(--destructive)' };
// Visual metadata }
const performanceValue = isAggressive ? '+14.2%' : isSafe ? '+4.8%' : '+8.5%'; if (preset.riskStyleId === 'safe') {
const volatilityRating = isAggressive ? 'High' : isSafe ? 'Low' : 'Med'; return { performance: '+4.8%', volatility: 'Low', icon: Shield, accent: 'var(--primary)' };
}
return ( return { performance: '+8.5%', volatility: 'Medium', icon: Scale, accent: 'var(--ring)' };
<div style={{ }
background: '#14151a',
borderRadius: '28px', const StrategyMarketplaceCard: React.FC<{
border: '1px solid rgba(255, 255, 255, 0.05)', preset: StrategyPreset;
padding: '32px', onSelect: (preset: StrategyPreset) => void;
display: 'flex', index: number;
flexDirection: 'column', }> = ({ preset, onSelect, index }) => {
position: 'relative', const theme = themeByRisk[preset.riskStyleId] || themeByRisk.balanced;
overflow: 'hidden', const metric = metricForPreset(preset);
transition: 'all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275)', const RiskIcon = metric.icon;
cursor: 'default',
height: '100%', return (
minHeight: '620px' <Card className="h-full rounded-[28px] transition duration-200 hover:-translate-y-1 hover:border-[var(--ring)]/30 hover:shadow-xl">
}} className="marketplace-card-hover"> <CardHeader className="gap-5">
<div className="flex items-start justify-between gap-4">
{/* 1. Header Area - Perfectly Aligned */} <div className="flex items-center gap-3">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '28px' }}> <div
<div style={{ display: 'flex', gap: '14px', alignItems: 'center' }}> className="flex h-11 w-11 items-center justify-center rounded-2xl border"
<div style={{ style={{
width: '44px', background: 'var(--accent-soft)',
height: '44px', borderColor: 'var(--border)',
borderRadius: '14px', color: metric.accent,
background: 'rgba(255,255,255,0.03)', }}
display: 'flex', >
alignItems: 'center', <RiskIcon size={18} />
justifyContent: 'center', </div>
border: '1px solid rgba(255,255,255,0.05)', <div className="space-y-1">
color: 'rgba(255,255,255,0.5)' <div className="text-[10px] font-semibold uppercase tracking-[0.2em] text-[var(--muted-foreground)]">
}}> Strategy Profile
{isSafe ? <Shield size={20} /> : isBalanced ? <Scale size={20} /> : <Zap size={20} />} </div>
</div> <div className="text-xs font-semibold uppercase tracking-wide text-[var(--foreground)]">
<div style={{ display: 'flex', flexDirection: 'column', gap: '2px' }}> {preset.riskStyleId} strategy
<span style={{ fontSize: '10px', fontWeight: 900, color: '#444', textTransform: 'uppercase', letterSpacing: '2px' }}>Strategy Profile</span> </div>
<span style={{ fontSize: '13px', fontWeight: 800, color: 'white', textTransform: 'uppercase', letterSpacing: '0.5px' }}>{preset.riskStyleId} Strategy</span> </div>
</div> </div>
</div> <div className="stat-chip">V{index + 1}.4</div>
<div style={{ </div>
background: 'rgba(0,0,0,0.3)',
border: '1px solid rgba(255,255,255,0.05)', <div className="space-y-3">
padding: '6px 12px', <CardTitle className="text-2xl">{preset.name}</CardTitle>
borderRadius: '10px', <div
fontSize: '11px', className="inline-flex w-fit items-center gap-2 rounded-lg border px-3 py-1.5 text-[11px] font-semibold uppercase tracking-wide"
fontWeight: 900, style={{
color: '#333' background: 'var(--accent-soft)',
}}> borderColor: 'var(--border)',
V{index + 1}.4 color: metric.accent,
</div> }}
</div> >
<Fingerprint size={13} />
{/* 2. Identity - Aligned Left */} {theme.label} {metric.performance}
<div style={{ marginBottom: '24px', display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: '12px' }}> </div>
<h3 style={{ <CardDescription className="text-sm leading-6">
fontSize: '24px', {preset.description} Optimized for automated execution without changing your core risk budget.
fontWeight: 900, </CardDescription>
color: 'white', </div>
lineHeight: '1.1', </CardHeader>
letterSpacing: '-0.02em',
margin: 0 <CardContent className="flex h-full flex-col gap-6">
}}> <div className="grid grid-cols-2 gap-3">
{preset.name} {[
</h3> { label: 'Growth', value: metric.performance, icon: <TrendingUp size={14} /> },
<div style={{ { label: 'Latency', value: 'Low', icon: <Cpu size={14} /> },
display: 'inline-flex', { label: 'Liquidity', value: 'Prime', icon: <Users size={14} /> },
alignItems: 'center', { label: 'Volatility', value: metric.volatility, icon: <Activity size={14} /> },
gap: '8px', ].map((spec) => (
background: 'rgba(0, 255, 136, 0.05)', <div
padding: '6px 14px', key={spec.label}
borderRadius: '8px', className="rounded-2xl border p-4"
fontSize: '11px', style={{ background: 'var(--card-elevated)', borderColor: 'var(--border)' }}
color: '#00ff88', >
fontWeight: 900, <div className="mb-2 flex items-center gap-2 text-[10px] font-semibold uppercase tracking-[0.14em] text-[var(--muted-foreground)]">
textTransform: 'uppercase', {spec.icon}
border: '1px solid rgba(0, 255, 136, 0.1)', {spec.label}
letterSpacing: '0.5px' </div>
}}> <div className="text-base font-semibold text-[var(--foreground)]">{spec.value}</div>
<Fingerprint size={14} /> Institutional Alpha {performanceValue} </div>
</div> ))}
</div> </div>
<p style={{ <div className="space-y-2 text-sm text-[var(--muted-foreground)]">
fontSize: '14px', <div className="flex items-center gap-2">
color: '#888', <CheckCircle size={15} className="text-[var(--primary)]" />
lineHeight: '1.6', Logical invariant verified
marginBottom: '28px', </div>
textAlign: 'left', <div className="flex items-center gap-2">
flex: 1 <CheckCircle size={15} className="text-[var(--primary)]" />
}}> Risk-isolated execution
{preset.description} Optimized for dominance and high-conviction momentum in volatile periods. </div>
</p> </div>
{/* 3. Specs Grid - Balanced Alignment */} <div className="mt-auto flex gap-3">
<div style={{ <Button className="h-12 flex-1 rounded-2xl" onClick={() => onSelect(preset)}>
display: 'grid', Use This Strategy
gridTemplateColumns: '1fr 1fr', <ArrowUpRight size={15} />
gap: '14px', </Button>
marginBottom: '28px' <Button
}}> variant="outline"
{[ size="icon"
{ label: 'Growth', value: performanceValue, icon: <TrendingUp size={14} />, color: '#00ff88' }, className="h-12 w-12 rounded-2xl"
{ label: 'Latency', value: 'Low', icon: <Cpu size={14} />, color: '#3498db' }, title="Preset information"
{ label: 'Liquidity', value: 'Prime', icon: <Users size={14} />, color: '#ffaa00' }, >
{ label: 'Rating', value: volatilityRating, icon: <Activity size={14} />, color: '#ff3366' } <Info size={18} />
].map((spec, i) => ( </Button>
<div key={i} style={{ </div>
background: 'rgba(0,0,0,0.2)', </CardContent>
border: '1px solid rgba(255,255,255,0.03)', </Card>
padding: '16px', );
borderRadius: '20px', };
display: 'flex',
flexDirection: 'column', export const PresetMarketplace: React.FC<PresetMarketplaceProps> = ({ onSelect, onClose }) => {
gap: '6px', const [customPresets, setCustomPresets] = useState<StrategyPreset[]>([]);
alignItems: 'flex-start'
}}> useEffect(() => {
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: '#555', fontSize: '10px', fontWeight: 900, textTransform: 'uppercase', letterSpacing: '1px' }}>
{spec.icon} {spec.label}
</div>
<div style={{ color: 'white', fontWeight: 900, fontSize: '16px' }}>{spec.value}</div>
</div>
))}
</div>
{/* 4. Verifications - Left Justified */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px', marginBottom: '32px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', fontSize: '12px', color: 'rgba(255,255,255,0.4)', fontWeight: 700 }}>
<CheckCircle size={16} style={{ color: '#00ff88' }} /> Logical Invariant Verified
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', fontSize: '12px', color: 'rgba(255,255,255,0.4)', fontWeight: 700 }}>
<CheckCircle size={16} style={{ color: '#00ff88' }} /> Risk-Isolated Execution
</div>
</div>
{/* 5. Action - Consistent Alignment */}
<div style={{ display: 'flex', gap: '12px', marginTop: 'auto' }}>
<button
onClick={() => onSelect(preset)}
style={{
flex: 1,
height: '56px',
background: '#00ff88',
color: 'black',
borderRadius: '18px',
border: 'none',
fontWeight: 900,
fontSize: '12px',
textTransform: 'uppercase',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '10px',
boxShadow: '0 12px 36px -12px rgba(0, 255, 136, 0.4)',
transition: 'all 0.2s',
letterSpacing: '1px'
}}
className="clone-btn"
>
USE THIS STRATEGY <ArrowUpRight size={16} strokeWidth={3} />
</button>
<button style={{
width: '56px',
height: '56px',
borderRadius: '18px',
border: '1px solid rgba(255,255,255,0.05)',
background: 'rgba(255,255,255,0.02)',
color: 'rgba(255,255,255,0.4)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer'
}}>
<Info size={22} />
</button>
</div>
<div style={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: '4px',
background: `linear-gradient(90deg, transparent, ${themeColor}, transparent)`,
opacity: 0.2
}} />
<style>{`
.marketplace-card-hover:hover {
border-color: rgba(0, 255, 136, 0.3) !important;
transform: translateY(-8px);
box-shadow: 0 40px 80px -20px rgba(0,0,0,0.8) !important;
background: #1a1b21 !important;
}
.clone-btn:hover {
filter: brightness(1.1);
transform: scale(1.02);
}
`}</style>
</div>
);
};
export const PresetMarketplace: React.FC<PresetMarketplaceProps> = ({ onSelect, onClose }) => {
const [customPresets, setCustomPresets] = useState<StrategyPreset[]>([]);
useEffect(() => {
const fetchCustomPresets = async () => { const fetchCustomPresets = async () => {
try { try {
const data = await fetchMarketplacePresets(); const data = await fetchMarketplacePresets();
@ -255,147 +164,60 @@ export const PresetMarketplace: React.FC<PresetMarketplaceProps> = ({ onSelect,
typicalTradesPerDay: d.typical_trades_per_day, typicalTradesPerDay: d.typical_trades_per_day,
performanceTag: d.performance_tag, performanceTag: d.performance_tag,
isPopular: d.is_popular, isPopular: d.is_popular,
strategy_config: d.strategy_config strategy_config: d.strategy_config,
})); }));
setCustomPresets(mappedData as any); setCustomPresets(mappedData as StrategyPreset[]);
} catch (e) { } catch (e) {
console.error('Error fetching marketplace presets:', e); console.error('Error fetching marketplace presets:', e);
} }
}; };
fetchCustomPresets(); void fetchCustomPresets();
}, []); }, []);
const allPresets = [...STRATEGY_PRESETS, ...customPresets]; const allPresets = useMemo(() => [...STRATEGY_PRESETS, ...customPresets], [customPresets]);
return ( return (
<div style={{ <div className="space-y-8">
maxWidth: '1400px', <div className="flex flex-wrap items-start justify-between gap-4">
margin: '0 auto', <PageHeader
padding: '0 20px 100px 20px', title="Strategy Marketplace"
animation: 'fadeIn 0.7s ease-out' description="Browse reusable strategy profiles with preconfigured risk posture and execution bias."
}}> />
{/* Premium Header Alignment Fix */} <div className="flex items-center gap-3">
<div style={{ <div className="stat-chip">{allPresets.length} presets</div>
display: 'flex', {onClose ? (
flexDirection: 'column', <Button variant="outline" onClick={onClose}>
marginBottom: '60px', Return
padding: '60px 0', </Button>
borderBottom: '1px solid rgba(255,255,255,0.05)', ) : null}
position: 'relative', </div>
alignItems: 'flex-start' /* Force consistent left alignment */ </div>
}}>
<div style={{ <div className="grid grid-cols-1 gap-6 xl:grid-cols-3">
position: 'absolute', {allPresets.map((preset, idx) => (
top: 0, <StrategyMarketplaceCard key={preset.id} preset={preset} index={idx} onSelect={onSelect} />
right: 0, ))}
opacity: 0.03,
pointerEvents: 'none', <Card className="rounded-[28px] border-dashed">
transform: 'translate(40px, -20px)' <CardContent className="flex min-h-[560px] flex-col items-center justify-center gap-4 px-8 py-10 text-center">
}}> <div
<Dna size={320} strokeWidth={1} /> className="flex h-16 w-16 items-center justify-center rounded-3xl border"
</div> style={{ background: 'var(--accent-soft)', borderColor: 'var(--border)' }}
>
{/* Removed indenting line for perfect optical left-alignment */} <LineChart size={28} className="text-[var(--muted-foreground)]" />
<div style={{ </div>
display: 'inline-flex', <div className="space-y-2">
alignItems: 'center', <div className="text-xs font-semibold uppercase tracking-[0.2em] text-[var(--muted-foreground)]">
gap: '12px', Analyzing DNA
color: '#00ff88', </div>
fontSize: '11px', <p className="mx-auto max-w-xs text-sm text-[var(--muted-foreground)]">
fontWeight: 900, Verification queue is active for new marketplace strategies.
textTransform: 'uppercase', </p>
letterSpacing: '4px', </div>
marginBottom: '24px' </CardContent>
}}> </Card>
QUANTITATIVE REPOSITORY </div>
<div style={{ width: '30px', height: '1px', background: '#00ff88', opacity: 0.2 }} /> </div>
</div> );
};
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', width: '100%', flexWrap: 'wrap', gap: '32px' }}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
<h2 style={{
fontSize: '84px',
fontWeight: 950,
color: 'white',
letterSpacing: '-0.04em',
lineHeight: '0.9',
margin: 0,
textTransform: 'uppercase'
}}>
Strategy<br />
<span style={{ color: '#00ff88' }}>Marketplace</span>
</h2>
<p style={{ fontSize: '20px', color: '#666', marginTop: '24px', maxWidth: '600px', fontWeight: 500, margin: '24px 0 0 0', textAlign: 'left' }}>
Institutional-grade algorithm DNA for automated retail deployment.
</p>
</div>
<div style={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', padding: '0 24px', borderLeft: '1px solid rgba(255,255,255,0.08)' }}>
<span style={{ color: '#444', fontSize: '11px', fontWeight: 900, textTransform: 'uppercase', letterSpacing: '2px' }}>Profiles</span>
<span style={{ color: 'white', fontSize: '32px', fontWeight: 950, lineHeight: '1' }}>{allPresets.length}</span>
</div>
{onClose && (
<button
onClick={onClose}
style={{
background: 'none',
border: '1px solid rgba(255,255,255,0.1)',
color: 'white',
padding: '14px 36px',
borderRadius: '16px',
cursor: 'pointer',
fontWeight: 900,
fontSize: '12px',
textTransform: 'uppercase',
letterSpacing: '1.5px',
transition: 'all 0.2s'
}}
>
Return
</button>
)}
</div>
</div>
</div>
{/* Grid Layout - Perfectly Symmetrical */}
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))',
gap: '40px',
width: '100%'
}}>
{allPresets.map((preset, idx) => (
<StrategyMarketplaceCard key={preset.id} preset={preset} index={idx} onSelect={onSelect} />
))}
{/* DNA Loader Placeholder - Aligned Center */}
<div style={{
background: 'rgba(255,255,255,0.01)',
border: '2px dashed rgba(255,255,255,0.03)',
borderRadius: '28px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '620px',
padding: '40px',
textAlign: 'center'
}}>
<LineChart size={56} style={{ color: '#1a1a1a', marginBottom: '24px' }} />
<span style={{ color: '#222', fontWeight: 900, textTransform: 'uppercase', letterSpacing: '4px', fontSize: '12px' }}>Analyzing DNA</span>
<span style={{ color: '#111', fontSize: '13px', marginTop: '12px', maxWidth: '200px', fontWeight: 700 }}>Verification queue currently active for new strategies.</span>
</div>
</div>
<style>{`
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
`}</style>
</div>
);
};

View File

@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { act, fireEvent, render, screen } from '@testing-library/react'; import { act, fireEvent, render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import { AppContext, type AppContextValue } from '../../context/AppContext'; import { AppContext, type AppContextValue } from '../../context/AppContext';
import { ThemeProvider } from '../theme/ThemeProvider';
import { Header } from './Header'; import { Header } from './Header';
const { fetchMarketIndicesMock } = vi.hoisted(() => ({ const { fetchMarketIndicesMock } = vi.hoisted(() => ({
@ -45,11 +46,13 @@ function setVisibilityState(value: DocumentVisibilityState) {
function renderHeader() { function renderHeader() {
return render( return render(
<AppContext.Provider value={appContext}> <ThemeProvider>
<MemoryRouter> <AppContext.Provider value={appContext}>
<Header /> <MemoryRouter>
</MemoryRouter> <Header />
</AppContext.Provider>, </MemoryRouter>
</AppContext.Provider>
</ThemeProvider>,
); );
} }

View File

@ -9,6 +9,8 @@ import { createTradeProfile } from '../../lib/profileApi';
import '../../lib/monacoLocalWorkers'; import '../../lib/monacoLocalWorkers';
import { tradingRuntime } from '../../lib/runtime'; import { tradingRuntime } from '../../lib/runtime';
import { createRequestId } from '../../../../shared/request-id.js'; import { createRequestId } from '../../../../shared/request-id.js';
import { Button } from '../ui/button';
import { Card, CardContent } from '../ui/card';
const DEFAULT_TEMPLATE = `/** const DEFAULT_TEMPLATE = `/**
* Custom Trading Strategy * Custom Trading Strategy
@ -197,42 +199,30 @@ export function CodeStrategyEditor({
return ( return (
<div onKeyDownCapture={handleShortcut}> <div onKeyDownCapture={handleShortcut}>
{/* Toolbar */} {/* Toolbar */}
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}> <div className="mb-3 flex items-center gap-2">
<span style={{ fontSize: 13, fontWeight: 600, color: '#374151' }}> <span className="text-sm font-semibold text-[var(--foreground)]">
Code Editor {symbol} Code Editor {symbol}
</span> </span>
<span style={{ fontSize: 11, color: '#6B7280' }}> <span className="text-xs text-[var(--muted-foreground)]">
Cmd/Ctrl-S save · Cmd/Ctrl-Enter backtest Cmd/Ctrl-S save · Cmd/Ctrl-Enter backtest
</span> </span>
<div style={{ flex: 1 }} /> <div style={{ flex: 1 }} />
<button onClick={handleCopy} title="Copy code" <Button variant="outline" size="sm" onClick={handleCopy} title="Copy code">
style={toolBtn('#F9FAFB','#374151','#E5E7EB')}>
<Copy size={13} /> Copy <Copy size={13} /> Copy
</button> </Button>
<button onClick={handleReset} title="Reset to template" <Button variant="outline" size="sm" onClick={handleReset} title="Reset to template">
style={toolBtn('#F9FAFB','#374151','#E5E7EB')}>
<RotateCcw size={13} /> Reset <RotateCcw size={13} /> Reset
</button> </Button>
<button onClick={handleSave} disabled={saving} title="Save code strategy (Cmd/Ctrl+S)" <Button variant={saved ? 'outline' : 'default'} size="sm" onClick={handleSave} disabled={saving} title="Save code strategy (Cmd/Ctrl+S)">
style={{
...toolBtn('#F0FDF4', saved ? '#16A34A' : '#374151', '#86EFAC'),
opacity: saving ? 0.7 : 1,
cursor: saving ? 'wait' : 'pointer',
}}>
<Save size={13} /> {saving ? 'Saving...' : saved ? 'Saved!' : 'Save'} <Save size={13} /> {saving ? 'Saving...' : saved ? 'Saved!' : 'Save'}
</button> </Button>
<button onClick={handleRunBacktest} disabled={running} title="Run backtest (Cmd/Ctrl+Enter)" <Button size="sm" onClick={handleRunBacktest} disabled={running} title="Run backtest (Cmd/Ctrl+Enter)">
style={{
...toolBtn('#2563EB','#fff','transparent'),
opacity: running ? 0.7 : 1,
cursor: running ? 'wait' : 'pointer',
}}>
<Play size={13} /> {running ? 'Running…' : 'Run Backtest'} <Play size={13} /> {running ? 'Running…' : 'Run Backtest'}
</button> </Button>
</div> </div>
{/* Monaco editor */} {/* Monaco editor */}
<div style={{ border: '1px solid #E5E7EB', borderRadius: 10, overflow: 'hidden' }}> <div style={{ border: '1px solid var(--border)', borderRadius: 16, overflow: 'hidden', background: 'var(--card)' }}>
<Suspense fallback={<CodeEditorFallback />}> <Suspense fallback={<CodeEditorFallback />}>
<MonacoEditor <MonacoEditor
height="380px" height="380px"
@ -242,7 +232,7 @@ export function CodeStrategyEditor({
setCode(v ?? ''); setCode(v ?? '');
setSaved(false); setSaved(false);
}} }}
theme="light" theme={document.documentElement.classList.contains('dark') ? 'vs-dark' : 'light'}
options={{ options={{
fontSize: 13, fontSize: 13,
minimap: { enabled: false }, minimap: { enabled: false },
@ -262,22 +252,18 @@ export function CodeStrategyEditor({
{/* Error */} {/* Error */}
{error && ( {error && (
<div style={{ <Card className="mt-3 border-[var(--destructive)]/30 bg-[var(--destructive)]/10">
marginTop: 12, padding: 12, background: '#FEF2F2', <CardContent className="px-4 py-3 text-sm font-medium text-[var(--destructive)] [font-family:monospace]">
border: '1px solid #FCA5A5', borderRadius: 8, {error}
fontSize: 13, color: '#DC2626', fontFamily: 'monospace', </CardContent>
}}> </Card>
{error}
</div>
)} )}
{/* Backtest results */} {/* Backtest results */}
{result && ( {result && (
<div style={{ <Card className="mt-3 border-[var(--primary)]/20 bg-[var(--accent-soft)]">
marginTop: 12, padding: 14, background: '#F0FDF4', <CardContent className="space-y-3 px-4 py-4">
border: '1px solid #86EFAC', borderRadius: 10, <div style={{ fontSize: 13, fontWeight: 700, color: 'var(--primary)', marginBottom: 10 }}>
}}>
<div style={{ fontSize: 13, fontWeight: 700, color: '#15803D', marginBottom: 10 }}>
Backtest Results Backtest Results
</div> </div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5,1fr)', gap: 12 }}> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(5,1fr)', gap: 12 }}>
@ -289,13 +275,13 @@ export function CodeStrategyEditor({
['Max Drawdown', fmt(result.maxDrawdown, '%')], ['Max Drawdown', fmt(result.maxDrawdown, '%')],
].map(([label, val]) => ( ].map(([label, val]) => (
<div key={label} style={{ <div key={label} style={{
background: '#fff', borderRadius: 8, padding: '10px 12px', background: 'var(--card)', borderRadius: 12, padding: '10px 12px',
border: '1px solid #D1FAE5', border: '1px solid var(--border)',
}}> }}>
<div style={{ fontSize: 10, color: '#6B7280', fontWeight: 500, marginBottom: 3 }}> <div style={{ fontSize: 10, color: 'var(--muted-foreground)', fontWeight: 500, marginBottom: 3 }}>
{label} {label}
</div> </div>
<div style={{ fontSize: 16, fontWeight: 700, color: '#111827' }}>{val}</div> <div style={{ fontSize: 16, fontWeight: 700, color: 'var(--foreground)' }}>{val}</div>
</div> </div>
))} ))}
</div> </div>
@ -308,19 +294,19 @@ export function CodeStrategyEditor({
</div> </div>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 11 }}> <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 11 }}>
<thead> <thead>
<tr style={{ borderBottom: '1px solid #E5E7EB' }}> <tr style={{ borderBottom: '1px solid var(--border)' }}>
{['Date','Side','Price','Qty','P&L'].map(h => ( {['Date','Side','Price','Qty','P&L'].map(h => (
<th key={h} style={{ padding: '4px 8px', textAlign: 'left', color: '#9CA3AF', fontWeight: 600 }}>{h}</th> <th key={h} style={{ padding: '4px 8px', textAlign: 'left', color: 'var(--muted-foreground)', fontWeight: 600 }}>{h}</th>
))} ))}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{result.tradeLog.slice(-10).map((t: any, i: number) => ( {result.tradeLog.slice(-10).map((t: any, i: number) => (
<tr key={i} style={{ borderBottom: '1px solid #F9FAFB' }}> <tr key={i} style={{ borderBottom: '1px solid var(--border)' }}>
<td style={{ padding: '4px 8px', color: '#374151' }}>{t.date ?? '—'}</td> <td style={{ padding: '4px 8px', color: 'var(--foreground)' }}>{t.date ?? '—'}</td>
<td style={{ padding: '4px 8px', color: t.side === 'BUY' ? '#16A34A' : '#DC2626', fontWeight: 600 }}>{t.side}</td> <td style={{ padding: '4px 8px', color: t.side === 'BUY' ? '#16A34A' : '#DC2626', fontWeight: 600 }}>{t.side}</td>
<td style={{ padding: '4px 8px', color: '#374151' }}>{t.price != null ? `$${t.price.toFixed(2)}` : '—'}</td> <td style={{ padding: '4px 8px', color: 'var(--foreground)' }}>{t.price != null ? `$${t.price.toFixed(2)}` : '—'}</td>
<td style={{ padding: '4px 8px', color: '#374151' }}>{t.qty ?? '—'}</td> <td style={{ padding: '4px 8px', color: 'var(--foreground)' }}>{t.qty ?? '—'}</td>
<td style={{ padding: '4px 8px', color: t.pnl >= 0 ? '#16A34A' : '#DC2626', fontWeight: 600 }}> <td style={{ padding: '4px 8px', color: t.pnl >= 0 ? '#16A34A' : '#DC2626', fontWeight: 600 }}>
{t.pnl != null ? `${t.pnl >= 0 ? '+' : ''}$${t.pnl.toFixed(2)}` : '—'} {t.pnl != null ? `${t.pnl >= 0 ? '+' : ''}$${t.pnl.toFixed(2)}` : '—'}
</td> </td>
@ -330,7 +316,8 @@ export function CodeStrategyEditor({
</table> </table>
</div> </div>
)} )}
</div> </CardContent>
</Card>
)} )}
</div> </div>
); );
@ -346,8 +333,8 @@ function CodeEditorFallback() {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
background: 'linear-gradient(135deg, #F8FAFC, #EEF2FF)', background: 'var(--hero-gradient)',
color: '#4B5563', color: 'var(--muted-foreground)',
fontSize: 13, fontSize: 13,
fontWeight: 700, fontWeight: 700,
}} }}
@ -356,13 +343,3 @@ function CodeEditorFallback() {
</div> </div>
); );
} }
function toolBtn(bg: string, color: string, border: string): React.CSSProperties {
return {
display: 'flex', alignItems: 'center', gap: 5,
padding: '7px 12px', border: `1px solid ${border}`,
borderRadius: 8, background: bg, color,
fontSize: 12, fontWeight: 600, cursor: 'pointer',
fontFamily: 'inherit',
};
}

View File

@ -19,6 +19,8 @@ import {
} from '@dnd-kit/sortable'; } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities'; import { CSS } from '@dnd-kit/utilities';
import { GripVertical, Plus, Trash2, Save, Play } from 'lucide-react'; import { GripVertical, Plus, Trash2, Save, Play } from 'lucide-react';
import { Button } from '../ui/button';
import { Card, CardContent } from '../ui/card';
// ─── Types ──────────────────────────────────────────────────────────────────── // ─── Types ────────────────────────────────────────────────────────────────────
export type Indicator = 'RSI' | 'MACD' | 'EMA_50' | 'EMA_200' | 'Price' | 'Volume'; export type Indicator = 'RSI' | 'MACD' | 'EMA_50' | 'EMA_200' | 'Price' | 'Volume';
@ -88,9 +90,9 @@ function RuleCard({
transform: CSS.Transform.toString(transform), transform: CSS.Transform.toString(transform),
transition, transition,
opacity: isDragging ? 0.55 : 1, opacity: isDragging ? 0.55 : 1,
background: '#fff', background: 'var(--card)',
border: '1px solid #E5E7EB', border: '1px solid var(--border)',
borderRadius: 10, borderRadius: 14,
padding: '12px 14px', padding: '12px 14px',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
@ -100,8 +102,8 @@ function RuleCard({
}; };
const sel: React.CSSProperties = { const sel: React.CSSProperties = {
border: '1px solid #E5E7EB', borderRadius: 6, padding: '5px 8px', border: '1px solid var(--border)', borderRadius: 10, padding: '6px 10px',
fontSize: 12, background: '#F9FAFB', cursor: 'pointer', color: '#374151', fontSize: 12, background: 'var(--input)', cursor: 'pointer', color: 'var(--foreground)',
fontFamily: 'inherit', fontFamily: 'inherit',
}; };
const numInp: React.CSSProperties = { const numInp: React.CSSProperties = {
@ -292,55 +294,47 @@ export function VisualRuleBuilder({ symbol, onSave, onBacktest }: Props) {
{/* Header row */} {/* Header row */}
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 20 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 20 }}>
<div> <div>
<div style={{ fontSize: 11, color: '#9CA3AF', fontWeight: 500, marginBottom: 3 }}>Strategy name</div> <div style={{ fontSize: 11, color: 'var(--muted-foreground)', fontWeight: 500, marginBottom: 3 }}>Strategy name</div>
<input <input
value={name} value={name}
onChange={e => setName(e.target.value)} onChange={e => setName(e.target.value)}
style={{ style={{
border: '1px solid #E5E7EB', borderRadius: 8, border: '1px solid var(--border)', borderRadius: 12,
padding: '7px 12px', fontSize: 14, fontWeight: 600, padding: '7px 12px', fontSize: 14, fontWeight: 600,
color: '#111827', background: '#fff', fontFamily: 'inherit', color: 'var(--foreground)', background: 'var(--input)', fontFamily: 'inherit',
outline: 'none', width: 260, outline: 'none', width: 260,
}} }}
/> />
</div> </div>
<div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 8 }}> <div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 8 }}>
{savedMsg && ( {savedMsg && (
<span style={{ fontSize: 12, color: '#16A34A', fontWeight: 600 }}>{savedMsg}</span> <span style={{ fontSize: 12, color: 'var(--primary)', fontWeight: 600 }}>{savedMsg}</span>
)} )}
{onBacktest && ( {onBacktest && (
<button <Button
onClick={() => onBacktest(rules)} onClick={() => onBacktest(rules)}
title="Run visual strategy backtest (Cmd/Ctrl+Enter)" title="Run visual strategy backtest (Cmd/Ctrl+Enter)"
style={{ variant="outline"
display: 'flex', alignItems: 'center', gap: 6, size="sm"
padding: '8px 14px', border: '1px solid #E5E7EB', borderRadius: 8,
background: '#F9FAFB', color: '#374151', fontSize: 13, fontWeight: 600, cursor: 'pointer',
}}
> >
<Play size={13} /> Run Backtest <Play size={13} /> Run Backtest
</button> </Button>
)} )}
<button <Button
onClick={handleSave} onClick={handleSave}
disabled={saving || rules.length === 0} disabled={saving || rules.length === 0}
title="Save visual strategy (Cmd/Ctrl+S)" title="Save visual strategy (Cmd/Ctrl+S)"
style={{ size="sm"
display: 'flex', alignItems: 'center', gap: 6,
padding: '8px 14px', border: 'none', borderRadius: 8,
background: '#2563EB', color: '#fff', fontSize: 13, fontWeight: 600,
cursor: saving ? 'wait' : 'pointer', opacity: saving ? 0.7 : 1,
}}
> >
<Save size={13} /> {saving ? 'Saving…' : 'Save Strategy'} <Save size={13} /> {saving ? 'Saving…' : 'Save Strategy'}
</button> </Button>
</div> </div>
</div> </div>
{/* Column headers */} {/* Column headers */}
<div style={{ <div style={{
display: 'flex', gap: 10, paddingLeft: 58, paddingRight: 40, display: 'flex', gap: 10, paddingLeft: 58, paddingRight: 40,
fontSize: 10, fontWeight: 700, color: '#9CA3AF', textTransform: 'uppercase', fontSize: 10, fontWeight: 700, color: 'var(--muted-foreground)', textTransform: 'uppercase',
letterSpacing: '0.05em', marginBottom: 6, letterSpacing: '0.05em', marginBottom: 6,
}}> }}>
<span style={{ flex: 1 }}>IF Indicator</span> <span style={{ flex: 1 }}>IF Indicator</span>
@ -372,8 +366,8 @@ export function VisualRuleBuilder({ symbol, onSave, onBacktest }: Props) {
onClick={() => setRules(prev => [...prev, makeRule()])} onClick={() => setRules(prev => [...prev, makeRule()])}
style={{ style={{
display: 'flex', alignItems: 'center', gap: 7, display: 'flex', alignItems: 'center', gap: 7,
width: '100%', padding: '10px 0', border: '1px dashed #D1D5DB', width: '100%', padding: '10px 0', border: '1px dashed var(--border-strong)',
borderRadius: 10, background: 'transparent', color: '#6B7280', borderRadius: 14, background: 'transparent', color: 'var(--muted-foreground)',
fontSize: 13, fontWeight: 600, cursor: 'pointer', justifyContent: 'center', fontSize: 13, fontWeight: 600, cursor: 'pointer', justifyContent: 'center',
marginTop: 4, marginTop: 4,
}} }}
@ -383,20 +377,19 @@ export function VisualRuleBuilder({ symbol, onSave, onBacktest }: Props) {
{/* Rule summary */} {/* Rule summary */}
{rules.length > 0 && ( {rules.length > 0 && (
<div style={{ <Card className="mt-5 border-[var(--ring)]/20 bg-[var(--accent-soft)]">
marginTop: 20, padding: 14, background: '#F0F9FF', <CardContent className="px-4 py-4">
border: '1px solid #BAE6FD', borderRadius: 10, <div style={{ fontSize: 12, fontWeight: 700, color: 'var(--ring)', marginBottom: 6 }}>
}}>
<div style={{ fontSize: 12, fontWeight: 700, color: '#0369A1', marginBottom: 6 }}>
Strategy Preview {symbol} Strategy Preview {symbol}
</div> </div>
{rules.map((r, i) => ( {rules.map((r, i) => (
<div key={r.id} style={{ fontSize: 12, color: '#374151', marginBottom: 3 }}> <div key={r.id} style={{ fontSize: 12, color: 'var(--foreground)', marginBottom: 3 }}>
{i + 1}. IF {INDICATOR_LABELS[r.indicator]} {CONDITION_LABELS[r.condition]} {r.value} {i + 1}. IF {INDICATOR_LABELS[r.indicator]} {CONDITION_LABELS[r.condition]} {r.value}
{' → '}{r.action} {r.quantity} {r.quantityType === 'percent' ? `% of capital` : 'shares'} {' → '}{r.action} {r.quantity} {r.quantityType === 'percent' ? `% of capital` : 'shares'}
</div> </div>
))} ))}
</div> </CardContent>
</Card>
)} )}
</div> </div>
); );

View File

@ -66,8 +66,8 @@ describe('ScreenerView sector filters', () => {
expect(moreSectors).toHaveValue('Energy'); expect(moreSectors).toHaveValue('Energy');
expect(moreSectors).toHaveStyle({ expect(moreSectors).toHaveStyle({
background: '#EFF6FF', background: 'var(--accent-soft)',
color: '#2563EB', color: 'var(--primary)',
fontWeight: '700', fontWeight: '700',
}); });
await waitFor(() => expect(globalThis.fetch).toHaveBeenCalledTimes(2)); await waitFor(() => expect(globalThis.fetch).toHaveBeenCalledTimes(2));

View File

@ -51,7 +51,7 @@ describe('SettingsView legacy surface contrast', () => {
const surface = container.querySelector('.settings-legacy-surface') as HTMLDivElement; const surface = container.querySelector('.settings-legacy-surface') as HTMLDivElement;
expect(surface).toBeInTheDocument(); expect(surface).toBeInTheDocument();
expect(surface).toHaveStyle({ color: '#F9FAFB' }); expect(surface).toHaveStyle({ color: 'var(--foreground)' });
expect(screen.getByText('Account settings content')).toBeInTheDocument(); expect(screen.getByText('Account settings content')).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'Bot Config' })); await user.click(screen.getByRole('button', { name: 'Bot Config' }));