feat(ui): migrate trade plan and chat controls
This commit is contained in:
parent
3892093dc4
commit
324e34d537
@ -4,22 +4,22 @@ import { tradingRuntime } from '../lib/runtime';
|
|||||||
import { getPlatformAccessToken } from '../lib/authSession';
|
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 { Button, Input, Select, Textarea } from './ui/Primitives';
|
||||||
import { cn } from '../lib/utils';
|
import { cn } from '../lib/utils';
|
||||||
|
|
||||||
interface ChatMessage {
|
interface ChatMessage {
|
||||||
id: number;
|
id: number;
|
||||||
role: 'user' | 'assistant';
|
role: 'user' | 'assistant';
|
||||||
content: string;
|
content: string;
|
||||||
profileData?: any;
|
profileData?: any;
|
||||||
action?: string;
|
action?: string;
|
||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChatControlProps {
|
interface ChatControlProps {
|
||||||
profiles: any[];
|
profiles: any[];
|
||||||
onApplyProfile: (action: string, profile: any) => Promise<{ success: boolean; error?: string }>;
|
onApplyProfile: (action: string, profile: any) => Promise<{ success: boolean; error?: string }>;
|
||||||
@ -84,58 +84,58 @@ export const normalizeProfileForApply = (profileData: any) => ({
|
|||||||
symbols: String(profileData?.symbols || '').trim(),
|
symbols: String(profileData?.symbols || '').trim(),
|
||||||
is_active: profileData?.is_active !== false,
|
is_active: profileData?.is_active !== false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3D Robot SVG Icon
|
// 3D Robot SVG Icon
|
||||||
const RobotIcon = ({ size = 32 }: { size?: number }) => (
|
const RobotIcon = ({ size = 32 }: { size?: number }) => (
|
||||||
<svg width={size} height={size} viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width={size} height={size} viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
{/* Antenna */}
|
{/* Antenna */}
|
||||||
<line x1="32" y1="6" x2="32" y2="14" stroke="#00ff88" strokeWidth="2.5" strokeLinecap="round" />
|
<line x1="32" y1="6" x2="32" y2="14" stroke="#00ff88" strokeWidth="2.5" strokeLinecap="round" />
|
||||||
<circle cx="32" cy="5" r="3" fill="#00ff88" opacity="0.9">
|
<circle cx="32" cy="5" r="3" fill="#00ff88" opacity="0.9">
|
||||||
<animate attributeName="opacity" values="0.5;1;0.5" dur="2s" repeatCount="indefinite" />
|
<animate attributeName="opacity" values="0.5;1;0.5" dur="2s" repeatCount="indefinite" />
|
||||||
</circle>
|
</circle>
|
||||||
{/* Head */}
|
{/* Head */}
|
||||||
<rect x="14" y="14" width="36" height="28" rx="8" fill="url(#headGrad)" stroke="#00ff88" strokeWidth="1.5" opacity="0.95" />
|
<rect x="14" y="14" width="36" height="28" rx="8" fill="url(#headGrad)" stroke="#00ff88" strokeWidth="1.5" opacity="0.95" />
|
||||||
{/* Eyes */}
|
{/* Eyes */}
|
||||||
<circle cx="24" cy="28" r="4.5" fill="#0a0b10" />
|
<circle cx="24" cy="28" r="4.5" fill="#0a0b10" />
|
||||||
<circle cx="24" cy="28" r="3" fill="#00ff88" opacity="0.9">
|
<circle cx="24" cy="28" r="3" fill="#00ff88" opacity="0.9">
|
||||||
<animate attributeName="r" values="3;2.5;3" dur="3s" repeatCount="indefinite" />
|
<animate attributeName="r" values="3;2.5;3" dur="3s" repeatCount="indefinite" />
|
||||||
</circle>
|
</circle>
|
||||||
<circle cx="40" cy="28" r="4.5" fill="#0a0b10" />
|
<circle cx="40" cy="28" r="4.5" fill="#0a0b10" />
|
||||||
<circle cx="40" cy="28" r="3" fill="#00ff88" opacity="0.9">
|
<circle cx="40" cy="28" r="3" fill="#00ff88" opacity="0.9">
|
||||||
<animate attributeName="r" values="3;2.5;3" dur="3s" repeatCount="indefinite" />
|
<animate attributeName="r" values="3;2.5;3" dur="3s" repeatCount="indefinite" />
|
||||||
</circle>
|
</circle>
|
||||||
{/* Mouth */}
|
{/* Mouth */}
|
||||||
<rect x="24" y="35" width="16" height="3" rx="1.5" fill="#00ff88" opacity="0.4" />
|
<rect x="24" y="35" width="16" height="3" rx="1.5" fill="#00ff88" opacity="0.4" />
|
||||||
{/* Body */}
|
{/* Body */}
|
||||||
<rect x="18" y="44" width="28" height="14" rx="5" fill="url(#bodyGrad)" stroke="#00ff88" strokeWidth="1" opacity="0.8" />
|
<rect x="18" y="44" width="28" height="14" rx="5" fill="url(#bodyGrad)" stroke="#00ff88" strokeWidth="1" opacity="0.8" />
|
||||||
{/* Body detail */}
|
{/* Body detail */}
|
||||||
<circle cx="32" cy="51" r="3" fill="#00ff88" opacity="0.3" />
|
<circle cx="32" cy="51" r="3" fill="#00ff88" opacity="0.3" />
|
||||||
{/* Arms */}
|
{/* Arms */}
|
||||||
<rect x="8" y="46" width="8" height="10" rx="4" fill="url(#headGrad)" stroke="#00ff88" strokeWidth="1" opacity="0.7" />
|
<rect x="8" y="46" width="8" height="10" rx="4" fill="url(#headGrad)" stroke="#00ff88" strokeWidth="1" opacity="0.7" />
|
||||||
<rect x="48" y="46" width="8" height="10" rx="4" fill="url(#headGrad)" stroke="#00ff88" strokeWidth="1" opacity="0.7" />
|
<rect x="48" y="46" width="8" height="10" rx="4" fill="url(#headGrad)" stroke="#00ff88" strokeWidth="1" opacity="0.7" />
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="headGrad" x1="14" y1="14" x2="50" y2="42" gradientUnits="userSpaceOnUse">
|
<linearGradient id="headGrad" x1="14" y1="14" x2="50" y2="42" gradientUnits="userSpaceOnUse">
|
||||||
<stop stopColor="#1a1b2e" />
|
<stop stopColor="#1a1b2e" />
|
||||||
<stop offset="1" stopColor="#12131a" />
|
<stop offset="1" stopColor="#12131a" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient id="bodyGrad" x1="18" y1="44" x2="46" y2="58" gradientUnits="userSpaceOnUse">
|
<linearGradient id="bodyGrad" x1="18" y1="44" x2="46" y2="58" gradientUnits="userSpaceOnUse">
|
||||||
<stop stopColor="#1a1b2e" />
|
<stop stopColor="#1a1b2e" />
|
||||||
<stop offset="1" stopColor="#0f1017" />
|
<stop offset="1" stopColor="#0f1017" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [messages, setMessages] = useState<ChatMessage[]>([
|
const [messages, setMessages] = useState<ChatMessage[]>([
|
||||||
{
|
{
|
||||||
id: 0,
|
id: 0,
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: "Hi! I'm your trading assistant. Tell me what kind of strategy profile you'd like to create or modify, and I'll generate the configuration for you.\n\nTry: \"Create a conservative BTC scalper with $1000 capital\"",
|
content: "Hi! I'm your trading assistant. Tell me what kind of strategy profile you'd like to create or modify, and I'll generate the configuration for you.\n\nTry: \"Create a conservative BTC scalper with $1000 capital\"",
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
const [input, setInput] = useState('');
|
const [input, setInput] = useState('');
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [appliedIds, setAppliedIds] = useState<Set<number>>(new Set());
|
const [appliedIds, setAppliedIds] = useState<Set<number>>(new Set());
|
||||||
@ -175,31 +175,31 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
setTimeout(() => inputRef.current?.focus(), 300);
|
setTimeout(() => inputRef.current?.focus(), 300);
|
||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
const sendMessage = async (text?: string) => {
|
const sendMessage = async (text?: string) => {
|
||||||
const msg = text || input.trim();
|
const msg = text || input.trim();
|
||||||
if (!msg || isLoading) return;
|
if (!msg || isLoading) return;
|
||||||
|
|
||||||
const userMsg: ChatMessage = {
|
const userMsg: ChatMessage = {
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: msg,
|
content: msg,
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
};
|
};
|
||||||
setMessages(prev => [...prev, userMsg]);
|
setMessages(prev => [...prev, userMsg]);
|
||||||
setInput('');
|
setInput('');
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const apiUrl = tradingRuntime.tradingApiUrl;
|
const apiUrl = tradingRuntime.tradingApiUrl;
|
||||||
const accessToken = await getPlatformAccessToken();
|
const accessToken = await getPlatformAccessToken();
|
||||||
@ -213,50 +213,50 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
message: msg,
|
message: msg,
|
||||||
context: profiles.map(p => ({
|
context: profiles.map(p => ({
|
||||||
id: p.id,
|
id: p.id,
|
||||||
name: p.name,
|
name: p.name,
|
||||||
allocated_capital: p.allocated_capital,
|
allocated_capital: p.allocated_capital,
|
||||||
risk_per_trade_percent: p.risk_per_trade_percent,
|
risk_per_trade_percent: p.risk_per_trade_percent,
|
||||||
symbols: p.symbols,
|
symbols: p.symbols,
|
||||||
is_active: p.is_active,
|
is_active: p.is_active,
|
||||||
strategy_config: p.strategy_config,
|
strategy_config: p.strategy_config,
|
||||||
})),
|
})),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const err = await res.json();
|
const err = await res.json();
|
||||||
throw new Error(err.error || 'Chat request failed');
|
throw new Error(err.error || 'Chat request failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
const assistantMsg: ChatMessage = {
|
const assistantMsg: ChatMessage = {
|
||||||
id: Date.now() + 1,
|
id: Date.now() + 1,
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: data.summary || data.reasoning || 'Profile configuration generated.',
|
content: data.summary || data.reasoning || 'Profile configuration generated.',
|
||||||
profileData: data.profile || null,
|
profileData: data.profile || null,
|
||||||
action: data.action,
|
action: data.action,
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (data.reasoning && data.summary) {
|
if (data.reasoning && data.summary) {
|
||||||
assistantMsg.content = `${data.summary}\n\n${data.reasoning}`;
|
assistantMsg.content = `${data.summary}\n\n${data.reasoning}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
setMessages(prev => [...prev, assistantMsg]);
|
setMessages(prev => [...prev, assistantMsg]);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setMessages(prev => [...prev, {
|
setMessages(prev => [...prev, {
|
||||||
id: Date.now() + 1,
|
id: Date.now() + 1,
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: `Error: ${err.message}`,
|
content: `Error: ${err.message}`,
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
}]);
|
}]);
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleApply = async (msg: ChatMessage) => {
|
const handleApply = async (msg: ChatMessage) => {
|
||||||
if (msg.profileData && msg.action) {
|
if (msg.profileData && msg.action) {
|
||||||
const activeDraft = draftProfiles[msg.id] || msg.profileData;
|
const activeDraft = draftProfiles[msg.id] || msg.profileData;
|
||||||
@ -275,15 +275,15 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
|||||||
}]);
|
}]);
|
||||||
} else {
|
} else {
|
||||||
setMessages(prev => [...prev, {
|
setMessages(prev => [...prev, {
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: `Failed to ${msg.action === 'create_profile' ? 'create' : 'update'} profile: ${result.error || 'Unknown error'}. Please try again or check your permissions.`,
|
content: `Failed to ${msg.action === 'create_profile' ? 'create' : 'update'} profile: ${result.error || 'Unknown error'}. Please try again or check your permissions.`,
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
}]);
|
}]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = (msgId: number) => {
|
const handleCancel = (msgId: number) => {
|
||||||
setCancelledIds(prev => new Set(prev).add(msgId));
|
setCancelledIds(prev => new Set(prev).add(msgId));
|
||||||
closeDraftEditor(msgId);
|
closeDraftEditor(msgId);
|
||||||
@ -291,14 +291,14 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
|||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: 'Profile creation cancelled. You can ask me to create a different one or modify the parameters.',
|
content: 'Profile creation cancelled. You can ask me to create a different one or modify the parameters.',
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
}]);
|
}]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyJson = (data: any) => {
|
const copyJson = (data: any) => {
|
||||||
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();
|
||||||
@ -323,21 +323,23 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
|||||||
// Floating robot button - bottom right corner (portaled to body to avoid parent CSS issues)
|
// Floating robot button - bottom right corner (portaled to body to avoid parent CSS issues)
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<button
|
<Button
|
||||||
onClick={() => setIsOpen(true)}
|
type="button"
|
||||||
style={{
|
onClick={() => setIsOpen(true)}
|
||||||
position: 'fixed',
|
variant="ghost"
|
||||||
bottom: '24px',
|
style={{
|
||||||
right: '24px',
|
position: 'fixed',
|
||||||
zIndex: 99999,
|
bottom: '24px',
|
||||||
cursor: 'pointer',
|
right: '24px',
|
||||||
background: 'none',
|
zIndex: 99999,
|
||||||
border: 'none',
|
cursor: 'pointer',
|
||||||
padding: 0,
|
background: 'none',
|
||||||
animation: 'robotFloat 3s ease-in-out infinite',
|
border: 'none',
|
||||||
}}
|
padding: 0,
|
||||||
className="group"
|
animation: 'robotFloat 3s ease-in-out infinite',
|
||||||
>
|
}}
|
||||||
|
className="group"
|
||||||
|
>
|
||||||
<div style={{ position: 'relative' }}>
|
<div style={{ position: 'relative' }}>
|
||||||
{/* Glow ring */}
|
{/* Glow ring */}
|
||||||
<div style={{
|
<div style={{
|
||||||
@ -351,8 +353,8 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
|||||||
{/* 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',
|
||||||
@ -365,8 +367,8 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
|||||||
<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',
|
||||||
@ -378,24 +380,24 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
|||||||
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); }
|
||||||
50% { transform: translateY(-4px); }
|
50% { transform: translateY(-4px); }
|
||||||
}
|
}
|
||||||
@keyframes pulseDot {
|
@keyframes pulseDot {
|
||||||
0%, 100% { transform: scale(1); opacity: 1; }
|
0%, 100% { transform: scale(1); opacity: 1; }
|
||||||
50% { transform: scale(0.8); opacity: 0.6; }
|
50% { transform: scale(0.8); opacity: 0.6; }
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
</button>,
|
</Button>,
|
||||||
document.body
|
document.body
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<>
|
<>
|
||||||
{/* Backdrop */}
|
{/* Backdrop */}
|
||||||
<div
|
<div
|
||||||
onClick={() => setIsOpen(false)}
|
onClick={() => setIsOpen(false)}
|
||||||
style={{
|
style={{
|
||||||
@ -407,14 +409,14 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
|||||||
animation: 'fadeIn 0.15s ease-out',
|
animation: 'fadeIn 0.15s ease-out',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div style={{
|
<div style={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
bottom: '24px',
|
bottom: '24px',
|
||||||
right: '24px',
|
right: '24px',
|
||||||
zIndex: 999999,
|
zIndex: 999999,
|
||||||
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',
|
||||||
@ -448,7 +450,7 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
|||||||
<Button
|
<Button
|
||||||
onClick={() => setIsOpen(false)}
|
onClick={() => setIsOpen(false)}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="sm"
|
||||||
className="h-8 w-8 rounded-lg"
|
className="h-8 w-8 rounded-lg"
|
||||||
>
|
>
|
||||||
<X size={14} />
|
<X size={14} />
|
||||||
@ -471,9 +473,9 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
|||||||
: <Bot size={12} className="text-[var(--primary)]" />
|
: <Bot size={12} className="text-[var(--primary)]" />
|
||||||
}
|
}
|
||||||
</div>
|
</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))'
|
||||||
@ -484,8 +486,8 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
|||||||
}}>
|
}}>
|
||||||
{msg.content}
|
{msg.content}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Profile preview card */}
|
{/* Profile preview card */}
|
||||||
{msg.profileData && msg.action !== 'explain' && (() => {
|
{msg.profileData && msg.action !== 'explain' && (() => {
|
||||||
const activeProfileData = draftProfiles[msg.id] || msg.profileData;
|
const activeProfileData = draftProfiles[msg.id] || msg.profileData;
|
||||||
const isEditing = editingIds.has(msg.id);
|
const isEditing = editingIds.has(msg.id);
|
||||||
@ -509,13 +511,16 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
|||||||
{msg.action === 'create_profile' ? 'New Profile' : 'Update Profile'}
|
{msg.action === 'create_profile' ? 'New Profile' : 'Update Profile'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
onClick={() => copyJson(activeProfileData)}
|
onClick={() => copyJson(activeProfileData)}
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
className="text-[var(--muted-foreground)] hover:text-[var(--foreground)] 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} />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-3.5 py-2.5 space-y-1.5">
|
<div className="px-3.5 py-2.5 space-y-1.5">
|
||||||
@ -554,7 +559,7 @@ 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-[var(--muted-foreground)] 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"
|
||||||
@ -562,7 +567,7 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
|||||||
style={inputStyle}
|
style={inputStyle}
|
||||||
/>
|
/>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
step="1"
|
step="1"
|
||||||
@ -572,7 +577,7 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
|||||||
className="w-full rounded-lg px-2.5 py-1.5 text-[11px] outline-none"
|
className="w-full rounded-lg px-2.5 py-1.5 text-[11px] outline-none"
|
||||||
style={inputStyle}
|
style={inputStyle}
|
||||||
/>
|
/>
|
||||||
<input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
step="0.1"
|
step="0.1"
|
||||||
@ -583,7 +588,7 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
|||||||
style={inputStyle}
|
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)"
|
||||||
@ -592,15 +597,16 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
|||||||
/>
|
/>
|
||||||
<div className="flex items-center justify-between rounded-lg px-2.5 py-1.5" style={inputStyle}>
|
<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>
|
<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] outline-none"
|
className="rounded px-2 py-1 text-[10px] outline-none"
|
||||||
style={inputStyle}
|
style={inputStyle}
|
||||||
>
|
options={[
|
||||||
<option value="true">Active</option>
|
{ value: 'true', label: 'Active' },
|
||||||
<option value="false">Paused</option>
|
{ value: 'false', label: 'Paused' },
|
||||||
</select>
|
]}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button
|
<Button
|
||||||
@ -617,8 +623,10 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
|||||||
|
|
||||||
{!appliedIds.has(msg.id) && !cancelledIds.has(msg.id) ? (
|
{!appliedIds.has(msg.id) && !cancelledIds.has(msg.id) ? (
|
||||||
<div className="flex" style={{ borderTop: '1px solid var(--border)' }}>
|
<div className="flex" style={{ borderTop: '1px solid var(--border)' }}>
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
onClick={() => handleCancel(msg.id)}
|
onClick={() => handleCancel(msg.id)}
|
||||||
|
variant="ghost"
|
||||||
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: 'var(--destructive)',
|
color: 'var(--destructive)',
|
||||||
@ -627,9 +635,11 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
|||||||
>
|
>
|
||||||
<X size={11} />
|
<X size={11} />
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
onClick={() => isEditing ? closeDraftEditor(msg.id) : openDraftEditor(msg)}
|
onClick={() => isEditing ? closeDraftEditor(msg.id) : openDraftEditor(msg)}
|
||||||
|
variant="ghost"
|
||||||
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',
|
||||||
@ -638,9 +648,11 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
|||||||
>
|
>
|
||||||
<Copy size={11} />
|
<Copy size={11} />
|
||||||
{isEditing ? 'Done Editing' : 'Edit Params'}
|
{isEditing ? 'Done Editing' : 'Edit Params'}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
onClick={() => handleApply(msg)}
|
onClick={() => handleApply(msg)}
|
||||||
|
variant="ghost"
|
||||||
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: 'var(--accent-soft)',
|
background: 'var(--accent-soft)',
|
||||||
@ -649,7 +661,7 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
|||||||
>
|
>
|
||||||
<Zap size={11} />
|
<Zap size={11} />
|
||||||
Apply to Dashboard
|
Apply to Dashboard
|
||||||
</button>
|
</Button>
|
||||||
</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={{
|
||||||
@ -677,18 +689,20 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
|||||||
{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: 'var(--muted-foreground)', 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
|
||||||
|
type="button"
|
||||||
key={i}
|
key={i}
|
||||||
onClick={() => sendMessage(action.prompt)}
|
onClick={() => sendMessage(action.prompt)}
|
||||||
|
variant="ghost"
|
||||||
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: 'var(--card)',
|
background: 'var(--card)',
|
||||||
@ -706,11 +720,11 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
|||||||
>
|
>
|
||||||
<span style={{ fontSize: '13px', display: 'block', marginBottom: '3px', color: 'var(--foreground)' }}>{action.label}</span>
|
<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>
|
<span style={{ fontSize: '10px', color: 'var(--muted-foreground)', lineHeight: '1.4', display: 'block' }}>{action.prompt.slice(0, 55)}...</span>
|
||||||
</button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="flex gap-2.5">
|
<div className="flex gap-2.5">
|
||||||
@ -726,10 +740,10 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div ref={messagesEndRef} />
|
<div ref={messagesEndRef} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Input area */}
|
{/* Input area */}
|
||||||
<div style={{
|
<div style={{
|
||||||
background: 'var(--card)',
|
background: 'var(--card)',
|
||||||
@ -738,20 +752,22 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
|||||||
}}>
|
}}>
|
||||||
<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={{ ...inputStyle, lineHeight: '1.5', fontFamily: 'inherit', fontSize: '13px' }}
|
style={{ ...inputStyle, lineHeight: '1.5', fontFamily: 'inherit', fontSize: '13px' }}
|
||||||
/>
|
/>
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
onClick={() => sendMessage()}
|
onClick={() => sendMessage()}
|
||||||
disabled={!input.trim() || isLoading}
|
disabled={!input.trim() || isLoading}
|
||||||
|
variant="ghost"
|
||||||
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"
|
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={{
|
style={{
|
||||||
background: input.trim() ? 'var(--primary)' : 'var(--accent-soft)',
|
background: input.trim() ? 'var(--primary)' : 'var(--accent-soft)',
|
||||||
@ -760,24 +776,24 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Send size={14} />
|
<Send size={14} />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1.5 ml-1 text-[9px] text-[var(--muted-foreground)]">Enter to send · Shift+Enter new line</p>
|
<p className="mt-1.5 ml-1 text-[9px] text-[var(--muted-foreground)]">Enter to send · Shift+Enter new line</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>{`
|
<style>{`
|
||||||
@keyframes chatSlideUp {
|
@keyframes chatSlideUp {
|
||||||
from { transform: translateY(20px) scale(0.97); opacity: 0; }
|
from { transform: translateY(20px) scale(0.97); opacity: 0; }
|
||||||
to { transform: translateY(0) scale(1); opacity: 1; }
|
to { transform: translateY(0) scale(1); opacity: 1; }
|
||||||
}
|
}
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from { opacity: 0; }
|
from { opacity: 0; }
|
||||||
to { opacity: 1; }
|
to { opacity: 1; }
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
</>,
|
</>,
|
||||||
document.body
|
document.body
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,9 +7,7 @@ import { tradingRuntime } from '../lib/runtime';
|
|||||||
import { createRequestId } from '../../../shared/request-id.js';
|
import { createRequestId } from '../../../shared/request-id.js';
|
||||||
import { SkeletonBlock } from '../components/Skeleton';
|
import { SkeletonBlock } from '../components/Skeleton';
|
||||||
import { PageHeader } from '../components/ui/page-header';
|
import { PageHeader } from '../components/ui/page-header';
|
||||||
import { Button } from '../components/ui/button';
|
import { Button, Input, Select } from '../components/ui/Primitives';
|
||||||
import { Input } from '../components/ui/input';
|
|
||||||
import { Select } from '../components/ui/select';
|
|
||||||
import { Card, CardContent } from '../components/ui/card';
|
import { Card, CardContent } from '../components/ui/card';
|
||||||
|
|
||||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||||
@ -198,22 +196,20 @@ export function ScreenerView() {
|
|||||||
value={String(capIdx)}
|
value={String(capIdx)}
|
||||||
onChange={e => setCapIdx(Number(e.target.value))}
|
onChange={e => setCapIdx(Number(e.target.value))}
|
||||||
style={{ width: 180 }}
|
style={{ width: 180 }}
|
||||||
>
|
options={CAP_OPTIONS.map((c, i) => ({ value: String(i), label: c.label }))}
|
||||||
{CAP_OPTIONS.map((c, i) => (
|
/>
|
||||||
<option key={c.label} value={i}>{c.label}</option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: 5, flexWrap: 'wrap', alignItems: 'center' }}>
|
<div style={{ display: 'flex', gap: 5, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||||
<SlidersHorizontal size={13} color="var(--muted-foreground)" />
|
<SlidersHorizontal size={13} color="var(--muted-foreground)" />
|
||||||
{SECTORS.slice(0, 6).map(s => (
|
{SECTORS.slice(0, 6).map(s => (
|
||||||
<button
|
<Button
|
||||||
key={s}
|
key={s}
|
||||||
onClick={() => setSector(s)}
|
onClick={() => setSector(s)}
|
||||||
|
variant={sector === s ? 'secondary' : 'outline'}
|
||||||
|
size="sm"
|
||||||
style={{
|
style={{
|
||||||
padding: '5px 10px', borderRadius: 20,
|
padding: '5px 10px', borderRadius: 20,
|
||||||
border: '1px solid', fontSize: 11, fontWeight: 600,
|
border: '1px solid', fontSize: 11, fontWeight: 600,
|
||||||
cursor: 'pointer',
|
|
||||||
borderColor: sector === s ? 'var(--primary)' : 'var(--border)',
|
borderColor: sector === s ? 'var(--primary)' : 'var(--border)',
|
||||||
background: sector === s ? 'var(--accent-soft)' : 'var(--card)',
|
background: sector === s ? 'var(--accent-soft)' : 'var(--card)',
|
||||||
color: sector === s ? 'var(--primary)' : 'var(--muted-foreground)',
|
color: sector === s ? 'var(--primary)' : 'var(--muted-foreground)',
|
||||||
@ -221,12 +217,16 @@ export function ScreenerView() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{s}
|
{s}
|
||||||
</button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
<Select
|
<Select
|
||||||
aria-label="More sectors"
|
aria-label="More sectors"
|
||||||
value={SECTORS.indexOf(sector) >= 6 ? sector : ''}
|
value={SECTORS.indexOf(sector) >= 6 ? sector : ''}
|
||||||
onChange={e => e.target.value && setSector(e.target.value)}
|
onChange={e => e.target.value && setSector(e.target.value)}
|
||||||
|
options={[
|
||||||
|
{ value: '', label: 'More sectors…' },
|
||||||
|
...SECTORS.slice(6).map(s => ({ value: s, label: s })),
|
||||||
|
]}
|
||||||
style={{
|
style={{
|
||||||
width: 140,
|
width: 140,
|
||||||
borderRadius: 999,
|
borderRadius: 999,
|
||||||
@ -235,12 +235,7 @@ export function ScreenerView() {
|
|||||||
color: moreSectorSelected ? 'var(--primary)' : 'var(--muted-foreground)',
|
color: moreSectorSelected ? 'var(--primary)' : 'var(--muted-foreground)',
|
||||||
fontWeight: moreSectorSelected ? 700 : 500,
|
fontWeight: moreSectorSelected ? 700 : 500,
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<option value="">More sectors…</option>
|
|
||||||
{SECTORS.slice(6).map(s => (
|
|
||||||
<option key={s} value={s}>{s}</option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@ -12,11 +12,9 @@ import {
|
|||||||
type ManualEntryPayload,
|
type ManualEntryPayload,
|
||||||
} from '../lib/manualEntriesApi';
|
} from '../lib/manualEntriesApi';
|
||||||
import { fetchTradeProfiles, type TradeProfilePayload } from '../lib/profileApi';
|
import { fetchTradeProfiles, type TradeProfilePayload } from '../lib/profileApi';
|
||||||
import { Button } from '../components/ui/button';
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../components/ui/card';
|
||||||
import { Input } from '../components/ui/input';
|
|
||||||
import { PageHeader } from '../components/ui/page-header';
|
import { PageHeader } from '../components/ui/page-header';
|
||||||
import { Select } from '../components/ui/select';
|
import { Button, Input, Select } from '../components/ui/Primitives';
|
||||||
import {
|
import {
|
||||||
DEFAULT_TRADE_PLANS_UI_STATE,
|
DEFAULT_TRADE_PLANS_UI_STATE,
|
||||||
reduceTradePlansUiState,
|
reduceTradePlansUiState,
|
||||||
@ -1000,13 +998,14 @@ export function SimpleView() {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||||
<div className="grid gap-3 md:grid-cols-2">
|
<div className="grid gap-3 md:grid-cols-2">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
dispatch({ type: 'clear-feedback' });
|
dispatch({ type: 'clear-feedback' });
|
||||||
dispatch({ type: 'set-selected-holding-trade-id', value: null });
|
dispatch({ type: 'set-selected-holding-trade-id', value: null });
|
||||||
updateDraft('side', 'buy');
|
updateDraft('side', 'buy');
|
||||||
}}
|
}}
|
||||||
|
variant="ghost"
|
||||||
className={`rounded-[1.25rem] border px-4 py-4 text-left transition ${
|
className={`rounded-[1.25rem] border px-4 py-4 text-left transition ${
|
||||||
draft.side === 'buy'
|
draft.side === 'buy'
|
||||||
? 'border-[var(--primary)] bg-[var(--accent-soft)]'
|
? 'border-[var(--primary)] bg-[var(--accent-soft)]'
|
||||||
@ -1016,8 +1015,8 @@ export function SimpleView() {
|
|||||||
<div className="text-[11px] font-black uppercase tracking-[0.24em] text-[var(--muted-foreground)]">Create plan</div>
|
<div className="text-[11px] font-black uppercase tracking-[0.24em] text-[var(--muted-foreground)]">Create plan</div>
|
||||||
<div className="mt-1 text-sm font-semibold text-[var(--foreground)]">New short-term buy plan</div>
|
<div className="mt-1 text-sm font-semibold text-[var(--foreground)]">New short-term buy plan</div>
|
||||||
<div className="mt-1 text-sm text-[var(--muted-foreground)]">Arm a dip-buy trigger and let the app manage the profit exit after fill.</div>
|
<div className="mt-1 text-sm text-[var(--muted-foreground)]">Arm a dip-buy trigger and let the app manage the profit exit after fill.</div>
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
dispatch({ type: 'clear-feedback' });
|
dispatch({ type: 'clear-feedback' });
|
||||||
@ -1027,6 +1026,7 @@ export function SimpleView() {
|
|||||||
updateDraft('side', 'sell');
|
updateDraft('side', 'sell');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
variant="ghost"
|
||||||
className={`rounded-[1.25rem] border px-4 py-4 text-left transition ${
|
className={`rounded-[1.25rem] border px-4 py-4 text-left transition ${
|
||||||
draft.side === 'sell'
|
draft.side === 'sell'
|
||||||
? 'border-[var(--primary)] bg-[var(--accent-soft)]'
|
? 'border-[var(--primary)] bg-[var(--accent-soft)]'
|
||||||
@ -1036,7 +1036,7 @@ export function SimpleView() {
|
|||||||
<div className="text-[11px] font-black uppercase tracking-[0.24em] text-[var(--muted-foreground)]">Manage holding</div>
|
<div className="text-[11px] font-black uppercase tracking-[0.24em] text-[var(--muted-foreground)]">Manage holding</div>
|
||||||
<div className="mt-1 text-sm font-semibold text-[var(--foreground)]">Attach an exit plan</div>
|
<div className="mt-1 text-sm font-semibold text-[var(--foreground)]">Attach an exit plan</div>
|
||||||
<div className="mt-1 text-sm text-[var(--muted-foreground)]">Choose an existing holding and place it back under managed profit-taking.</div>
|
<div className="mt-1 text-sm text-[var(--muted-foreground)]">Choose an existing holding and place it back under managed profit-taking.</div>
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{draft.side === 'sell' && (
|
{draft.side === 'sell' && (
|
||||||
@ -1049,17 +1049,13 @@ export function SimpleView() {
|
|||||||
if (selected) applyHoldingToDraft(selected);
|
if (selected) applyHoldingToDraft(selected);
|
||||||
}}
|
}}
|
||||||
disabled={availableSellHoldings.length === 0}
|
disabled={availableSellHoldings.length === 0}
|
||||||
>
|
options={availableSellHoldings.length === 0
|
||||||
{availableSellHoldings.length === 0 ? (
|
? [{ value: '', label: 'No eligible holdings available' }]
|
||||||
<option value="">No eligible holdings available</option>
|
: availableSellHoldings.map((holding) => ({
|
||||||
) : (
|
value: holding.tradeId || '',
|
||||||
availableSellHoldings.map((holding) => (
|
label: `${holding.symbol} · ${holding.size} @ ${holding.entryPrice.toFixed(4)}`,
|
||||||
<option key={`${holding.symbol}:${holding.tradeId || 'holding'}`} value={holding.tradeId || ''}>
|
}))}
|
||||||
{holding.symbol} · {holding.size} @ {holding.entryPrice.toFixed(4)}
|
/>
|
||||||
</option>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</Select>
|
|
||||||
<span className="block text-[11px] text-[var(--muted-foreground)]">
|
<span className="block text-[11px] text-[var(--muted-foreground)]">
|
||||||
Trade Plans can manage an existing filled holding by attaching a profit exit target to it.
|
Trade Plans can manage an existing filled holding by attaching a profit exit target to it.
|
||||||
</span>
|
</span>
|
||||||
@ -1104,7 +1100,7 @@ export function SimpleView() {
|
|||||||
{filteredSymbolSuggestions.length > 0 ? (
|
{filteredSymbolSuggestions.length > 0 ? (
|
||||||
<div className="flex flex-wrap gap-2 pt-1">
|
<div className="flex flex-wrap gap-2 pt-1">
|
||||||
{filteredSymbolSuggestions.map((symbol) => (
|
{filteredSymbolSuggestions.map((symbol) => (
|
||||||
<button
|
<Button
|
||||||
key={symbol}
|
key={symbol}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -1121,6 +1117,8 @@ export function SimpleView() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
className={`rounded-full border px-3 py-1 text-[11px] font-semibold transition ${
|
className={`rounded-full border px-3 py-1 text-[11px] font-semibold transition ${
|
||||||
symbol === normalizedSymbol
|
symbol === normalizedSymbol
|
||||||
? 'border-[var(--primary)] bg-[var(--accent-soft)] text-[var(--primary)]'
|
? 'border-[var(--primary)] bg-[var(--accent-soft)] text-[var(--primary)]'
|
||||||
@ -1128,7 +1126,7 @@ export function SimpleView() {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{symbol}
|
{symbol}
|
||||||
</button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@ -1139,10 +1137,11 @@ export function SimpleView() {
|
|||||||
<Select
|
<Select
|
||||||
value={draft.side}
|
value={draft.side}
|
||||||
onChange={(e) => updateDraft('side', e.target.value as SimpleSide)}
|
onChange={(e) => updateDraft('side', e.target.value as SimpleSide)}
|
||||||
>
|
options={[
|
||||||
<option value="buy">Buy the dip + profit exit</option>
|
{ value: 'buy', label: 'Buy the dip + profit exit' },
|
||||||
<option value="sell">Manage existing holding at profit</option>
|
{ value: 'sell', label: 'Manage existing holding at profit' },
|
||||||
</Select>
|
]}
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -1194,10 +1193,11 @@ export function SimpleView() {
|
|||||||
<Select
|
<Select
|
||||||
value={draft.sizingMode}
|
value={draft.sizingMode}
|
||||||
onChange={(e) => updateDraft('sizingMode', e.target.value as 'quantity' | 'amount')}
|
onChange={(e) => updateDraft('sizingMode', e.target.value as 'quantity' | 'amount')}
|
||||||
>
|
options={[
|
||||||
<option value="quantity">Quantity / fractional shares</option>
|
{ value: 'quantity', label: 'Quantity / fractional shares' },
|
||||||
<option value="amount">USD amount</option>
|
{ value: 'amount', label: 'USD amount' },
|
||||||
</Select>
|
]}
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label className="space-y-2">
|
<label className="space-y-2">
|
||||||
@ -1255,10 +1255,11 @@ export function SimpleView() {
|
|||||||
<Select
|
<Select
|
||||||
value={draft.dropMode}
|
value={draft.dropMode}
|
||||||
onChange={(e) => updateDraft('dropMode', e.target.value as TriggerMode)}
|
onChange={(e) => updateDraft('dropMode', e.target.value as TriggerMode)}
|
||||||
>
|
options={[
|
||||||
<option value="dollar">Dollar drop from current market</option>
|
{ value: 'dollar', label: 'Dollar drop from current market' },
|
||||||
<option value="percent">Percent drop from current market</option>
|
{ value: 'percent', label: 'Percent drop from current market' },
|
||||||
</Select>
|
]}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<label className="space-y-3">
|
<label className="space-y-3">
|
||||||
<span className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">
|
<span className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">
|
||||||
@ -1279,10 +1280,11 @@ export function SimpleView() {
|
|||||||
<Select
|
<Select
|
||||||
value={draft.profitMode}
|
value={draft.profitMode}
|
||||||
onChange={(e) => updateDraft('profitMode', e.target.value as TriggerMode)}
|
onChange={(e) => updateDraft('profitMode', e.target.value as TriggerMode)}
|
||||||
>
|
options={[
|
||||||
<option value="dollar">Dollar gain from purchase</option>
|
{ value: 'dollar', label: 'Dollar gain from purchase' },
|
||||||
<option value="percent">Percent gain from purchase</option>
|
{ value: 'percent', label: 'Percent gain from purchase' },
|
||||||
</Select>
|
]}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<label className="space-y-3">
|
<label className="space-y-3">
|
||||||
<span className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">
|
<span className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user