feat(ui): migrate trade plan and chat controls

This commit is contained in:
Saravana Achu Mac 2026-05-06 15:49:04 -07:00
parent 3892093dc4
commit 324e34d537
3 changed files with 318 additions and 305 deletions

View File

@ -8,7 +8,7 @@ import {
Check, Loader2,
Zap, Copy
} from 'lucide-react';
import { Button } from './ui/button';
import { Button, Input, Select, Textarea } from './ui/Primitives';
import { cn } from '../lib/utils';
interface ChatMessage {
@ -323,8 +323,10 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
// Floating robot button - bottom right corner (portaled to body to avoid parent CSS issues)
if (!isOpen) {
return createPortal(
<button
<Button
type="button"
onClick={() => setIsOpen(true)}
variant="ghost"
style={{
position: 'fixed',
bottom: '24px',
@ -388,7 +390,7 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
50% { transform: scale(0.8); opacity: 0.6; }
}
`}</style>
</button>,
</Button>,
document.body
);
}
@ -448,7 +450,7 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
<Button
onClick={() => setIsOpen(false)}
variant="ghost"
size="icon"
size="sm"
className="h-8 w-8 rounded-lg"
>
<X size={14} />
@ -509,13 +511,16 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
{msg.action === 'create_profile' ? 'New Profile' : 'Update Profile'}
</span>
</div>
<button
<Button
type="button"
onClick={() => copyJson(activeProfileData)}
variant="ghost"
size="sm"
className="text-[var(--muted-foreground)] hover:text-[var(--foreground)] transition-colors"
title="Copy JSON"
>
<Copy size={11} />
</button>
</Button>
</div>
<div className="px-3.5 py-2.5 space-y-1.5">
@ -554,7 +559,7 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
{isEditing ? (
<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>
<input
<Input
value={activeProfileData?.name || ''}
onChange={(e) => updateDraftField(msg.id, 'name', e.target.value)}
placeholder="Profile Name"
@ -562,7 +567,7 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
style={inputStyle}
/>
<div className="grid grid-cols-2 gap-2">
<input
<Input
type="number"
min="0"
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"
style={inputStyle}
/>
<input
<Input
type="number"
min="0"
step="0.1"
@ -583,7 +588,7 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
style={inputStyle}
/>
</div>
<input
<Input
value={activeProfileData?.symbols || ''}
onChange={(e) => updateDraftField(msg.id, 'symbols', e.target.value)}
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}>
<span className="text-[10px] text-[var(--muted-foreground)] uppercase tracking-wider">Auto Trading</span>
<select
<Select
value={activeProfileData?.is_active === false ? 'false' : 'true'}
onChange={(e) => updateDraftField(msg.id, 'is_active', e.target.value === 'true')}
className="rounded px-2 py-1 text-[10px] outline-none"
style={inputStyle}
>
<option value="true">Active</option>
<option value="false">Paused</option>
</select>
options={[
{ value: 'true', label: 'Active' },
{ value: 'false', label: 'Paused' },
]}
/>
</div>
<div className="flex justify-end">
<Button
@ -617,8 +623,10 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
{!appliedIds.has(msg.id) && !cancelledIds.has(msg.id) ? (
<div className="flex" style={{ borderTop: '1px solid var(--border)' }}>
<button
<Button
type="button"
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]"
style={{
color: 'var(--destructive)',
@ -627,9 +635,11 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
>
<X size={11} />
Cancel
</button>
<button
</Button>
<Button
type="button"
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]"
style={{
color: '#fbbf24',
@ -638,9 +648,11 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
>
<Copy size={11} />
{isEditing ? 'Done Editing' : 'Edit Params'}
</button>
<button
</Button>
<Button
type="button"
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"
style={{
background: 'var(--accent-soft)',
@ -649,7 +661,7 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
>
<Zap size={11} />
Apply to Dashboard
</button>
</Button>
</div>
) : cancelledIds.has(msg.id) ? (
<div className="w-full py-2 flex items-center justify-center gap-1.5 text-[10px] font-semibold" style={{
@ -686,9 +698,11 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
<p style={{ fontSize: '9px', color: 'var(--muted-foreground)', textTransform: 'uppercase', letterSpacing: '0.1em', fontWeight: 700, marginBottom: '10px', paddingLeft: '4px' }}>Quick Actions</p>
<div className="grid grid-cols-2 gap-2">
{quickActions.map((action, i) => (
<button
<Button
type="button"
key={i}
onClick={() => sendMessage(action.prompt)}
variant="ghost"
className="text-left px-3.5 py-3 rounded-xl transition-all"
style={{
background: 'var(--card)',
@ -706,7 +720,7 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
>
<span style={{ fontSize: '13px', display: 'block', marginBottom: '3px', color: 'var(--foreground)' }}>{action.label}</span>
<span style={{ fontSize: '10px', color: 'var(--muted-foreground)', lineHeight: '1.4', display: 'block' }}>{action.prompt.slice(0, 55)}...</span>
</button>
</Button>
))}
</div>
</div>
@ -738,7 +752,7 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
}}>
<div className="flex items-end gap-2.5">
<div className="flex-1 relative">
<textarea
<Textarea
ref={inputRef}
value={input}
onChange={e => setInput(e.target.value)}
@ -749,9 +763,11 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
className="w-full rounded-xl py-3 pl-4 pr-12 outline-none disabled:opacity-50 transition-all resize-none"
style={{ ...inputStyle, lineHeight: '1.5', fontFamily: 'inherit', fontSize: '13px' }}
/>
<button
<Button
type="button"
onClick={() => sendMessage()}
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"
style={{
background: input.trim() ? 'var(--primary)' : 'var(--accent-soft)',
@ -760,7 +776,7 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
}}
>
<Send size={14} />
</button>
</Button>
</div>
</div>
<p className="mt-1.5 ml-1 text-[9px] text-[var(--muted-foreground)]">Enter to send · Shift+Enter new line</p>

View File

@ -7,9 +7,7 @@ import { tradingRuntime } from '../lib/runtime';
import { createRequestId } from '../../../shared/request-id.js';
import { SkeletonBlock } from '../components/Skeleton';
import { PageHeader } from '../components/ui/page-header';
import { Button } from '../components/ui/button';
import { Input } from '../components/ui/input';
import { Select } from '../components/ui/select';
import { Button, Input, Select } from '../components/ui/Primitives';
import { Card, CardContent } from '../components/ui/card';
// ─── Types ────────────────────────────────────────────────────────────────────
@ -198,22 +196,20 @@ export function ScreenerView() {
value={String(capIdx)}
onChange={e => setCapIdx(Number(e.target.value))}
style={{ width: 180 }}
>
{CAP_OPTIONS.map((c, i) => (
<option key={c.label} value={i}>{c.label}</option>
))}
</Select>
options={CAP_OPTIONS.map((c, i) => ({ value: String(i), label: c.label }))}
/>
<div style={{ display: 'flex', gap: 5, flexWrap: 'wrap', alignItems: 'center' }}>
<SlidersHorizontal size={13} color="var(--muted-foreground)" />
{SECTORS.slice(0, 6).map(s => (
<button
<Button
key={s}
onClick={() => setSector(s)}
variant={sector === s ? 'secondary' : 'outline'}
size="sm"
style={{
padding: '5px 10px', borderRadius: 20,
border: '1px solid', fontSize: 11, fontWeight: 600,
cursor: 'pointer',
borderColor: sector === s ? 'var(--primary)' : 'var(--border)',
background: sector === s ? 'var(--accent-soft)' : 'var(--card)',
color: sector === s ? 'var(--primary)' : 'var(--muted-foreground)',
@ -221,12 +217,16 @@ export function ScreenerView() {
}}
>
{s}
</button>
</Button>
))}
<Select
aria-label="More sectors"
value={SECTORS.indexOf(sector) >= 6 ? sector : ''}
onChange={e => e.target.value && setSector(e.target.value)}
options={[
{ value: '', label: 'More sectors…' },
...SECTORS.slice(6).map(s => ({ value: s, label: s })),
]}
style={{
width: 140,
borderRadius: 999,
@ -235,12 +235,7 @@ export function ScreenerView() {
color: moreSectorSelected ? 'var(--primary)' : 'var(--muted-foreground)',
fontWeight: moreSectorSelected ? 700 : 500,
}}
>
<option value="">More sectors</option>
{SECTORS.slice(6).map(s => (
<option key={s} value={s}>{s}</option>
))}
</Select>
/>
</div>
</div>
</CardContent>

View File

@ -12,11 +12,9 @@ import {
type ManualEntryPayload,
} from '../lib/manualEntriesApi';
import { fetchTradeProfiles, type TradeProfilePayload } from '../lib/profileApi';
import { Button } from '../components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../components/ui/card';
import { Input } from '../components/ui/input';
import { PageHeader } from '../components/ui/page-header';
import { Select } from '../components/ui/select';
import { Button, Input, Select } from '../components/ui/Primitives';
import {
DEFAULT_TRADE_PLANS_UI_STATE,
reduceTradePlansUiState,
@ -1000,13 +998,14 @@ export function SimpleView() {
<CardContent>
<form className="space-y-6" onSubmit={handleSubmit}>
<div className="grid gap-3 md:grid-cols-2">
<button
<Button
type="button"
onClick={() => {
dispatch({ type: 'clear-feedback' });
dispatch({ type: 'set-selected-holding-trade-id', value: null });
updateDraft('side', 'buy');
}}
variant="ghost"
className={`rounded-[1.25rem] border px-4 py-4 text-left transition ${
draft.side === 'buy'
? '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="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>
</button>
<button
</Button>
<Button
type="button"
onClick={() => {
dispatch({ type: 'clear-feedback' });
@ -1027,6 +1026,7 @@ export function SimpleView() {
updateDraft('side', 'sell');
}
}}
variant="ghost"
className={`rounded-[1.25rem] border px-4 py-4 text-left transition ${
draft.side === 'sell'
? '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="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>
</button>
</Button>
</div>
{draft.side === 'sell' && (
@ -1049,17 +1049,13 @@ export function SimpleView() {
if (selected) applyHoldingToDraft(selected);
}}
disabled={availableSellHoldings.length === 0}
>
{availableSellHoldings.length === 0 ? (
<option value="">No eligible holdings available</option>
) : (
availableSellHoldings.map((holding) => (
<option key={`${holding.symbol}:${holding.tradeId || 'holding'}`} value={holding.tradeId || ''}>
{holding.symbol} · {holding.size} @ {holding.entryPrice.toFixed(4)}
</option>
))
)}
</Select>
options={availableSellHoldings.length === 0
? [{ value: '', label: 'No eligible holdings available' }]
: availableSellHoldings.map((holding) => ({
value: holding.tradeId || '',
label: `${holding.symbol} · ${holding.size} @ ${holding.entryPrice.toFixed(4)}`,
}))}
/>
<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.
</span>
@ -1104,7 +1100,7 @@ export function SimpleView() {
{filteredSymbolSuggestions.length > 0 ? (
<div className="flex flex-wrap gap-2 pt-1">
{filteredSymbolSuggestions.map((symbol) => (
<button
<Button
key={symbol}
type="button"
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 ${
symbol === normalizedSymbol
? 'border-[var(--primary)] bg-[var(--accent-soft)] text-[var(--primary)]'
@ -1128,7 +1126,7 @@ export function SimpleView() {
}`}
>
{symbol}
</button>
</Button>
))}
</div>
) : null}
@ -1139,10 +1137,11 @@ export function SimpleView() {
<Select
value={draft.side}
onChange={(e) => updateDraft('side', e.target.value as SimpleSide)}
>
<option value="buy">Buy the dip + profit exit</option>
<option value="sell">Manage existing holding at profit</option>
</Select>
options={[
{ value: 'buy', label: 'Buy the dip + profit exit' },
{ value: 'sell', label: 'Manage existing holding at profit' },
]}
/>
</label>
</div>
@ -1194,10 +1193,11 @@ export function SimpleView() {
<Select
value={draft.sizingMode}
onChange={(e) => updateDraft('sizingMode', e.target.value as 'quantity' | 'amount')}
>
<option value="quantity">Quantity / fractional shares</option>
<option value="amount">USD amount</option>
</Select>
options={[
{ value: 'quantity', label: 'Quantity / fractional shares' },
{ value: 'amount', label: 'USD amount' },
]}
/>
</label>
<label className="space-y-2">
@ -1255,10 +1255,11 @@ export function SimpleView() {
<Select
value={draft.dropMode}
onChange={(e) => updateDraft('dropMode', e.target.value as TriggerMode)}
>
<option value="dollar">Dollar drop from current market</option>
<option value="percent">Percent drop from current market</option>
</Select>
options={[
{ value: 'dollar', label: 'Dollar drop from current market' },
{ value: 'percent', label: 'Percent drop from current market' },
]}
/>
</div>
<label className="space-y-3">
<span className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">
@ -1279,10 +1280,11 @@ export function SimpleView() {
<Select
value={draft.profitMode}
onChange={(e) => updateDraft('profitMode', e.target.value as TriggerMode)}
>
<option value="dollar">Dollar gain from purchase</option>
<option value="percent">Percent gain from purchase</option>
</Select>
options={[
{ value: 'dollar', label: 'Dollar gain from purchase' },
{ value: 'percent', label: 'Percent gain from purchase' },
]}
/>
</div>
<label className="space-y-3">
<span className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">