refactor(web): normalize advanced theme surfaces
This commit is contained in:
parent
76d326c793
commit
69e1b12d63
@ -5,9 +5,11 @@ import { getPlatformAccessToken } from '../lib/authSession';
|
||||
import { createRequestId } from '../../../shared/request-id.js';
|
||||
import {
|
||||
Send, X, Bot, User,
|
||||
Check, Loader2,
|
||||
Zap, Copy
|
||||
} from 'lucide-react';
|
||||
Check, Loader2,
|
||||
Zap, Copy
|
||||
} from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
interface ChatMessage {
|
||||
id: number;
|
||||
@ -297,17 +299,31 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
||||
navigator.clipboard.writeText(JSON.stringify(data, null, 2));
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
};
|
||||
|
||||
// Floating robot button - bottom right corner (portaled to body to avoid parent CSS issues)
|
||||
if (!isOpen) {
|
||||
return createPortal(
|
||||
<button
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
};
|
||||
|
||||
const assistantTint = 'var(--accent-soft)';
|
||||
const panelStyle: React.CSSProperties = {
|
||||
background: 'var(--card)',
|
||||
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)}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
@ -322,46 +338,46 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
||||
}}
|
||||
className="group"
|
||||
>
|
||||
<div style={{ position: 'relative' }}>
|
||||
{/* Glow ring */}
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
inset: '-8px',
|
||||
borderRadius: '50%',
|
||||
opacity: 0.4,
|
||||
background: 'radial-gradient(circle, rgba(0,255,136,0.25), transparent 70%)',
|
||||
transition: 'opacity 0.3s',
|
||||
}} />
|
||||
{/* Robot container */}
|
||||
<div style={{
|
||||
position: 'relative',
|
||||
<div style={{ position: 'relative' }}>
|
||||
{/* Glow ring */}
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
inset: '-8px',
|
||||
borderRadius: '50%',
|
||||
opacity: 0.4,
|
||||
background: 'radial-gradient(circle, color-mix(in oklab, var(--ring) 30%, transparent), transparent 70%)',
|
||||
transition: 'opacity 0.3s',
|
||||
}} />
|
||||
{/* Robot container */}
|
||||
<div style={{
|
||||
position: 'relative',
|
||||
width: '56px',
|
||||
height: '56px',
|
||||
borderRadius: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'linear-gradient(145deg, #1a1b2e, #0f1017)',
|
||||
border: '1.5px solid rgba(0,255,136,0.25)',
|
||||
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)',
|
||||
transition: 'transform 0.2s',
|
||||
}}>
|
||||
<RobotIcon size={34} />
|
||||
</div>
|
||||
{/* Pulse dot */}
|
||||
borderRadius: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'var(--card)',
|
||||
border: '1.5px solid var(--border)',
|
||||
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',
|
||||
}}>
|
||||
<RobotIcon size={34} />
|
||||
</div>
|
||||
{/* Pulse dot */}
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '-2px',
|
||||
right: '-2px',
|
||||
width: '14px',
|
||||
height: '14px',
|
||||
borderRadius: '50%',
|
||||
background: '#00ff88',
|
||||
border: '2px solid #0a0b10',
|
||||
boxShadow: '0 0 8px rgba(0,255,136,0.5)',
|
||||
animation: 'pulseDot 2s ease-in-out infinite',
|
||||
}} />
|
||||
</div>
|
||||
top: '-2px',
|
||||
right: '-2px',
|
||||
width: '14px',
|
||||
height: '14px',
|
||||
borderRadius: '50%',
|
||||
background: 'var(--primary)',
|
||||
border: '2px solid var(--card)',
|
||||
boxShadow: '0 0 8px color-mix(in oklab, var(--primary) 45%, transparent)',
|
||||
animation: 'pulseDot 2s ease-in-out infinite',
|
||||
}} />
|
||||
</div>
|
||||
<style>{`
|
||||
@keyframes robotFloat {
|
||||
0%, 100% { transform: translateY(0px); }
|
||||
@ -380,17 +396,17 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
||||
return createPortal(
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
onClick={() => setIsOpen(false)}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
zIndex: 999998,
|
||||
background: 'rgba(0,0,0,0.6)',
|
||||
backdropFilter: 'blur(6px)',
|
||||
animation: 'fadeIn 0.15s ease-out',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
onClick={() => setIsOpen(false)}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
zIndex: 999998,
|
||||
background: 'rgba(15, 23, 42, 0.45)',
|
||||
backdropFilter: 'blur(6px)',
|
||||
animation: 'fadeIn 0.15s ease-out',
|
||||
}}
|
||||
/>
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
bottom: '24px',
|
||||
@ -399,80 +415,75 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
||||
width: '460px',
|
||||
maxWidth: 'calc(100vw - 48px)',
|
||||
height: '640px',
|
||||
maxHeight: 'calc(100vh - 48px)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
borderRadius: '20px',
|
||||
overflow: 'hidden',
|
||||
border: '1px solid rgba(0,255,136,0.12)',
|
||||
boxShadow: '0 25px 80px rgba(0,0,0,0.6), 0 0 40px rgba(0,255,136,0.06)',
|
||||
animation: 'chatSlideUp 0.25s ease-out',
|
||||
}}>
|
||||
{/* Header */}
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #14151f 0%, #0f1017 100%)',
|
||||
borderBottom: '1px solid rgba(255,255,255,0.06)',
|
||||
padding: '14px 18px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl flex items-center justify-center" style={{
|
||||
background: 'linear-gradient(145deg, #1a1b2e, #0f1017)',
|
||||
border: '1px solid rgba(0,255,136,0.2)',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.3), 0 0 10px rgba(0,255,136,0.06)',
|
||||
}}>
|
||||
<RobotIcon size={26} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-[13px] font-bold text-white leading-none">AI Strategy Assistant</h3>
|
||||
<p className="text-[10px] text-zinc-500 mt-1">Create & manage profiles with natural language</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="w-8 h-8 rounded-lg flex items-center justify-center text-zinc-500 hover:text-white transition-all"
|
||||
style={{
|
||||
background: 'rgba(255,255,255,0.04)',
|
||||
border: '1px solid rgba(255,255,255,0.06)',
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<div className="flex-1 overflow-y-auto px-4 py-4 space-y-4" style={{
|
||||
background: '#0a0b10',
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: 'rgba(0,255,136,0.06) transparent',
|
||||
}}>
|
||||
{messages.map(msg => (
|
||||
<div key={msg.id} className={`flex gap-2.5 ${msg.role === 'user' ? 'flex-row-reverse' : ''}`}>
|
||||
{/* Avatar */}
|
||||
<div className={`shrink-0 w-7 h-7 rounded-lg flex items-center justify-center ${msg.role === 'user'
|
||||
? 'bg-blue-500/15 border border-blue-500/20'
|
||||
: 'bg-[#00ff88]/10 border border-[#00ff88]/20'
|
||||
}`}>
|
||||
{msg.role === 'user'
|
||||
? <User size={12} className="text-blue-400" />
|
||||
: <Bot size={12} className="text-[#00ff88]" />
|
||||
}
|
||||
</div>
|
||||
maxHeight: 'calc(100vh - 48px)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
borderRadius: '20px',
|
||||
overflow: 'hidden',
|
||||
animation: 'chatSlideUp 0.25s ease-out',
|
||||
...panelStyle,
|
||||
}}>
|
||||
{/* Header */}
|
||||
<div style={{
|
||||
background: 'var(--hero-gradient)',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
padding: '14px 18px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl flex items-center justify-center" style={{
|
||||
background: 'var(--card)',
|
||||
border: '1px solid var(--border)',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
|
||||
}}>
|
||||
<RobotIcon size={26} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-[13px] font-bold text-[var(--foreground)] leading-none">AI Strategy Assistant</h3>
|
||||
<p className="mt-1 text-[10px] text-[var(--muted-foreground)]">Create & manage profiles with natural language</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setIsOpen(false)}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 rounded-lg"
|
||||
>
|
||||
<X size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<div className="flex-1 overflow-y-auto px-4 py-4 space-y-4" style={{
|
||||
background: 'var(--background)',
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: 'var(--border) transparent',
|
||||
}}>
|
||||
{messages.map(msg => (
|
||||
<div key={msg.id} className={`flex gap-2.5 ${msg.role === 'user' ? 'flex-row-reverse' : ''}`}>
|
||||
{/* Avatar */}
|
||||
<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' : '')}
|
||||
style={msg.role === 'user' ? undefined : { background: assistantTint, borderColor: 'var(--border)' }}>
|
||||
{msg.role === 'user'
|
||||
? <User size={12} className="text-blue-400" />
|
||||
: <Bot size={12} className="text-[var(--primary)]" />
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* Bubble */}
|
||||
<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={{
|
||||
background: msg.role === 'user'
|
||||
? '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))',
|
||||
border: `1px solid ${msg.role === 'user' ? 'rgba(59,130,246,0.2)' : 'rgba(255,255,255,0.06)'}`,
|
||||
color: msg.role === 'user' ? '#93c5fd' : '#d4d4d8',
|
||||
whiteSpace: 'pre-wrap',
|
||||
}}>
|
||||
{msg.content}
|
||||
</div>
|
||||
<div className="rounded-xl px-3.5 py-2.5 text-[12px] leading-relaxed" style={{
|
||||
background: msg.role === 'user'
|
||||
? 'linear-gradient(135deg, rgba(59,130,246,0.15), rgba(59,130,246,0.08))'
|
||||
: 'var(--card)',
|
||||
border: `1px solid ${msg.role === 'user' ? 'rgba(59,130,246,0.2)' : 'var(--border)'}`,
|
||||
color: msg.role === 'user' ? '#93c5fd' : 'var(--foreground)',
|
||||
whiteSpace: 'pre-wrap',
|
||||
}}>
|
||||
{msg.content}
|
||||
</div>
|
||||
|
||||
{/* Profile preview card */}
|
||||
{msg.profileData && msg.action !== 'explain' && (() => {
|
||||
@ -484,23 +495,23 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
||||
|
||||
return (
|
||||
<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))',
|
||||
border: '1px solid rgba(255,255,255,0.06)',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.2)',
|
||||
background: 'var(--card-elevated)',
|
||||
border: '1px solid var(--border)',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
|
||||
}}>
|
||||
<div className="px-3.5 py-2 flex items-center justify-between" style={{
|
||||
background: 'linear-gradient(90deg, rgba(0,255,136,0.06), transparent)',
|
||||
borderBottom: '1px solid rgba(255,255,255,0.04)',
|
||||
background: 'var(--accent-soft)',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
}}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Zap size={10} className="text-[#00ff88]" />
|
||||
<span className="text-[10px] font-bold text-zinc-400 uppercase tracking-wider">
|
||||
<Zap size={10} className="text-[var(--primary)]" />
|
||||
<span className="text-[10px] font-bold text-[var(--muted-foreground)] uppercase tracking-wider">
|
||||
{msg.action === 'create_profile' ? 'New Profile' : 'Update Profile'}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<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="flex justify-between">
|
||||
<span className="text-[10px] text-zinc-600">Name</span>
|
||||
<span className="text-[11px] font-bold text-white">{activeProfileData?.name}</span>
|
||||
<span className="text-[10px] text-[var(--muted-foreground)]">Name</span>
|
||||
<span className="text-[11px] font-bold text-[var(--foreground)]">{activeProfileData?.name}</span>
|
||||
</div>
|
||||
{activeProfileData?.allocated_capital ? (
|
||||
<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>
|
||||
</div>
|
||||
) : null}
|
||||
{activeProfileData?.risk_per_trade_percent ? (
|
||||
<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>
|
||||
</div>
|
||||
) : null}
|
||||
{activeProfileData?.symbols ? (
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-[10px] text-zinc-600">Symbols</span>
|
||||
<span className="text-[10px] font-mono text-zinc-400">{activeProfileData.symbols}</span>
|
||||
<span className="text-[10px] text-[var(--muted-foreground)]">Symbols</span>
|
||||
<span className="text-[10px] font-mono text-[var(--foreground)]">{activeProfileData.symbols}</span>
|
||||
</div>
|
||||
) : null}
|
||||
{activeRules > 0 ? (
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-[10px] text-zinc-600">Rules</span>
|
||||
<span className="text-[10px] font-mono text-[#00ff88]">
|
||||
<span className="text-[10px] text-[var(--muted-foreground)]">Rules</span>
|
||||
<span className="text-[10px] font-mono text-[var(--primary)]">
|
||||
{activeRules} active
|
||||
</span>
|
||||
</div>
|
||||
@ -542,12 +553,13 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
||||
|
||||
{isEditing ? (
|
||||
<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
|
||||
value={activeProfileData?.name || ''}
|
||||
onChange={(e) => updateDraftField(msg.id, 'name', e.target.value)}
|
||||
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">
|
||||
<input
|
||||
@ -557,7 +569,8 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
||||
value={activeProfileData?.allocated_capital ?? ''}
|
||||
onChange={(e) => updateDraftField(msg.id, 'allocated_capital', e.target.value)}
|
||||
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
|
||||
type="number"
|
||||
@ -566,45 +579,50 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
||||
value={activeProfileData?.risk_per_trade_percent ?? ''}
|
||||
onChange={(e) => updateDraftField(msg.id, 'risk_per_trade_percent', e.target.value)}
|
||||
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>
|
||||
<input
|
||||
value={activeProfileData?.symbols || ''}
|
||||
onChange={(e) => updateDraftField(msg.id, 'symbols', e.target.value)}
|
||||
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">
|
||||
<span className="text-[10px] text-zinc-400 uppercase tracking-wider">Auto Trading</span>
|
||||
<div className="flex items-center justify-between rounded-lg px-2.5 py-1.5" style={inputStyle}>
|
||||
<span className="text-[10px] text-[var(--muted-foreground)] uppercase tracking-wider">Auto Trading</span>
|
||||
<select
|
||||
value={activeProfileData?.is_active === false ? 'false' : '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="false">Paused</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
<Button
|
||||
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
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!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
|
||||
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]"
|
||||
style={{
|
||||
color: '#ef4444',
|
||||
borderRight: '1px solid rgba(255,255,255,0.04)',
|
||||
color: 'var(--destructive)',
|
||||
borderRight: '1px solid var(--border)',
|
||||
}}
|
||||
>
|
||||
<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]"
|
||||
style={{
|
||||
color: '#fbbf24',
|
||||
borderRight: '1px solid rgba(255,255,255,0.04)',
|
||||
borderRight: '1px solid var(--border)',
|
||||
}}
|
||||
>
|
||||
<Copy size={11} />
|
||||
@ -625,8 +643,8 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
||||
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"
|
||||
style={{
|
||||
background: 'linear-gradient(90deg, rgba(0,255,136,0.12), rgba(0,255,136,0.06))',
|
||||
color: '#00ff88',
|
||||
background: 'var(--accent-soft)',
|
||||
color: 'var(--primary)',
|
||||
}}
|
||||
>
|
||||
<Zap size={11} />
|
||||
@ -635,17 +653,17 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
||||
</div>
|
||||
) : cancelledIds.has(msg.id) ? (
|
||||
<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)',
|
||||
color: '#71717a',
|
||||
borderTop: '1px solid var(--border)',
|
||||
color: 'var(--muted-foreground)',
|
||||
}}>
|
||||
<X size={10} />
|
||||
Cancelled
|
||||
</div>
|
||||
) : (
|
||||
<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)',
|
||||
borderTop: '1px solid rgba(0,255,136,0.1)',
|
||||
color: 'rgba(0,255,136,0.5)',
|
||||
background: 'var(--accent-soft)',
|
||||
borderTop: '1px solid var(--border)',
|
||||
color: 'var(--primary)',
|
||||
}}>
|
||||
<Check size={12} />
|
||||
Applied
|
||||
@ -654,108 +672,99 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
<span className="text-[9px] text-zinc-700 mt-1 block">
|
||||
{msg.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span className="mt-1 block text-[9px] text-[var(--muted-foreground)]">
|
||||
{msg.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Suggested quick actions - shown when only welcome message exists */}
|
||||
{messages.length <= 1 && !isLoading && (
|
||||
<div className="mt-2">
|
||||
<p style={{ fontSize: '9px', color: '#a1a1aa', textTransform: 'uppercase', letterSpacing: '0.1em', fontWeight: 700, marginBottom: '10px', paddingLeft: '4px' }}>Quick Actions</p>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{/* Suggested quick actions - shown when only welcome message exists */}
|
||||
{messages.length <= 1 && !isLoading && (
|
||||
<div className="mt-2">
|
||||
<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">
|
||||
{quickActions.map((action, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => sendMessage(action.prompt)}
|
||||
className="text-left px-3.5 py-3 rounded-xl transition-all"
|
||||
style={{
|
||||
background: '#161722',
|
||||
border: '1px solid rgba(255,255,255,0.08)',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onMouseEnter={e => {
|
||||
e.currentTarget.style.borderColor = 'rgba(0,255,136,0.25)';
|
||||
e.currentTarget.style.background = '#1a1b2a';
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
e.currentTarget.style.borderColor = 'rgba(255,255,255,0.08)';
|
||||
e.currentTarget.style.background = '#161722';
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: '13px', display: 'block', marginBottom: '3px' }}>{action.label}</span>
|
||||
<span style={{ fontSize: '10px', color: '#a1a1aa', lineHeight: '1.4', display: 'block' }}>{action.prompt.slice(0, 55)}...</span>
|
||||
</button>
|
||||
))}
|
||||
</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>
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => sendMessage(action.prompt)}
|
||||
className="text-left px-3.5 py-3 rounded-xl transition-all"
|
||||
style={{
|
||||
background: 'var(--card)',
|
||||
border: '1px solid var(--border)',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onMouseEnter={e => {
|
||||
e.currentTarget.style.borderColor = 'var(--ring)';
|
||||
e.currentTarget.style.background = 'var(--accent-soft)';
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
e.currentTarget.style.borderColor = 'var(--border)';
|
||||
e.currentTarget.style.background = 'var(--card)';
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: '13px', display: 'block', marginBottom: '3px', color: 'var(--foreground)' }}>{action.label}</span>
|
||||
<span style={{ fontSize: '10px', color: 'var(--muted-foreground)', lineHeight: '1.4', display: 'block' }}>{action.prompt.slice(0, 55)}...</span>
|
||||
</button>
|
||||
))}
|
||||
</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>
|
||||
|
||||
{/* Input area */}
|
||||
<div style={{
|
||||
background: '#0e0f18',
|
||||
borderTop: '1px solid rgba(0,255,136,0.1)',
|
||||
padding: '14px 16px',
|
||||
}}>
|
||||
<div className="flex items-end gap-2.5">
|
||||
<div className="flex-1 relative">
|
||||
{/* Input area */}
|
||||
<div style={{
|
||||
background: 'var(--card)',
|
||||
borderTop: '1px solid var(--border)',
|
||||
padding: '14px 16px',
|
||||
}}>
|
||||
<div className="flex items-end gap-2.5">
|
||||
<div className="flex-1 relative">
|
||||
<textarea
|
||||
ref={inputRef}
|
||||
value={input}
|
||||
onChange={e => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Describe a strategy profile..."
|
||||
disabled={isLoading}
|
||||
rows={2}
|
||||
className="w-full rounded-xl py-3 pl-4 pr-12 outline-none disabled:opacity-50 transition-all resize-none"
|
||||
style={{
|
||||
background: '#161722',
|
||||
border: '1px solid rgba(255,255,255,0.12)',
|
||||
boxShadow: 'inset 0 2px 6px rgba(0,0,0,0.3)',
|
||||
lineHeight: '1.5',
|
||||
fontFamily: 'inherit',
|
||||
fontSize: '13px',
|
||||
color: '#ffffff',
|
||||
caretColor: '#00ff88',
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={() => sendMessage()}
|
||||
disabled={!input.trim() || isLoading}
|
||||
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"
|
||||
style={{
|
||||
background: input.trim() ? 'linear-gradient(135deg, #00ff88, #00cc6a)' : 'rgba(255,255,255,0.05)',
|
||||
color: input.trim() ? '#000' : '#52525b',
|
||||
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>
|
||||
disabled={isLoading}
|
||||
rows={2}
|
||||
className="w-full rounded-xl py-3 pl-4 pr-12 outline-none disabled:opacity-50 transition-all resize-none"
|
||||
style={{ ...inputStyle, lineHeight: '1.5', fontFamily: 'inherit', fontSize: '13px' }}
|
||||
/>
|
||||
<button
|
||||
onClick={() => sendMessage()}
|
||||
disabled={!input.trim() || isLoading}
|
||||
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"
|
||||
style={{
|
||||
background: input.trim() ? 'var(--primary)' : 'var(--accent-soft)',
|
||||
color: input.trim() ? 'var(--primary-foreground)' : 'var(--muted-foreground)',
|
||||
boxShadow: input.trim() ? '0 2px 10px color-mix(in oklab, var(--primary) 25%, transparent)' : 'none',
|
||||
}}
|
||||
>
|
||||
<Send size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-1.5 ml-1 text-[9px] text-[var(--muted-foreground)]">Enter to send · Shift+Enter new line</p>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
@keyframes chatSlideUp {
|
||||
|
||||
@ -1,248 +1,157 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { fetchMarketplacePresets } from '../lib/marketplaceApi';
|
||||
import {
|
||||
Activity,
|
||||
ArrowUpRight,
|
||||
Shield,
|
||||
Zap,
|
||||
Scale,
|
||||
CheckCircle,
|
||||
TrendingUp,
|
||||
Info,
|
||||
Dna,
|
||||
Cpu,
|
||||
Fingerprint,
|
||||
Users,
|
||||
LineChart
|
||||
} from 'lucide-react';
|
||||
import type { StrategyPreset } from '../lib/PresetRegistry';
|
||||
import { STRATEGY_PRESETS } from '../lib/PresetRegistry';
|
||||
|
||||
interface PresetMarketplaceProps {
|
||||
onSelect: (preset: StrategyPreset) => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
const StrategyMarketplaceCard: React.FC<{
|
||||
preset: StrategyPreset,
|
||||
onSelect: (preset: StrategyPreset) => void,
|
||||
index: number
|
||||
}> = ({ preset, onSelect, index }) => {
|
||||
const isSafe = preset.riskStyleId === 'safe';
|
||||
const isBalanced = preset.riskStyleId === 'balanced';
|
||||
const isAggressive = preset.riskStyleId === 'aggressive';
|
||||
|
||||
const themeColor = isSafe ? '#00ff88' : isBalanced ? '#3498db' : '#ff3366';
|
||||
|
||||
// Visual metadata
|
||||
const performanceValue = isAggressive ? '+14.2%' : isSafe ? '+4.8%' : '+8.5%';
|
||||
const volatilityRating = isAggressive ? 'High' : isSafe ? 'Low' : 'Med';
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
background: '#14151a',
|
||||
borderRadius: '28px',
|
||||
border: '1px solid rgba(255, 255, 255, 0.05)',
|
||||
padding: '32px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
transition: 'all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275)',
|
||||
cursor: 'default',
|
||||
height: '100%',
|
||||
minHeight: '620px'
|
||||
}} className="marketplace-card-hover">
|
||||
|
||||
{/* 1. Header Area - Perfectly Aligned */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '28px' }}>
|
||||
<div style={{ display: 'flex', gap: '14px', alignItems: 'center' }}>
|
||||
<div style={{
|
||||
width: '44px',
|
||||
height: '44px',
|
||||
borderRadius: '14px',
|
||||
background: 'rgba(255,255,255,0.03)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
border: '1px solid rgba(255,255,255,0.05)',
|
||||
color: 'rgba(255,255,255,0.5)'
|
||||
}}>
|
||||
{isSafe ? <Shield size={20} /> : isBalanced ? <Scale size={20} /> : <Zap size={20} />}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '2px' }}>
|
||||
<span style={{ fontSize: '10px', fontWeight: 900, color: '#444', textTransform: 'uppercase', letterSpacing: '2px' }}>Strategy Profile</span>
|
||||
<span style={{ fontSize: '13px', fontWeight: 800, color: 'white', textTransform: 'uppercase', letterSpacing: '0.5px' }}>{preset.riskStyleId} Strategy</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
background: 'rgba(0,0,0,0.3)',
|
||||
border: '1px solid rgba(255,255,255,0.05)',
|
||||
padding: '6px 12px',
|
||||
borderRadius: '10px',
|
||||
fontSize: '11px',
|
||||
fontWeight: 900,
|
||||
color: '#333'
|
||||
}}>
|
||||
V{index + 1}.4
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 2. Identity - Aligned Left */}
|
||||
<div style={{ marginBottom: '24px', display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: '12px' }}>
|
||||
<h3 style={{
|
||||
fontSize: '24px',
|
||||
fontWeight: 900,
|
||||
color: 'white',
|
||||
lineHeight: '1.1',
|
||||
letterSpacing: '-0.02em',
|
||||
margin: 0
|
||||
}}>
|
||||
{preset.name}
|
||||
</h3>
|
||||
<div style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
background: 'rgba(0, 255, 136, 0.05)',
|
||||
padding: '6px 14px',
|
||||
borderRadius: '8px',
|
||||
fontSize: '11px',
|
||||
color: '#00ff88',
|
||||
fontWeight: 900,
|
||||
textTransform: 'uppercase',
|
||||
border: '1px solid rgba(0, 255, 136, 0.1)',
|
||||
letterSpacing: '0.5px'
|
||||
}}>
|
||||
<Fingerprint size={14} /> Institutional Alpha {performanceValue}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p style={{
|
||||
fontSize: '14px',
|
||||
color: '#888',
|
||||
lineHeight: '1.6',
|
||||
marginBottom: '28px',
|
||||
textAlign: 'left',
|
||||
flex: 1
|
||||
}}>
|
||||
{preset.description} Optimized for dominance and high-conviction momentum in volatile periods.
|
||||
</p>
|
||||
|
||||
{/* 3. Specs Grid - Balanced Alignment */}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gap: '14px',
|
||||
marginBottom: '28px'
|
||||
}}>
|
||||
{[
|
||||
{ label: 'Growth', value: performanceValue, icon: <TrendingUp size={14} />, color: '#00ff88' },
|
||||
{ label: 'Latency', value: 'Low', icon: <Cpu size={14} />, color: '#3498db' },
|
||||
{ label: 'Liquidity', value: 'Prime', icon: <Users size={14} />, color: '#ffaa00' },
|
||||
{ label: 'Rating', value: volatilityRating, icon: <Activity size={14} />, color: '#ff3366' }
|
||||
].map((spec, i) => (
|
||||
<div key={i} style={{
|
||||
background: 'rgba(0,0,0,0.2)',
|
||||
border: '1px solid rgba(255,255,255,0.03)',
|
||||
padding: '16px',
|
||||
borderRadius: '20px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '6px',
|
||||
alignItems: 'flex-start'
|
||||
}}>
|
||||
<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(() => {
|
||||
import {
|
||||
Activity,
|
||||
ArrowUpRight,
|
||||
CheckCircle,
|
||||
Cpu,
|
||||
Fingerprint,
|
||||
Info,
|
||||
LineChart,
|
||||
Scale,
|
||||
Shield,
|
||||
TrendingUp,
|
||||
Users,
|
||||
Zap,
|
||||
} from 'lucide-react';
|
||||
import type { StrategyPreset } from '../lib/PresetRegistry';
|
||||
import { STRATEGY_PRESETS } from '../lib/PresetRegistry';
|
||||
import { Button } from './ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card';
|
||||
import { PageHeader } from './ui/page-header';
|
||||
|
||||
interface PresetMarketplaceProps {
|
||||
onSelect: (preset: StrategyPreset) => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
const themeByRisk: Record<string, { tone: 'safe' | 'balanced' | 'aggressive'; label: string }> = {
|
||||
safe: { tone: 'safe', label: 'Low Volatility' },
|
||||
balanced: { tone: 'balanced', label: 'Balanced' },
|
||||
aggressive: { tone: 'aggressive', label: 'High Conviction' },
|
||||
};
|
||||
|
||||
function metricForPreset(preset: StrategyPreset) {
|
||||
if (preset.riskStyleId === 'aggressive') {
|
||||
return { performance: '+14.2%', volatility: 'High', icon: Zap, accent: 'var(--destructive)' };
|
||||
}
|
||||
if (preset.riskStyleId === 'safe') {
|
||||
return { performance: '+4.8%', volatility: 'Low', icon: Shield, accent: 'var(--primary)' };
|
||||
}
|
||||
return { performance: '+8.5%', volatility: 'Medium', icon: Scale, accent: 'var(--ring)' };
|
||||
}
|
||||
|
||||
const StrategyMarketplaceCard: React.FC<{
|
||||
preset: StrategyPreset;
|
||||
onSelect: (preset: StrategyPreset) => void;
|
||||
index: number;
|
||||
}> = ({ preset, onSelect, index }) => {
|
||||
const theme = themeByRisk[preset.riskStyleId] || themeByRisk.balanced;
|
||||
const metric = metricForPreset(preset);
|
||||
const RiskIcon = metric.icon;
|
||||
|
||||
return (
|
||||
<Card className="h-full rounded-[28px] transition duration-200 hover:-translate-y-1 hover:border-[var(--ring)]/30 hover:shadow-xl">
|
||||
<CardHeader className="gap-5">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className="flex h-11 w-11 items-center justify-center rounded-2xl border"
|
||||
style={{
|
||||
background: 'var(--accent-soft)',
|
||||
borderColor: 'var(--border)',
|
||||
color: metric.accent,
|
||||
}}
|
||||
>
|
||||
<RiskIcon size={18} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="text-[10px] font-semibold uppercase tracking-[0.2em] text-[var(--muted-foreground)]">
|
||||
Strategy Profile
|
||||
</div>
|
||||
<div className="text-xs font-semibold uppercase tracking-wide text-[var(--foreground)]">
|
||||
{preset.riskStyleId} strategy
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-chip">V{index + 1}.4</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<CardTitle className="text-2xl">{preset.name}</CardTitle>
|
||||
<div
|
||||
className="inline-flex w-fit items-center gap-2 rounded-lg border px-3 py-1.5 text-[11px] font-semibold uppercase tracking-wide"
|
||||
style={{
|
||||
background: 'var(--accent-soft)',
|
||||
borderColor: 'var(--border)',
|
||||
color: metric.accent,
|
||||
}}
|
||||
>
|
||||
<Fingerprint size={13} />
|
||||
{theme.label} • {metric.performance}
|
||||
</div>
|
||||
<CardDescription className="text-sm leading-6">
|
||||
{preset.description} Optimized for automated execution without changing your core risk budget.
|
||||
</CardDescription>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex h-full flex-col gap-6">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{[
|
||||
{ label: 'Growth', value: metric.performance, icon: <TrendingUp size={14} /> },
|
||||
{ label: 'Latency', value: 'Low', icon: <Cpu size={14} /> },
|
||||
{ label: 'Liquidity', value: 'Prime', icon: <Users size={14} /> },
|
||||
{ label: 'Volatility', value: metric.volatility, icon: <Activity size={14} /> },
|
||||
].map((spec) => (
|
||||
<div
|
||||
key={spec.label}
|
||||
className="rounded-2xl border p-4"
|
||||
style={{ background: 'var(--card-elevated)', borderColor: 'var(--border)' }}
|
||||
>
|
||||
<div className="mb-2 flex items-center gap-2 text-[10px] font-semibold uppercase tracking-[0.14em] text-[var(--muted-foreground)]">
|
||||
{spec.icon}
|
||||
{spec.label}
|
||||
</div>
|
||||
<div className="text-base font-semibold text-[var(--foreground)]">{spec.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-sm text-[var(--muted-foreground)]">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle size={15} className="text-[var(--primary)]" />
|
||||
Logical invariant verified
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle size={15} className="text-[var(--primary)]" />
|
||||
Risk-isolated execution
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-auto flex gap-3">
|
||||
<Button className="h-12 flex-1 rounded-2xl" onClick={() => onSelect(preset)}>
|
||||
Use This Strategy
|
||||
<ArrowUpRight size={15} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-12 w-12 rounded-2xl"
|
||||
title="Preset information"
|
||||
>
|
||||
<Info size={18} />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export const PresetMarketplace: React.FC<PresetMarketplaceProps> = ({ onSelect, onClose }) => {
|
||||
const [customPresets, setCustomPresets] = useState<StrategyPreset[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCustomPresets = async () => {
|
||||
try {
|
||||
const data = await fetchMarketplacePresets();
|
||||
@ -255,147 +164,60 @@ export const PresetMarketplace: React.FC<PresetMarketplaceProps> = ({ onSelect,
|
||||
typicalTradesPerDay: d.typical_trades_per_day,
|
||||
performanceTag: d.performance_tag,
|
||||
isPopular: d.is_popular,
|
||||
strategy_config: d.strategy_config
|
||||
strategy_config: d.strategy_config,
|
||||
}));
|
||||
setCustomPresets(mappedData as any);
|
||||
setCustomPresets(mappedData as StrategyPreset[]);
|
||||
} catch (e) {
|
||||
console.error('Error fetching marketplace presets:', e);
|
||||
}
|
||||
};
|
||||
|
||||
fetchCustomPresets();
|
||||
}, []);
|
||||
|
||||
const allPresets = [...STRATEGY_PRESETS, ...customPresets];
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
maxWidth: '1400px',
|
||||
margin: '0 auto',
|
||||
padding: '0 20px 100px 20px',
|
||||
animation: 'fadeIn 0.7s ease-out'
|
||||
}}>
|
||||
{/* Premium Header Alignment Fix */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
marginBottom: '60px',
|
||||
padding: '60px 0',
|
||||
borderBottom: '1px solid rgba(255,255,255,0.05)',
|
||||
position: 'relative',
|
||||
alignItems: 'flex-start' /* Force consistent left alignment */
|
||||
}}>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
opacity: 0.03,
|
||||
pointerEvents: 'none',
|
||||
transform: 'translate(40px, -20px)'
|
||||
}}>
|
||||
<Dna size={320} strokeWidth={1} />
|
||||
</div>
|
||||
|
||||
{/* Removed indenting line for perfect optical left-alignment */}
|
||||
<div style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
color: '#00ff88',
|
||||
fontSize: '11px',
|
||||
fontWeight: 900,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '4px',
|
||||
marginBottom: '24px'
|
||||
}}>
|
||||
QUANTITATIVE REPOSITORY
|
||||
<div style={{ width: '30px', height: '1px', background: '#00ff88', opacity: 0.2 }} />
|
||||
</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>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
void fetchCustomPresets();
|
||||
}, []);
|
||||
|
||||
const allPresets = useMemo(() => [...STRATEGY_PRESETS, ...customPresets], [customPresets]);
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||
<PageHeader
|
||||
title="Strategy Marketplace"
|
||||
description="Browse reusable strategy profiles with preconfigured risk posture and execution bias."
|
||||
/>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="stat-chip">{allPresets.length} presets</div>
|
||||
{onClose ? (
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Return
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 xl:grid-cols-3">
|
||||
{allPresets.map((preset, idx) => (
|
||||
<StrategyMarketplaceCard key={preset.id} preset={preset} index={idx} onSelect={onSelect} />
|
||||
))}
|
||||
|
||||
<Card className="rounded-[28px] border-dashed">
|
||||
<CardContent className="flex min-h-[560px] flex-col items-center justify-center gap-4 px-8 py-10 text-center">
|
||||
<div
|
||||
className="flex h-16 w-16 items-center justify-center rounded-3xl border"
|
||||
style={{ background: 'var(--accent-soft)', borderColor: 'var(--border)' }}
|
||||
>
|
||||
<LineChart size={28} className="text-[var(--muted-foreground)]" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.2em] text-[var(--muted-foreground)]">
|
||||
Analyzing DNA
|
||||
</div>
|
||||
<p className="mx-auto max-w-xs text-sm text-[var(--muted-foreground)]">
|
||||
Verification queue is active for new marketplace strategies.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { AppContext, type AppContextValue } from '../../context/AppContext';
|
||||
import { ThemeProvider } from '../theme/ThemeProvider';
|
||||
import { Header } from './Header';
|
||||
|
||||
const { fetchMarketIndicesMock } = vi.hoisted(() => ({
|
||||
@ -45,11 +46,13 @@ function setVisibilityState(value: DocumentVisibilityState) {
|
||||
|
||||
function renderHeader() {
|
||||
return render(
|
||||
<AppContext.Provider value={appContext}>
|
||||
<MemoryRouter>
|
||||
<Header />
|
||||
</MemoryRouter>
|
||||
</AppContext.Provider>,
|
||||
<ThemeProvider>
|
||||
<AppContext.Provider value={appContext}>
|
||||
<MemoryRouter>
|
||||
<Header />
|
||||
</MemoryRouter>
|
||||
</AppContext.Provider>
|
||||
</ThemeProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -9,6 +9,8 @@ import { createTradeProfile } from '../../lib/profileApi';
|
||||
import '../../lib/monacoLocalWorkers';
|
||||
import { tradingRuntime } from '../../lib/runtime';
|
||||
import { createRequestId } from '../../../../shared/request-id.js';
|
||||
import { Button } from '../ui/button';
|
||||
import { Card, CardContent } from '../ui/card';
|
||||
|
||||
const DEFAULT_TEMPLATE = `/**
|
||||
* Custom Trading Strategy
|
||||
@ -197,42 +199,30 @@ export function CodeStrategyEditor({
|
||||
return (
|
||||
<div onKeyDownCapture={handleShortcut}>
|
||||
{/* Toolbar */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
|
||||
<span style={{ fontSize: 13, fontWeight: 600, color: '#374151' }}>
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<span className="text-sm font-semibold text-[var(--foreground)]">
|
||||
Code Editor — {symbol}
|
||||
</span>
|
||||
<span style={{ fontSize: 11, color: '#6B7280' }}>
|
||||
<span className="text-xs text-[var(--muted-foreground)]">
|
||||
Cmd/Ctrl-S save · Cmd/Ctrl-Enter backtest
|
||||
</span>
|
||||
<div style={{ flex: 1 }} />
|
||||
<button onClick={handleCopy} title="Copy code"
|
||||
style={toolBtn('#F9FAFB','#374151','#E5E7EB')}>
|
||||
<Button variant="outline" size="sm" onClick={handleCopy} title="Copy code">
|
||||
<Copy size={13} /> Copy
|
||||
</button>
|
||||
<button onClick={handleReset} title="Reset to template"
|
||||
style={toolBtn('#F9FAFB','#374151','#E5E7EB')}>
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={handleReset} title="Reset to template">
|
||||
<RotateCcw size={13} /> Reset
|
||||
</button>
|
||||
<button 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',
|
||||
}}>
|
||||
</Button>
|
||||
<Button variant={saved ? 'outline' : 'default'} size="sm" onClick={handleSave} disabled={saving} title="Save code strategy (Cmd/Ctrl+S)">
|
||||
<Save size={13} /> {saving ? 'Saving...' : saved ? 'Saved!' : 'Save'}
|
||||
</button>
|
||||
<button onClick={handleRunBacktest} disabled={running} title="Run backtest (Cmd/Ctrl+Enter)"
|
||||
style={{
|
||||
...toolBtn('#2563EB','#fff','transparent'),
|
||||
opacity: running ? 0.7 : 1,
|
||||
cursor: running ? 'wait' : 'pointer',
|
||||
}}>
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleRunBacktest} disabled={running} title="Run backtest (Cmd/Ctrl+Enter)">
|
||||
<Play size={13} /> {running ? 'Running…' : 'Run Backtest'}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 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 />}>
|
||||
<MonacoEditor
|
||||
height="380px"
|
||||
@ -242,7 +232,7 @@ export function CodeStrategyEditor({
|
||||
setCode(v ?? '');
|
||||
setSaved(false);
|
||||
}}
|
||||
theme="light"
|
||||
theme={document.documentElement.classList.contains('dark') ? 'vs-dark' : 'light'}
|
||||
options={{
|
||||
fontSize: 13,
|
||||
minimap: { enabled: false },
|
||||
@ -262,22 +252,18 @@ export function CodeStrategyEditor({
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div style={{
|
||||
marginTop: 12, padding: 12, background: '#FEF2F2',
|
||||
border: '1px solid #FCA5A5', borderRadius: 8,
|
||||
fontSize: 13, color: '#DC2626', fontFamily: 'monospace',
|
||||
}}>
|
||||
{error}
|
||||
</div>
|
||||
<Card className="mt-3 border-[var(--destructive)]/30 bg-[var(--destructive)]/10">
|
||||
<CardContent className="px-4 py-3 text-sm font-medium text-[var(--destructive)] [font-family:monospace]">
|
||||
{error}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Backtest results */}
|
||||
{result && (
|
||||
<div style={{
|
||||
marginTop: 12, padding: 14, background: '#F0FDF4',
|
||||
border: '1px solid #86EFAC', borderRadius: 10,
|
||||
}}>
|
||||
<div style={{ fontSize: 13, fontWeight: 700, color: '#15803D', marginBottom: 10 }}>
|
||||
<Card className="mt-3 border-[var(--primary)]/20 bg-[var(--accent-soft)]">
|
||||
<CardContent className="space-y-3 px-4 py-4">
|
||||
<div style={{ fontSize: 13, fontWeight: 700, color: 'var(--primary)', marginBottom: 10 }}>
|
||||
Backtest Results
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5,1fr)', gap: 12 }}>
|
||||
@ -289,13 +275,13 @@ export function CodeStrategyEditor({
|
||||
['Max Drawdown', fmt(result.maxDrawdown, '%')],
|
||||
].map(([label, val]) => (
|
||||
<div key={label} style={{
|
||||
background: '#fff', borderRadius: 8, padding: '10px 12px',
|
||||
border: '1px solid #D1FAE5',
|
||||
background: 'var(--card)', borderRadius: 12, padding: '10px 12px',
|
||||
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}
|
||||
</div>
|
||||
<div style={{ fontSize: 16, fontWeight: 700, color: '#111827' }}>{val}</div>
|
||||
<div style={{ fontSize: 16, fontWeight: 700, color: 'var(--foreground)' }}>{val}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -308,19 +294,19 @@ export function CodeStrategyEditor({
|
||||
</div>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 11 }}>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '1px solid #E5E7EB' }}>
|
||||
<tr style={{ borderBottom: '1px solid var(--border)' }}>
|
||||
{['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>
|
||||
</thead>
|
||||
<tbody>
|
||||
{result.tradeLog.slice(-10).map((t: any, i: number) => (
|
||||
<tr key={i} style={{ borderBottom: '1px solid #F9FAFB' }}>
|
||||
<td style={{ padding: '4px 8px', color: '#374151' }}>{t.date ?? '—'}</td>
|
||||
<tr key={i} style={{ borderBottom: '1px solid var(--border)' }}>
|
||||
<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: '#374151' }}>{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.price != null ? `$${t.price.toFixed(2)}` : '—'}</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 }}>
|
||||
{t.pnl != null ? `${t.pnl >= 0 ? '+' : ''}$${t.pnl.toFixed(2)}` : '—'}
|
||||
</td>
|
||||
@ -330,7 +316,8 @@ export function CodeStrategyEditor({
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@ -346,8 +333,8 @@ function CodeEditorFallback() {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'linear-gradient(135deg, #F8FAFC, #EEF2FF)',
|
||||
color: '#4B5563',
|
||||
background: 'var(--hero-gradient)',
|
||||
color: 'var(--muted-foreground)',
|
||||
fontSize: 13,
|
||||
fontWeight: 700,
|
||||
}}
|
||||
@ -356,13 +343,3 @@ function CodeEditorFallback() {
|
||||
</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',
|
||||
};
|
||||
}
|
||||
|
||||
@ -19,6 +19,8 @@ import {
|
||||
} from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { GripVertical, Plus, Trash2, Save, Play } from 'lucide-react';
|
||||
import { Button } from '../ui/button';
|
||||
import { Card, CardContent } from '../ui/card';
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
export type Indicator = 'RSI' | 'MACD' | 'EMA_50' | 'EMA_200' | 'Price' | 'Volume';
|
||||
@ -88,9 +90,9 @@ function RuleCard({
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.55 : 1,
|
||||
background: '#fff',
|
||||
border: '1px solid #E5E7EB',
|
||||
borderRadius: 10,
|
||||
background: 'var(--card)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 14,
|
||||
padding: '12px 14px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
@ -100,8 +102,8 @@ function RuleCard({
|
||||
};
|
||||
|
||||
const sel: React.CSSProperties = {
|
||||
border: '1px solid #E5E7EB', borderRadius: 6, padding: '5px 8px',
|
||||
fontSize: 12, background: '#F9FAFB', cursor: 'pointer', color: '#374151',
|
||||
border: '1px solid var(--border)', borderRadius: 10, padding: '6px 10px',
|
||||
fontSize: 12, background: 'var(--input)', cursor: 'pointer', color: 'var(--foreground)',
|
||||
fontFamily: 'inherit',
|
||||
};
|
||||
const numInp: React.CSSProperties = {
|
||||
@ -292,55 +294,47 @@ export function VisualRuleBuilder({ symbol, onSave, onBacktest }: Props) {
|
||||
{/* Header row */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 20 }}>
|
||||
<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
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
style={{
|
||||
border: '1px solid #E5E7EB', borderRadius: 8,
|
||||
border: '1px solid var(--border)', borderRadius: 12,
|
||||
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,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
{savedMsg && (
|
||||
<span style={{ fontSize: 12, color: '#16A34A', fontWeight: 600 }}>{savedMsg}</span>
|
||||
<span style={{ fontSize: 12, color: 'var(--primary)', fontWeight: 600 }}>{savedMsg}</span>
|
||||
)}
|
||||
{onBacktest && (
|
||||
<button
|
||||
<Button
|
||||
onClick={() => onBacktest(rules)}
|
||||
title="Run visual strategy backtest (Cmd/Ctrl+Enter)"
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
padding: '8px 14px', border: '1px solid #E5E7EB', borderRadius: 8,
|
||||
background: '#F9FAFB', color: '#374151', fontSize: 13, fontWeight: 600, cursor: 'pointer',
|
||||
}}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
<Play size={13} /> Run Backtest
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
<button
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={saving || rules.length === 0}
|
||||
title="Save visual strategy (Cmd/Ctrl+S)"
|
||||
style={{
|
||||
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,
|
||||
}}
|
||||
size="sm"
|
||||
>
|
||||
<Save size={13} /> {saving ? 'Saving…' : 'Save Strategy'}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Column headers */}
|
||||
<div style={{
|
||||
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,
|
||||
}}>
|
||||
<span style={{ flex: 1 }}>IF Indicator</span>
|
||||
@ -372,8 +366,8 @@ export function VisualRuleBuilder({ symbol, onSave, onBacktest }: Props) {
|
||||
onClick={() => setRules(prev => [...prev, makeRule()])}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 7,
|
||||
width: '100%', padding: '10px 0', border: '1px dashed #D1D5DB',
|
||||
borderRadius: 10, background: 'transparent', color: '#6B7280',
|
||||
width: '100%', padding: '10px 0', border: '1px dashed var(--border-strong)',
|
||||
borderRadius: 14, background: 'transparent', color: 'var(--muted-foreground)',
|
||||
fontSize: 13, fontWeight: 600, cursor: 'pointer', justifyContent: 'center',
|
||||
marginTop: 4,
|
||||
}}
|
||||
@ -383,20 +377,19 @@ export function VisualRuleBuilder({ symbol, onSave, onBacktest }: Props) {
|
||||
|
||||
{/* Rule summary */}
|
||||
{rules.length > 0 && (
|
||||
<div style={{
|
||||
marginTop: 20, padding: 14, background: '#F0F9FF',
|
||||
border: '1px solid #BAE6FD', borderRadius: 10,
|
||||
}}>
|
||||
<div style={{ fontSize: 12, fontWeight: 700, color: '#0369A1', marginBottom: 6 }}>
|
||||
<Card className="mt-5 border-[var(--ring)]/20 bg-[var(--accent-soft)]">
|
||||
<CardContent className="px-4 py-4">
|
||||
<div style={{ fontSize: 12, fontWeight: 700, color: 'var(--ring)', marginBottom: 6 }}>
|
||||
Strategy Preview — {symbol}
|
||||
</div>
|
||||
{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}
|
||||
{' → '}{r.action} {r.quantity} {r.quantityType === 'percent' ? `% of capital` : 'shares'}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -66,8 +66,8 @@ describe('ScreenerView sector filters', () => {
|
||||
|
||||
expect(moreSectors).toHaveValue('Energy');
|
||||
expect(moreSectors).toHaveStyle({
|
||||
background: '#EFF6FF',
|
||||
color: '#2563EB',
|
||||
background: 'var(--accent-soft)',
|
||||
color: 'var(--primary)',
|
||||
fontWeight: '700',
|
||||
});
|
||||
await waitFor(() => expect(globalThis.fetch).toHaveBeenCalledTimes(2));
|
||||
|
||||
@ -51,7 +51,7 @@ describe('SettingsView legacy surface contrast', () => {
|
||||
|
||||
const surface = container.querySelector('.settings-legacy-surface') as HTMLDivElement;
|
||||
expect(surface).toBeInTheDocument();
|
||||
expect(surface).toHaveStyle({ color: '#F9FAFB' });
|
||||
expect(surface).toHaveStyle({ color: 'var(--foreground)' });
|
||||
expect(screen.getByText('Account settings content')).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Bot Config' }));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user