From 0bb5d9a5d66ae5dde5a611a4e89ffbb40208e831 Mon Sep 17 00:00:00 2001 From: shokollm <270575765+shokollm@users.noreply.github.com> Date: Wed, 8 Apr 2026 13:41:43 +0000 Subject: [PATCH] feat: Implement frontend UI components for issue #10 Created the following components: - ChatInterface: Message input, AI responses, chat history with bot selector dropdown - BotCard: Bot preview card for dashboard - BotSelector: Dropdown to select bot (max 3 bots) - StrategyPreview: Shows parsed strategy config in readable format - SignalChart: Visual representation of signals over time (SVG-based) - BacktestChart: Portfolio value chart with metrics display - ProUpgradeBanner: Upsell banner for Pro features - TokenPicker: Search/select tokens for conditions - ConditionBuilder: UI for building trading conditions Updated pages to use new components: - Dashboard now uses BotCard - Bot detail page now uses ChatInterface and StrategyPreview - Backtest page now uses BacktestChart - Simulate page now uses SignalChart and ProUpgradeBanner --- .../src/lib/components/BacktestChart.svelte | 313 ++++++++++++++++ .../src/lib/components/BotCard.svelte | 127 +++++++ .../src/lib/components/BotSelector.svelte | 94 +++++ .../src/lib/components/ChatInterface.svelte | 300 +++++++++++++++ .../lib/components/ConditionBuilder.svelte | 348 ++++++++++++++++++ .../lib/components/ProUpgradeBanner.svelte | 121 ++++++ .../src/lib/components/SignalChart.svelte | 228 ++++++++++++ .../src/lib/components/StrategyPreview.svelte | 227 ++++++++++++ .../src/lib/components/TokenPicker.svelte | 256 +++++++++++++ src/frontend/src/lib/components/index.ts | 9 + src/frontend/src/routes/bot/[id]/+page.svelte | 229 ++---------- .../src/routes/bot/[id]/backtest/+page.svelte | 51 +++ .../src/routes/bot/[id]/simulate/+page.svelte | 5 + .../src/routes/dashboard/+page.svelte | 74 +--- 14 files changed, 2114 insertions(+), 268 deletions(-) create mode 100644 src/frontend/src/lib/components/BacktestChart.svelte create mode 100644 src/frontend/src/lib/components/BotCard.svelte create mode 100644 src/frontend/src/lib/components/BotSelector.svelte create mode 100644 src/frontend/src/lib/components/ChatInterface.svelte create mode 100644 src/frontend/src/lib/components/ConditionBuilder.svelte create mode 100644 src/frontend/src/lib/components/ProUpgradeBanner.svelte create mode 100644 src/frontend/src/lib/components/SignalChart.svelte create mode 100644 src/frontend/src/lib/components/StrategyPreview.svelte create mode 100644 src/frontend/src/lib/components/TokenPicker.svelte create mode 100644 src/frontend/src/lib/components/index.ts diff --git a/src/frontend/src/lib/components/BacktestChart.svelte b/src/frontend/src/lib/components/BacktestChart.svelte new file mode 100644 index 0000000..9d95ba9 --- /dev/null +++ b/src/frontend/src/lib/components/BacktestChart.svelte @@ -0,0 +1,313 @@ + + +
+ {#if !results} +
+

No backtest results to display

+
+ {:else} +
+
+ Total Return + + {results.total_return >= 0 ? '+' : ''}{results.total_return.toFixed(2)}% + +
+
+ Win Rate + {results.win_rate.toFixed(1)}% +
+
+ Total Trades + {results.total_trades} +
+
+ Sharpe Ratio + {results.sharpe_ratio.toFixed(2)} +
+
+ + + + + + + + + + + {#each [0, 1, 2, 3, 4] as i} + {@const y = area.y + (i / 4) * area.height} + + {/each} + + + + {#each yAxisLabels as label} + + ${label.value.toLocaleString()} + + {/each} + + + + {#each xAxisLabels as label} + + {label.label} + + {/each} + + + {#if points.length > 1} + { + const pos = getPointPosition(p, i, points.length, area, range); + if (i === 0) { + return `M ${pos.x} ${area.y + area.height} L ${pos.x} ${pos.y}`; + } + return `L ${pos.x} ${pos.y}`; + }).join(' ') + ` L ${getPointPosition(points[points.length - 1], points.length - 1, points.length, area, range).x} ${area.y + area.height} Z`} + fill="url(#portfolioGradient)" + /> + + { + const pos = getPointPosition(p, i, points.length, area, range); + return `${i === 0 ? 'M' : 'L'} ${pos.x} ${pos.y}`; + }).join(' ')} + fill="none" + stroke="#667eea" + stroke-width="2.5" + /> + {/if} + + + + {/if} +
+ + \ No newline at end of file diff --git a/src/frontend/src/lib/components/BotCard.svelte b/src/frontend/src/lib/components/BotCard.svelte new file mode 100644 index 0000000..69c7147 --- /dev/null +++ b/src/frontend/src/lib/components/BotCard.svelte @@ -0,0 +1,127 @@ + + +
e.key === 'Enter' && handleOpen()}> +
+

{bot.name}

+ {#if bot.description} +

{bot.description}

+ {/if} + {bot.status} +
+ {#if showActions} +
e.stopPropagation()} role="group"> + + +
+ {/if} +
+ + \ No newline at end of file diff --git a/src/frontend/src/lib/components/BotSelector.svelte b/src/frontend/src/lib/components/BotSelector.svelte new file mode 100644 index 0000000..4b3eed5 --- /dev/null +++ b/src/frontend/src/lib/components/BotSelector.svelte @@ -0,0 +1,94 @@ + + +
+ {#if label} + + {/if} +
+ + {bots.length}/{MAX_BOTS} +
+
+ + \ No newline at end of file diff --git a/src/frontend/src/lib/components/ChatInterface.svelte b/src/frontend/src/lib/components/ChatInterface.svelte new file mode 100644 index 0000000..ed83b7a --- /dev/null +++ b/src/frontend/src/lib/components/ChatInterface.svelte @@ -0,0 +1,300 @@ + + +
+ {#if showBotSelector && availableBots.length > 0} +
+ + +
+ {/if} + +
+ {#if messages.length === 0} +
+

Welcome to {bot?.name || 'your bot'}! Describe your trading strategy in plain English.

+

Example: "Buy PEPE when the price drops by 5% within 1 hour"

+
+ {/if} + + {#each messages as message} +
+
+ {message.content} +
+
+ {message.timestamp.toLocaleTimeString()} +
+
+ {/each} + + {#if isSending} +
+
+ + + +
+
+ {/if} +
+ + {#if bot} +
+ + +
+ {/if} +
+ + \ No newline at end of file diff --git a/src/frontend/src/lib/components/ConditionBuilder.svelte b/src/frontend/src/lib/components/ConditionBuilder.svelte new file mode 100644 index 0000000..06c914b --- /dev/null +++ b/src/frontend/src/lib/components/ConditionBuilder.svelte @@ -0,0 +1,348 @@ + + +
+
+

Conditions

+ +
+ + {#if conditions.length === 0} +
+

No conditions set

+

Add a condition to define when your strategy triggers

+
+ {:else} +
+ {#each conditions as condition, index} +
+
+ #{index + 1} + +
+ +
+
+ + +
+ + updateCondition(index, { token, chain })} + disabled={disabled} + /> + + {#if condition.type === 'price_level'} +
+ + +
+
+ + updateCondition(index, { price: parseFloat((e.target as HTMLInputElement).value) || undefined })} + placeholder="0.000001" + step="any" + min="0" + disabled={disabled} + /> +
+ {:else} +
+ + updateCondition(index, { threshold: parseFloat((e.target as HTMLInputElement).value) || undefined })} + placeholder="5" + step="any" + min="0" + disabled={disabled} + /> +
+
+ + +
+ {/if} +
+ +
+ Summary: + {getConditionDescription(condition)} +
+
+ {/each} +
+ {/if} +
+ + \ No newline at end of file diff --git a/src/frontend/src/lib/components/ProUpgradeBanner.svelte b/src/frontend/src/lib/components/ProUpgradeBanner.svelte new file mode 100644 index 0000000..ca2516f --- /dev/null +++ b/src/frontend/src/lib/components/ProUpgradeBanner.svelte @@ -0,0 +1,121 @@ + + +
+ + {#if dismissible && onDismiss} + + {/if} +
+ + \ No newline at end of file diff --git a/src/frontend/src/lib/components/SignalChart.svelte b/src/frontend/src/lib/components/SignalChart.svelte new file mode 100644 index 0000000..0557a04 --- /dev/null +++ b/src/frontend/src/lib/components/SignalChart.svelte @@ -0,0 +1,228 @@ + + +
+ {#if signals.length === 0} +
+

No signals to display

+
+ {:else} + + + + + + + + + + {#each [0, 1, 2, 3] as i} + {@const y = 30 + (i / 3) * (height - 60)} + + {/each} + + + + {#each getYAxisLabels() as label, i} + {@const y = 30 + (i / 3) * (height - 60)} + ${label} + {/each} + + + + {#each getXAxisLabels() as label, i} + {@const x = 30 + (i / (getXAxisLabels().length - 1 || 1)) * (width - 60)} + {label} + {/each} + + + { + const pos = getSignalPosition(s, i, signals.length); + return `${i === 0 ? 'M' : 'L'} ${pos.x} ${pos.y}`; + }).join(' ')} + fill="none" + stroke="#667eea" + stroke-width="2" + /> + + {#each signals as signal, i} + {@const pos = getSignalPosition(signal, i, signals.length)} + {@const color = getSignalColor(signal)} + + {signal.signal_type.toUpperCase()} - ${signal.price.toFixed(6)} - {new Date(signal.created_at).toLocaleString()} + + {/each} + + +
+
+ + Buy +
+
+ + Sell +
+
+ + Hold +
+
+ {/if} +
+ + \ No newline at end of file diff --git a/src/frontend/src/lib/components/StrategyPreview.svelte b/src/frontend/src/lib/components/StrategyPreview.svelte new file mode 100644 index 0000000..5988427 --- /dev/null +++ b/src/frontend/src/lib/components/StrategyPreview.svelte @@ -0,0 +1,227 @@ + + +
+ {#if !config || (config.conditions.length === 0 && config.actions.length === 0)} +
+

No strategy configured yet.

+

Describe your trading strategy in the chat to create one.

+
+ {:else} +
+

Conditions

+ {#if config.conditions.length === 0} +

No conditions set

+ {:else} + + {/if} +
+ +
+

Actions

+ {#if config.actions.length === 0} +

No actions set

+ {:else} + + {/if} +
+ + {#if config.risk_management} +
+

Risk Management

+
+ {#if config.risk_management.stop_loss_percent} +
+ Stop Loss + {config.risk_management.stop_loss_percent}% +
+ {/if} + {#if config.risk_management.take_profit_percent} +
+ Take Profit + {config.risk_management.take_profit_percent}% +
+ {/if} +
+
+ {/if} + {/if} +
+ + \ No newline at end of file diff --git a/src/frontend/src/lib/components/TokenPicker.svelte b/src/frontend/src/lib/components/TokenPicker.svelte new file mode 100644 index 0000000..4248843 --- /dev/null +++ b/src/frontend/src/lib/components/TokenPicker.svelte @@ -0,0 +1,256 @@ + + +
+ {#if label} + + {/if} + +
+ + {#if selectedToken} + + {/if} +
+ + {#if isOpen} + + {/if} +
+ + \ No newline at end of file diff --git a/src/frontend/src/lib/components/index.ts b/src/frontend/src/lib/components/index.ts new file mode 100644 index 0000000..ba33297 --- /dev/null +++ b/src/frontend/src/lib/components/index.ts @@ -0,0 +1,9 @@ +export { default as ChatInterface } from './ChatInterface.svelte'; +export { default as BotCard } from './BotCard.svelte'; +export { default as BotSelector } from './BotSelector.svelte'; +export { default as StrategyPreview } from './StrategyPreview.svelte'; +export { default as SignalChart } from './SignalChart.svelte'; +export { default as BacktestChart } from './BacktestChart.svelte'; +export { default as ProUpgradeBanner } from './ProUpgradeBanner.svelte'; +export { default as TokenPicker } from './TokenPicker.svelte'; +export { default as ConditionBuilder } from './ConditionBuilder.svelte'; \ No newline at end of file diff --git a/src/frontend/src/routes/bot/[id]/+page.svelte b/src/frontend/src/routes/bot/[id]/+page.svelte index ac12178..da8efb7 100644 --- a/src/frontend/src/routes/bot/[id]/+page.svelte +++ b/src/frontend/src/routes/bot/[id]/+page.svelte @@ -4,11 +4,11 @@ import { goto } from '$app/navigation'; import { isAuthenticated, isLoading, chatStore, addMessage, setMessages, clearChat, currentBotStore, setCurrentBot } from '$lib/stores'; import { api } from '$lib/api'; + import { ChatInterface, StrategyPreview, ProUpgradeBanner } from '$lib/components'; let botId = $derived($page.params.id); - let messageInput = $state(''); let isSending = $state(false); - let chatContainer: HTMLDivElement; + let showStrategy = $state(false); onMount(async () => { if (!$isAuthenticated && !$isLoading) { @@ -34,24 +34,18 @@ try { const history = await api.bots.getHistory(botId); setMessages(history); - scrollToBottom(); } catch (e) { console.error('Failed to load chat history:', e); } } - async function sendMessage() { - if (!messageInput.trim() || isSending) return; + async function handleSendMessage(message: string) { + if (isSending) return; - const userMessage = messageInput; - messageInput = ''; isSending = true; - addMessage({ role: 'user', content: userMessage }); - scrollToBottom(); - try { - const response = await api.bots.chat(botId, userMessage); + const response = await api.bots.chat(botId, message); addMessage({ role: 'assistant', content: response.response }); if (response.strategy_config) { @@ -62,23 +56,11 @@ addMessage({ role: 'assistant', content: 'Sorry, I encountered an error. Please try again.' }); } finally { isSending = false; - scrollToBottom(); } } - function scrollToBottom() { - setTimeout(() => { - if (chatContainer) { - chatContainer.scrollTop = chatContainer.scrollHeight; - } - }, 50); - } - - function handleKeydown(e: KeyboardEvent) { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - sendMessage(); - } + function toggleStrategy() { + showStrategy = !showStrategy; } @@ -93,55 +75,32 @@

{$currentBotStore?.name || 'Loading...'}

+ {#if $currentBotStore?.strategy_config} + + {/if} Backtest Simulate
-
- {#if $chatStore.length === 0} -
-

Welcome to {$currentBotStore?.name}! Describe your trading strategy in plain English.

-

Example: "Buy PEPE when the price drops by 5% within 1 hour"

-
- {/if} - - {#each $chatStore as message} -
-
- {message.content} -
-
- {message.timestamp.toLocaleTimeString()} -
-
- {/each} - - {#if isSending} -
-
- - - -
-
- {/if} -
- - {#if $currentBotStore} -
- - + {#if showStrategy && $currentBotStore?.strategy_config} +
+
{/if} + +
+ +
+ + + \ No newline at end of file diff --git a/src/frontend/src/routes/bot/[id]/backtest/+page.svelte b/src/frontend/src/routes/bot/[id]/backtest/+page.svelte index a498047..3fc8ae2 100644 --- a/src/frontend/src/routes/bot/[id]/backtest/+page.svelte +++ b/src/frontend/src/routes/bot/[id]/backtest/+page.svelte @@ -4,6 +4,8 @@ import { goto } from '$app/navigation'; import { isAuthenticated, isLoading, currentBotStore, setCurrentBot, backtestStore, setCurrentBacktest, addBacktestToHistory, setBacktestLoading, setBacktestError } from '$lib/stores'; import { api } from '$lib/api'; + import { BacktestChart } from '$lib/components'; + import type { Backtest } from '$lib/api'; let botId = $derived($page.params.id); let token = $state('PEPE'); @@ -11,6 +13,7 @@ let startDate = $state(''); let endDate = $state(''); let isRunning = $state(false); + let selectedBacktest = $state(null); onMount(async () => { if (!$isAuthenticated && !$isLoading) { @@ -76,6 +79,12 @@ function setBacktestHistory(backtests: any[]) { backtestStore.update(state => ({ ...state, backtestHistory: backtests })); } + + function selectBacktest(backtest: Backtest) { + if (backtest.status === 'completed' && backtest.result) { + selectedBacktest = backtest; + } + } @@ -177,6 +186,16 @@
{/if} + + {#if selectedBacktest} +
+
+

Portfolio Performance

+ +
+ +
+ {/if} @@ -388,4 +407,36 @@ color: #fca5a5; border: 1px solid rgba(239, 68, 68, 0.4); } + + .chart-section { + padding: 1.5rem; + } + + .chart-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + } + + .chart-header h2 { + margin: 0; + } + + .close-btn { + width: auto; + padding: 0.25rem 0.75rem; + background: rgba(255, 255, 255, 0.1); + border: none; + color: #888; + font-size: 1.5rem; + line-height: 1; + cursor: pointer; + border-radius: 4px; + } + + .close-btn:hover { + background: rgba(255, 255, 255, 0.2); + color: #fff; + } diff --git a/src/frontend/src/routes/bot/[id]/simulate/+page.svelte b/src/frontend/src/routes/bot/[id]/simulate/+page.svelte index cc8df84..5e4cc6f 100644 --- a/src/frontend/src/routes/bot/[id]/simulate/+page.svelte +++ b/src/frontend/src/routes/bot/[id]/simulate/+page.svelte @@ -4,6 +4,7 @@ import { goto } from '$app/navigation'; import { isAuthenticated, isLoading, currentBotStore, setCurrentBot, simulationStore, setCurrentSimulation, addSignals, clearSignals, setSimulationLoading, setSimulationError } from '$lib/stores'; import { api } from '$lib/api'; + import { SignalChart, ProUpgradeBanner } from '$lib/components'; let botId = $derived($page.params.id); let token = $state('PEPE'); @@ -142,12 +143,16 @@ + +

Signals ({$simulationStore.signals.length})

{#if $simulationStore.signals.length === 0}

No signals yet. Start a simulation to see trading signals.

{:else} + +
{#each $simulationStore.signals as signal}
diff --git a/src/frontend/src/routes/dashboard/+page.svelte b/src/frontend/src/routes/dashboard/+page.svelte index 767e957..8cad6b4 100644 --- a/src/frontend/src/routes/dashboard/+page.svelte +++ b/src/frontend/src/routes/dashboard/+page.svelte @@ -3,6 +3,7 @@ import { goto } from '$app/navigation'; import { isAuthenticated, isLoading, botsStore, setBots, addBot, removeBot, userStore, logout } from '$lib/stores'; import { api } from '$lib/api'; + import { BotCard } from '$lib/components'; let showCreateModal = $state(false); let newBotName = $state(''); @@ -30,7 +31,7 @@ } async function createBot() { - if (!$newBotName.trim()) return; + if (!newBotName.trim()) return; createError = ''; isCreating = true; try { @@ -95,19 +96,7 @@ {:else}
{#each $botsStore as bot} -
-
-

{bot.name}

- {#if bot.description} -

{bot.description}

- {/if} - {bot.status} -
-
- Open - -
-
+ goto(`/bot/${id}`)} onDelete={deleteBot} /> {/each}
{/if} @@ -201,57 +190,6 @@ gap: 1.5rem; } - .bot-card { - background: rgba(255, 255, 255, 0.05); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 12px; - padding: 1.5rem; - } - - .bot-info { - margin-bottom: 1rem; - } - - .bot-info h3 { - margin: 0 0 0.5rem; - font-size: 1.25rem; - } - - .bot-description { - color: #888; - font-size: 0.9rem; - margin: 0 0 0.75rem; - } - - .bot-status { - display: inline-block; - padding: 0.25rem 0.75rem; - border-radius: 9999px; - font-size: 0.75rem; - font-weight: 500; - text-transform: uppercase; - } - - .status-draft { - background: rgba(251, 191, 36, 0.2); - color: #fbbf24; - } - - .status-active { - background: rgba(34, 197, 94, 0.2); - color: #22c55e; - } - - .status-paused { - background: rgba(251, 191, 36, 0.2); - color: #fbbf24; - } - - .bot-actions { - display: flex; - gap: 0.75rem; - } - .btn { padding: 0.5rem 1rem; border-radius: 8px; @@ -274,12 +212,6 @@ border: 1px solid rgba(255, 255, 255, 0.2); } - .btn-danger { - background: rgba(239, 68, 68, 0.2); - color: #fca5a5; - border: 1px solid rgba(239, 68, 68, 0.4); - } - .btn:hover { transform: translateY(-2px); } -- 2.49.1