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 { 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 {
|
||||||
|
|||||||
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
@ -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>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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));
|
||||||
|
|||||||
@ -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' }));
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user