Compare commits
2 Commits
fix/issue-
...
fix/issue-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5eb623f022 | ||
| 0cc3327991 |
2
src/frontend/.env.example
Normal file
2
src/frontend/.env.example
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
VITE_API_URL=http://localhost:8000/api
|
||||||
|
VITE_WS_URL=ws://localhost:8000/ws
|
||||||
23
src/frontend/.gitignore
vendored
Normal file
23
src/frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
node_modules
|
||||||
|
|
||||||
|
# Output
|
||||||
|
.output
|
||||||
|
.vercel
|
||||||
|
.netlify
|
||||||
|
.wrangler
|
||||||
|
/.svelte-kit
|
||||||
|
/build
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Env
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
!.env.test
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
1
src/frontend/.npmrc
Normal file
1
src/frontend/.npmrc
Normal file
@@ -0,0 +1 @@
|
|||||||
|
engine-strict=true
|
||||||
3
src/frontend/.vscode/extensions.json
vendored
Normal file
3
src/frontend/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["svelte.svelte-vscode"]
|
||||||
|
}
|
||||||
42
src/frontend/README.md
Normal file
42
src/frontend/README.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# sv
|
||||||
|
|
||||||
|
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
||||||
|
|
||||||
|
## Creating a project
|
||||||
|
|
||||||
|
If you're seeing this, you've probably already done this step. Congrats!
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# create a new project
|
||||||
|
npx sv create my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
To recreate this project with the same configuration:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# recreate this project
|
||||||
|
npx sv@0.15.0 create --template minimal --types ts --no-install .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# or start the server and open the app in a new browser tab
|
||||||
|
npm run dev -- --open
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
To create a production version of your app:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
You can preview the production build with `npm run preview`.
|
||||||
|
|
||||||
|
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
||||||
1383
src/frontend/package-lock.json
generated
Normal file
1383
src/frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
src/frontend/package.json
Normal file
23
src/frontend/package.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite dev",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"prepare": "svelte-kit sync || echo ''",
|
||||||
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sveltejs/adapter-auto": "^7.0.1",
|
||||||
|
"@sveltejs/kit": "^2.57.0",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
||||||
|
"svelte": "^5.55.2",
|
||||||
|
"svelte-check": "^4.4.6",
|
||||||
|
"typescript": "^6.0.2",
|
||||||
|
"vite": "^8.0.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/frontend/src/app.d.ts
vendored
Normal file
13
src/frontend/src/app.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||||
|
// for information about these interfaces
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
// interface Locals {}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface PageState {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
12
src/frontend/src/app.html
Normal file
12
src/frontend/src/app.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="text-scale" content="scale" />
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
209
src/frontend/src/lib/api/client.ts
Normal file
209
src/frontend/src/lib/api/client.ts
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
import type {
|
||||||
|
User,
|
||||||
|
Bot,
|
||||||
|
BotConversation,
|
||||||
|
Backtest,
|
||||||
|
Simulation,
|
||||||
|
Signal,
|
||||||
|
AuthResponse,
|
||||||
|
BotChatRequest,
|
||||||
|
BotChatResponse,
|
||||||
|
StrategyConfig
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000/api';
|
||||||
|
|
||||||
|
function getAuthHeaders(): HeadersInit {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
return token ? { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' } : { 'Content-Type': 'application/json' };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleResponse<T>(response: Response): Promise<T> {
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({ detail: 'An error occurred' }));
|
||||||
|
throw new Error(error.detail || `HTTP error ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const api = {
|
||||||
|
auth: {
|
||||||
|
async register(email: string, password: string): Promise<AuthResponse> {
|
||||||
|
const response = await fetch(`${API_URL}/auth/register`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email, password })
|
||||||
|
});
|
||||||
|
return handleResponse<AuthResponse>(response);
|
||||||
|
},
|
||||||
|
|
||||||
|
async login(email: string, password: string): Promise<AuthResponse> {
|
||||||
|
const response = await fetch(`${API_URL}/auth/login`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email, password })
|
||||||
|
});
|
||||||
|
return handleResponse<AuthResponse>(response);
|
||||||
|
},
|
||||||
|
|
||||||
|
async logout(): Promise<void> {
|
||||||
|
await fetch(`${API_URL}/auth/logout`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getAuthHeaders()
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async me(): Promise<User> {
|
||||||
|
const response = await fetch(`${API_URL}/auth/me`, {
|
||||||
|
headers: getAuthHeaders()
|
||||||
|
});
|
||||||
|
return handleResponse<User>(response);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
bots: {
|
||||||
|
async list(): Promise<Bot[]> {
|
||||||
|
const response = await fetch(`${API_URL}/bots`, {
|
||||||
|
headers: getAuthHeaders()
|
||||||
|
});
|
||||||
|
return handleResponse<Bot[]>(response);
|
||||||
|
},
|
||||||
|
|
||||||
|
async create(name: string, description?: string): Promise<Bot> {
|
||||||
|
const response = await fetch(`${API_URL}/bots`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
body: JSON.stringify({ name, description })
|
||||||
|
});
|
||||||
|
return handleResponse<Bot>(response);
|
||||||
|
},
|
||||||
|
|
||||||
|
async get(id: string): Promise<Bot> {
|
||||||
|
const response = await fetch(`${API_URL}/bots/${id}`, {
|
||||||
|
headers: getAuthHeaders()
|
||||||
|
});
|
||||||
|
return handleResponse<Bot>(response);
|
||||||
|
},
|
||||||
|
|
||||||
|
async update(id: string, data: Partial<Bot>): Promise<Bot> {
|
||||||
|
const response = await fetch(`${API_URL}/bots/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
return handleResponse<Bot>(response);
|
||||||
|
},
|
||||||
|
|
||||||
|
async delete(id: string): Promise<void> {
|
||||||
|
const response = await fetch(`${API_URL}/bots/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: getAuthHeaders()
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error ${response.status}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async chat(id: string, message: string): Promise<BotChatResponse> {
|
||||||
|
const response = await fetch(`${API_URL}/bots/${id}/chat`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
body: JSON.stringify({ message } as BotChatRequest)
|
||||||
|
});
|
||||||
|
return handleResponse<BotChatResponse>(response);
|
||||||
|
},
|
||||||
|
|
||||||
|
async getHistory(id: string): Promise<BotConversation[]> {
|
||||||
|
const response = await fetch(`${API_URL}/bots/${id}/history`, {
|
||||||
|
headers: getAuthHeaders()
|
||||||
|
});
|
||||||
|
return handleResponse<BotConversation[]>(response);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
backtest: {
|
||||||
|
async start(botId: string, config: { token: string; timeframe: string; start_date: string; end_date: string }): Promise<Backtest> {
|
||||||
|
const response = await fetch(`${API_URL}/bots/${botId}/backtest`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
body: JSON.stringify(config)
|
||||||
|
});
|
||||||
|
return handleResponse<Backtest>(response);
|
||||||
|
},
|
||||||
|
|
||||||
|
async get(botId: string, runId: string): Promise<Backtest> {
|
||||||
|
const response = await fetch(`${API_URL}/bots/${botId}/backtest/${runId}`, {
|
||||||
|
headers: getAuthHeaders()
|
||||||
|
});
|
||||||
|
return handleResponse<Backtest>(response);
|
||||||
|
},
|
||||||
|
|
||||||
|
async list(botId: string): Promise<Backtest[]> {
|
||||||
|
const response = await fetch(`${API_URL}/bots/${botId}/backtests`, {
|
||||||
|
headers: getAuthHeaders()
|
||||||
|
});
|
||||||
|
return handleResponse<Backtest[]>(response);
|
||||||
|
},
|
||||||
|
|
||||||
|
async stop(botId: string, runId: string): Promise<void> {
|
||||||
|
const response = await fetch(`${API_URL}/bots/${botId}/backtest/${runId}/stop`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getAuthHeaders()
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error ${response.status}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
simulate: {
|
||||||
|
async start(botId: string, config: { token: string; interval_seconds: number; auto_execute: boolean }): Promise<Simulation> {
|
||||||
|
const response = await fetch(`${API_URL}/bots/${botId}/simulate`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
body: JSON.stringify(config)
|
||||||
|
});
|
||||||
|
return handleResponse<Simulation>(response);
|
||||||
|
},
|
||||||
|
|
||||||
|
async get(botId: string, runId: string): Promise<Simulation> {
|
||||||
|
const response = await fetch(`${API_URL}/bots/${botId}/simulate/${runId}`, {
|
||||||
|
headers: getAuthHeaders()
|
||||||
|
});
|
||||||
|
return handleResponse<Simulation>(response);
|
||||||
|
},
|
||||||
|
|
||||||
|
async list(botId: string): Promise<Simulation[]> {
|
||||||
|
const response = await fetch(`${API_URL}/bots/${botId}/simulations`, {
|
||||||
|
headers: getAuthHeaders()
|
||||||
|
});
|
||||||
|
return handleResponse<Simulation[]>(response);
|
||||||
|
},
|
||||||
|
|
||||||
|
async stop(botId: string, runId: string): Promise<void> {
|
||||||
|
const response = await fetch(`${API_URL}/bots/${botId}/simulate/${runId}/stop`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getAuthHeaders()
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error ${response.status}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
config: {
|
||||||
|
async getChains(): Promise<string[]> {
|
||||||
|
const response = await fetch(`${API_URL}/config/chains`, {
|
||||||
|
headers: getAuthHeaders()
|
||||||
|
});
|
||||||
|
return handleResponse<string[]>(response);
|
||||||
|
},
|
||||||
|
|
||||||
|
async getTokens(): Promise<{ symbol: string; chain: string; name: string }[]> {
|
||||||
|
const response = await fetch(`${API_URL}/config/tokens`, {
|
||||||
|
headers: getAuthHeaders()
|
||||||
|
});
|
||||||
|
return handleResponse<{ symbol: string; chain: string; name: string }[]>(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
2
src/frontend/src/lib/api/index.ts
Normal file
2
src/frontend/src/lib/api/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { api } from './client';
|
||||||
|
export * from './types';
|
||||||
128
src/frontend/src/lib/api/types.ts
Normal file
128
src/frontend/src/lib/api/types.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Bot {
|
||||||
|
id: string;
|
||||||
|
user_id: string;
|
||||||
|
name: string;
|
||||||
|
description: string | null;
|
||||||
|
strategy_config: StrategyConfig;
|
||||||
|
llm_config: LLMConfig;
|
||||||
|
status: 'draft' | 'active' | 'paused';
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StrategyConfig {
|
||||||
|
conditions: Condition[];
|
||||||
|
actions: Action[];
|
||||||
|
risk_management?: RiskManagement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Condition {
|
||||||
|
type: 'price_drop' | 'price_rise' | 'volume_spike' | 'price_level';
|
||||||
|
token: string;
|
||||||
|
chain?: string;
|
||||||
|
threshold?: number;
|
||||||
|
price?: number;
|
||||||
|
direction?: 'above' | 'below';
|
||||||
|
timeframe?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Action {
|
||||||
|
type: 'buy' | 'sell' | 'hold';
|
||||||
|
amount_percent?: number;
|
||||||
|
token?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RiskManagement {
|
||||||
|
stop_loss_percent?: number;
|
||||||
|
take_profit_percent?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LLMConfig {
|
||||||
|
model: string;
|
||||||
|
temperature: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BotConversation {
|
||||||
|
id: string;
|
||||||
|
bot_id: string;
|
||||||
|
role: 'user' | 'assistant' | 'system';
|
||||||
|
content: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Backtest {
|
||||||
|
id: string;
|
||||||
|
bot_id: string;
|
||||||
|
started_at: string;
|
||||||
|
ended_at: string | null;
|
||||||
|
status: 'running' | 'completed' | 'failed';
|
||||||
|
config: BacktestConfig;
|
||||||
|
result: BacktestResult | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BacktestConfig {
|
||||||
|
token: string;
|
||||||
|
timeframe: string;
|
||||||
|
start_date: string;
|
||||||
|
end_date: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BacktestResult {
|
||||||
|
total_return: number;
|
||||||
|
win_rate: number;
|
||||||
|
total_trades: number;
|
||||||
|
buy_signals: number;
|
||||||
|
sell_signals: number;
|
||||||
|
max_drawdown: number;
|
||||||
|
sharpe_ratio: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Simulation {
|
||||||
|
id: string;
|
||||||
|
bot_id: string;
|
||||||
|
started_at: string;
|
||||||
|
status: 'running' | 'stopped';
|
||||||
|
config: SimulationConfig;
|
||||||
|
signals: Signal[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SimulationConfig {
|
||||||
|
token: string;
|
||||||
|
interval_seconds: number;
|
||||||
|
auto_execute: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Signal {
|
||||||
|
id: string;
|
||||||
|
bot_id: string;
|
||||||
|
run_id: string;
|
||||||
|
signal_type: 'buy' | 'sell' | 'hold';
|
||||||
|
token: string;
|
||||||
|
price: number;
|
||||||
|
confidence: number | null;
|
||||||
|
reasoning: string | null;
|
||||||
|
executed: boolean;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthResponse {
|
||||||
|
access_token: string;
|
||||||
|
token_type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BotChatRequest {
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BotChatResponse {
|
||||||
|
response: string;
|
||||||
|
strategy_config: StrategyConfig | null;
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
1
src/frontend/src/lib/assets/favicon.svg
Normal file
1
src/frontend/src/lib/assets/favicon.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
1
src/frontend/src/lib/index.ts
Normal file
1
src/frontend/src/lib/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
// place files you want to import through the `$lib` alias in this folder.
|
||||||
50
src/frontend/src/lib/stores/authStore.ts
Normal file
50
src/frontend/src/lib/stores/authStore.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { writable, get } from 'svelte/store';
|
||||||
|
import { api } from '$lib/api';
|
||||||
|
import { setUser, clearUser, clearBots } from './index';
|
||||||
|
import { clearSimulationState } from './simulationStore';
|
||||||
|
import { clearBacktestState } from './backtestStore';
|
||||||
|
|
||||||
|
export const isAuthenticated = writable(false);
|
||||||
|
export const isLoading = writable(true);
|
||||||
|
|
||||||
|
export async function initAuth() {
|
||||||
|
isLoading.set(true);
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (token) {
|
||||||
|
try {
|
||||||
|
const user = await api.auth.me();
|
||||||
|
setUser(user);
|
||||||
|
isAuthenticated.set(true);
|
||||||
|
} catch {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
isAuthenticated.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isLoading.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function login(email: string, password: string) {
|
||||||
|
const response = await api.auth.login(email, password);
|
||||||
|
localStorage.setItem('token', response.access_token);
|
||||||
|
const user = await api.auth.me();
|
||||||
|
setUser(user);
|
||||||
|
isAuthenticated.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function register(email: string, password: string) {
|
||||||
|
const response = await api.auth.register(email, password);
|
||||||
|
localStorage.setItem('token', response.access_token);
|
||||||
|
const user = await api.auth.me();
|
||||||
|
setUser(user);
|
||||||
|
isAuthenticated.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logout() {
|
||||||
|
api.auth.logout().catch(() => {});
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
clearUser();
|
||||||
|
clearBots();
|
||||||
|
clearBacktestState();
|
||||||
|
clearSimulationState();
|
||||||
|
isAuthenticated.set(false);
|
||||||
|
}
|
||||||
45
src/frontend/src/lib/stores/backtestStore.ts
Normal file
45
src/frontend/src/lib/stores/backtestStore.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import type { Backtest, BacktestResult } from '$lib/api';
|
||||||
|
|
||||||
|
export interface BacktestState {
|
||||||
|
currentBacktest: Backtest | null;
|
||||||
|
backtestHistory: Backtest[];
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: BacktestState = {
|
||||||
|
currentBacktest: null,
|
||||||
|
backtestHistory: [],
|
||||||
|
isLoading: false,
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
|
||||||
|
export const backtestStore = writable<BacktestState>(initialState);
|
||||||
|
|
||||||
|
export function setCurrentBacktest(backtest: Backtest | null) {
|
||||||
|
backtestStore.update(state => ({ ...state, currentBacktest: backtest }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addBacktestToHistory(backtest: Backtest) {
|
||||||
|
backtestStore.update(state => ({
|
||||||
|
...state,
|
||||||
|
backtestHistory: [backtest, ...state.backtestHistory]
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setBacktestHistory(backtests: Backtest[]) {
|
||||||
|
backtestStore.update(state => ({ ...state, backtestHistory: backtests }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setBacktestLoading(loading: boolean) {
|
||||||
|
backtestStore.update(state => ({ ...state, isLoading: loading }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setBacktestError(error: string | null) {
|
||||||
|
backtestStore.update(state => ({ ...state, error }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearBacktestState() {
|
||||||
|
backtestStore.set(initialState);
|
||||||
|
}
|
||||||
24
src/frontend/src/lib/stores/botsStore.ts
Normal file
24
src/frontend/src/lib/stores/botsStore.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import type { Bot } from '$lib/api';
|
||||||
|
|
||||||
|
export const botsStore = writable<Bot[]>([]);
|
||||||
|
|
||||||
|
export function setBots(bots: Bot[]) {
|
||||||
|
botsStore.set(bots);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addBot(bot: Bot) {
|
||||||
|
botsStore.update(bots => [...bots, bot]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateBot(bot: Bot) {
|
||||||
|
botsStore.update(bots => bots.map(b => b.id === bot.id ? bot : b));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeBot(botId: string) {
|
||||||
|
botsStore.update(bots => bots.filter(b => b.id !== botId));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearBots() {
|
||||||
|
botsStore.set([]);
|
||||||
|
}
|
||||||
33
src/frontend/src/lib/stores/chatStore.ts
Normal file
33
src/frontend/src/lib/stores/chatStore.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import type { BotConversation } from '$lib/api';
|
||||||
|
|
||||||
|
export interface ChatMessage {
|
||||||
|
id: string;
|
||||||
|
role: 'user' | 'assistant' | 'system';
|
||||||
|
content: string;
|
||||||
|
timestamp: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const chatStore = writable<ChatMessage[]>([]);
|
||||||
|
|
||||||
|
export function addMessage(message: Omit<ChatMessage, 'id' | 'timestamp'>) {
|
||||||
|
const newMessage: ChatMessage = {
|
||||||
|
...message,
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
timestamp: new Date()
|
||||||
|
};
|
||||||
|
chatStore.update(messages => [...messages, newMessage]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setMessages(messages: BotConversation[]) {
|
||||||
|
chatStore.set(messages.map(m => ({
|
||||||
|
id: m.id,
|
||||||
|
role: m.role,
|
||||||
|
content: m.content,
|
||||||
|
timestamp: new Date(m.created_at)
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearChat() {
|
||||||
|
chatStore.set([]);
|
||||||
|
}
|
||||||
12
src/frontend/src/lib/stores/currentBotStore.ts
Normal file
12
src/frontend/src/lib/stores/currentBotStore.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import type { Bot } from '$lib/api';
|
||||||
|
|
||||||
|
export const currentBotStore = writable<Bot | null>(null);
|
||||||
|
|
||||||
|
export function setCurrentBot(bot: Bot | null) {
|
||||||
|
currentBotStore.set(bot);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearCurrentBot() {
|
||||||
|
currentBotStore.set(null);
|
||||||
|
}
|
||||||
30
src/frontend/src/lib/stores/index.ts
Normal file
30
src/frontend/src/lib/stores/index.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
export { userStore, setUser, clearUser } from './userStore';
|
||||||
|
export { botsStore, setBots, addBot, updateBot, removeBot, clearBots } from './botsStore';
|
||||||
|
export { currentBotStore, setCurrentBot, clearCurrentBot } from './currentBotStore';
|
||||||
|
export { chatStore, addMessage, setMessages, clearChat } from './chatStore';
|
||||||
|
export {
|
||||||
|
backtestStore,
|
||||||
|
setCurrentBacktest,
|
||||||
|
addBacktestToHistory,
|
||||||
|
setBacktestHistory,
|
||||||
|
setBacktestLoading,
|
||||||
|
setBacktestError,
|
||||||
|
clearBacktestState
|
||||||
|
} from './backtestStore';
|
||||||
|
export {
|
||||||
|
simulationStore,
|
||||||
|
setCurrentSimulation,
|
||||||
|
addSignals,
|
||||||
|
clearSignals,
|
||||||
|
setSimulationLoading,
|
||||||
|
setSimulationError,
|
||||||
|
clearSimulationState
|
||||||
|
} from './simulationStore';
|
||||||
|
export {
|
||||||
|
isAuthenticated,
|
||||||
|
isLoading,
|
||||||
|
initAuth,
|
||||||
|
login,
|
||||||
|
register,
|
||||||
|
logout
|
||||||
|
} from './authStore';
|
||||||
45
src/frontend/src/lib/stores/simulationStore.ts
Normal file
45
src/frontend/src/lib/stores/simulationStore.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import type { Simulation, Signal } from '$lib/api';
|
||||||
|
|
||||||
|
export interface SimulationState {
|
||||||
|
currentSimulation: Simulation | null;
|
||||||
|
signals: Signal[];
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: SimulationState = {
|
||||||
|
currentSimulation: null,
|
||||||
|
signals: [],
|
||||||
|
isLoading: false,
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
|
||||||
|
export const simulationStore = writable<SimulationState>(initialState);
|
||||||
|
|
||||||
|
export function setCurrentSimulation(simulation: Simulation | null) {
|
||||||
|
simulationStore.update(state => ({ ...state, currentSimulation: simulation }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addSignals(newSignals: Signal[]) {
|
||||||
|
simulationStore.update(state => ({
|
||||||
|
...state,
|
||||||
|
signals: [...state.signals, ...newSignals]
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearSignals() {
|
||||||
|
simulationStore.update(state => ({ ...state, signals: [] }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setSimulationLoading(loading: boolean) {
|
||||||
|
simulationStore.update(state => ({ ...state, isLoading: loading }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setSimulationError(error: string | null) {
|
||||||
|
simulationStore.update(state => ({ ...state, error }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearSimulationState() {
|
||||||
|
simulationStore.set(initialState);
|
||||||
|
}
|
||||||
12
src/frontend/src/lib/stores/userStore.ts
Normal file
12
src/frontend/src/lib/stores/userStore.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import type { User } from '$lib/api';
|
||||||
|
|
||||||
|
export const userStore = writable<User | null>(null);
|
||||||
|
|
||||||
|
export function setUser(user: User | null) {
|
||||||
|
userStore.set(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearUser() {
|
||||||
|
userStore.set(null);
|
||||||
|
}
|
||||||
54
src/frontend/src/routes/+layout.svelte
Normal file
54
src/frontend/src/routes/+layout.svelte
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { initAuth, isLoading } from '$lib/stores';
|
||||||
|
import favicon from '$lib/assets/favicon.svg';
|
||||||
|
|
||||||
|
let { children } = $props();
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
initAuth();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<link rel="icon" href={favicon} />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
{#if $isLoading}
|
||||||
|
<div class="loading">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
{@render children()}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(body) {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #0f0f0f;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-top-color: #667eea;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
135
src/frontend/src/routes/+page.svelte
Normal file
135
src/frontend/src/routes/+page.svelte
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { isAuthenticated } from '$lib/stores';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Randebu - AI Trading Bot Platform</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
{#if $isAuthenticated}
|
||||||
|
<script>
|
||||||
|
window.location.href = '/dashboard';
|
||||||
|
</script>
|
||||||
|
{:else}
|
||||||
|
<main>
|
||||||
|
<div class="hero">
|
||||||
|
<h1>Randebu</h1>
|
||||||
|
<p class="tagline">Create trading bots through conversation with AI</p>
|
||||||
|
<div class="cta">
|
||||||
|
<a href="/register" class="btn btn-primary">Get Started</a>
|
||||||
|
<a href="/login" class="btn btn-secondary">Login</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="features">
|
||||||
|
<h2>How It Works</h2>
|
||||||
|
<div class="feature-grid">
|
||||||
|
<div class="feature">
|
||||||
|
<h3>1. Describe Your Strategy</h3>
|
||||||
|
<p>Tell our AI what kind of trading you want to do in plain English</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature">
|
||||||
|
<h3>2. Backtest & Validate</h3>
|
||||||
|
<p>Test your strategy against historical data before risking real funds</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature">
|
||||||
|
<h3>3. Simulate & Monitor</h3>
|
||||||
|
<p>Run real-time simulations and watch for trading signals</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(body) {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #0f0f0f;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
text-align: center;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3.5rem;
|
||||||
|
margin: 0;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagline {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: #aaa;
|
||||||
|
margin: 1rem 0 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: transform 0.2s, opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: white;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.features {
|
||||||
|
margin-top: 4rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 2rem;
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature h3 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature p {
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
354
src/frontend/src/routes/bot/[id]/+page.svelte
Normal file
354
src/frontend/src/routes/bot/[id]/+page.svelte
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { isAuthenticated, isLoading, chatStore, addMessage, setMessages, clearChat, currentBotStore, setCurrentBot } from '$lib/stores';
|
||||||
|
import { api } from '$lib/api';
|
||||||
|
|
||||||
|
let botId = $derived($page.params.id);
|
||||||
|
let messageInput = $state('');
|
||||||
|
let isSending = $state(false);
|
||||||
|
let chatContainer: HTMLDivElement;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if (!$isAuthenticated && !$isLoading) {
|
||||||
|
goto('/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ($isAuthenticated && botId) {
|
||||||
|
await loadBot();
|
||||||
|
await loadChatHistory();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadBot() {
|
||||||
|
try {
|
||||||
|
const bot = await api.bots.get(botId);
|
||||||
|
setCurrentBot(bot);
|
||||||
|
} catch (e) {
|
||||||
|
goto('/dashboard');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadChatHistory() {
|
||||||
|
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;
|
||||||
|
|
||||||
|
const userMessage = messageInput;
|
||||||
|
messageInput = '';
|
||||||
|
isSending = true;
|
||||||
|
|
||||||
|
addMessage({ role: 'user', content: userMessage });
|
||||||
|
scrollToBottom();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api.bots.chat(botId, userMessage);
|
||||||
|
addMessage({ role: 'assistant', content: response.response });
|
||||||
|
|
||||||
|
if (response.strategy_config) {
|
||||||
|
const bot = await api.bots.get(botId);
|
||||||
|
setCurrentBot(bot);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{$currentBotStore?.name || 'Bot'} - Randebu</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<header>
|
||||||
|
<div class="header-left">
|
||||||
|
<a href="/dashboard" class="back-link">← Dashboard</a>
|
||||||
|
<h1>{$currentBotStore?.name || 'Loading...'}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<a href="/bot/{botId}/backtest" class="btn btn-secondary">Backtest</a>
|
||||||
|
<a href="/bot/{botId}/simulate" class="btn btn-secondary">Simulate</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="chat-container" bind:this={chatContainer}>
|
||||||
|
{#if $chatStore.length === 0}
|
||||||
|
<div class="welcome-message">
|
||||||
|
<p>Welcome to {$currentBotStore?.name}! Describe your trading strategy in plain English.</p>
|
||||||
|
<p class="hint">Example: "Buy PEPE when the price drops by 5% within 1 hour"</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#each $chatStore as message}
|
||||||
|
<div class="message {message.role}">
|
||||||
|
<div class="message-content">
|
||||||
|
{message.content}
|
||||||
|
</div>
|
||||||
|
<div class="message-time">
|
||||||
|
{message.timestamp.toLocaleTimeString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if isSending}
|
||||||
|
<div class="message assistant">
|
||||||
|
<div class="message-content typing">
|
||||||
|
<span class="dot"></span>
|
||||||
|
<span class="dot"></span>
|
||||||
|
<span class="dot"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if $currentBotStore}
|
||||||
|
<div class="input-container">
|
||||||
|
<textarea
|
||||||
|
bind:value={messageInput}
|
||||||
|
onkeydown={handleKeydown}
|
||||||
|
placeholder="Describe your trading strategy..."
|
||||||
|
rows="1"
|
||||||
|
disabled={isSending}
|
||||||
|
></textarea>
|
||||||
|
<button onclick={sendMessage} disabled={isSending || !messageInput.trim()}>
|
||||||
|
Send
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(body) {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #0f0f0f;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link {
|
||||||
|
color: #667eea;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: white;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-container {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-message {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-message .hint {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.user {
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.assistant {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
max-width: 70%;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.user .message-content {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.assistant .message-content {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-time {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background: #888;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: typing 1.4s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot:nth-child(2) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot:nth-child(3) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes typing {
|
||||||
|
0%, 60%, 100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
30% {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1rem;
|
||||||
|
resize: none;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
391
src/frontend/src/routes/bot/[id]/backtest/+page.svelte
Normal file
391
src/frontend/src/routes/bot/[id]/backtest/+page.svelte
Normal file
@@ -0,0 +1,391 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { isAuthenticated, isLoading, currentBotStore, setCurrentBot, backtestStore, setCurrentBacktest, addBacktestToHistory, setBacktestLoading, setBacktestError } from '$lib/stores';
|
||||||
|
import { api } from '$lib/api';
|
||||||
|
|
||||||
|
let botId = $derived($page.params.id);
|
||||||
|
let token = $state('PEPE');
|
||||||
|
let timeframe = $state('1h');
|
||||||
|
let startDate = $state('');
|
||||||
|
let endDate = $state('');
|
||||||
|
let isRunning = $state(false);
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if (!$isAuthenticated && !$isLoading) {
|
||||||
|
goto('/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ($isAuthenticated && botId) {
|
||||||
|
await loadBot();
|
||||||
|
await loadBacktests();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadBot() {
|
||||||
|
try {
|
||||||
|
const bot = await api.bots.get(botId);
|
||||||
|
setCurrentBot(bot);
|
||||||
|
} catch (e) {
|
||||||
|
goto('/dashboard');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBacktests() {
|
||||||
|
try {
|
||||||
|
const backtests = await api.backtest.list(botId);
|
||||||
|
setBacktestHistory(backtests);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load backtests:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startBacktest() {
|
||||||
|
if (!startDate || !endDate) return;
|
||||||
|
setBacktestError(null);
|
||||||
|
setBacktestLoading(true);
|
||||||
|
isRunning = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const backtest = await api.backtest.start(botId, {
|
||||||
|
token,
|
||||||
|
timeframe,
|
||||||
|
start_date: startDate,
|
||||||
|
end_date: endDate
|
||||||
|
});
|
||||||
|
setCurrentBacktest(backtest);
|
||||||
|
addBacktestToHistory(backtest);
|
||||||
|
} catch (e) {
|
||||||
|
setBacktestError(e instanceof Error ? e.message : 'Failed to start backtest');
|
||||||
|
} finally {
|
||||||
|
setBacktestLoading(false);
|
||||||
|
isRunning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopBacktest(runId: string) {
|
||||||
|
try {
|
||||||
|
await api.backtest.stop(botId, runId);
|
||||||
|
await loadBacktests();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to stop backtest:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBacktestHistory(backtests: any[]) {
|
||||||
|
backtestStore.update(state => ({ ...state, backtestHistory: backtests }));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Backtest - {$currentBotStore?.name || 'Bot'} - Randebu</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<header>
|
||||||
|
<div class="header-left">
|
||||||
|
<a href="/bot/{botId}" class="back-link">← Back to Chat</a>
|
||||||
|
<h1>Backtest</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<section class="config-section">
|
||||||
|
<h2>Configure Backtest</h2>
|
||||||
|
|
||||||
|
{#if $backtestStore.error}
|
||||||
|
<div class="error">{$backtestStore.error}</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<form onsubmit={(e) => { e.preventDefault(); startBacktest(); }}>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="field">
|
||||||
|
<label for="token">Token</label>
|
||||||
|
<input type="text" id="token" bind:value={token} required />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="timeframe">Timeframe</label>
|
||||||
|
<select id="timeframe" bind:value={timeframe}>
|
||||||
|
<option value="1m">1 minute</option>
|
||||||
|
<option value="5m">5 minutes</option>
|
||||||
|
<option value="15m">15 minutes</option>
|
||||||
|
<option value="1h">1 hour</option>
|
||||||
|
<option value="4h">4 hours</option>
|
||||||
|
<option value="1d">1 day</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="field">
|
||||||
|
<label for="startDate">Start Date</label>
|
||||||
|
<input type="date" id="startDate" bind:value={startDate} required />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="endDate">End Date</label>
|
||||||
|
<input type="date" id="endDate" bind:value={endDate} required />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" disabled={isRunning || $backtestStore.isLoading}>
|
||||||
|
{isRunning ? 'Running...' : 'Start Backtest'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="results-section">
|
||||||
|
<h2>Backtest History</h2>
|
||||||
|
|
||||||
|
{#if $backtestStore.backtestHistory.length === 0}
|
||||||
|
<p class="empty-state">No backtests yet. Run your first backtest above.</p>
|
||||||
|
{:else}
|
||||||
|
<div class="backtest-list">
|
||||||
|
{#each $backtestStore.backtestHistory as backtest}
|
||||||
|
<div class="backtest-card">
|
||||||
|
<div class="backtest-header">
|
||||||
|
<span class="backtest-status status-{backtest.status}">{backtest.status}</span>
|
||||||
|
<span class="backtest-date">{new Date(backtest.started_at).toLocaleDateString()}</span>
|
||||||
|
</div>
|
||||||
|
{#if backtest.result}
|
||||||
|
<div class="backtest-results">
|
||||||
|
<div class="result-item">
|
||||||
|
<span class="result-label">Total Return</span>
|
||||||
|
<span class="result-value" class:positive={backtest.result.total_return > 0} class:negative={backtest.result.total_return < 0}>
|
||||||
|
{backtest.result.total_return.toFixed(2)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="result-item">
|
||||||
|
<span class="result-label">Win Rate</span>
|
||||||
|
<span class="result-value">{backtest.result.win_rate.toFixed(1)}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="result-item">
|
||||||
|
<span class="result-label">Total Trades</span>
|
||||||
|
<span class="result-value">{backtest.result.total_trades}</span>
|
||||||
|
</div>
|
||||||
|
<div class="result-item">
|
||||||
|
<span class="result-label">Max Drawdown</span>
|
||||||
|
<span class="result-value negative">{backtest.result.max_drawdown.toFixed(2)}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if backtest.status === 'running'}
|
||||||
|
<button onclick={() => stopBacktest(backtest.id)} class="btn btn-danger">Stop</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(body) {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #0f0f0f;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
min-height: 100vh;
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link {
|
||||||
|
color: #667eea;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: grid;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background: rgba(239, 68, 68, 0.2);
|
||||||
|
border: 1px solid #ef4444;
|
||||||
|
color: #fca5a5;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, select {
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus, select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.875rem;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
color: #888;
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backtest-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backtest-card {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backtest-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backtest-status {
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-running {
|
||||||
|
background: rgba(59, 130, 246, 0.2);
|
||||||
|
color: #60a5fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-completed {
|
||||||
|
background: rgba(34, 197, 94, 0.2);
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-failed {
|
||||||
|
background: rgba(239, 68, 68, 0.2);
|
||||||
|
color: #fca5a5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backtest-date {
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backtest-results {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-value {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.positive {
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.negative {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
width: auto;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: rgba(239, 68, 68, 0.2);
|
||||||
|
color: #fca5a5;
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.4);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
426
src/frontend/src/routes/bot/[id]/simulate/+page.svelte
Normal file
426
src/frontend/src/routes/bot/[id]/simulate/+page.svelte
Normal file
@@ -0,0 +1,426 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { isAuthenticated, isLoading, currentBotStore, setCurrentBot, simulationStore, setCurrentSimulation, addSignals, clearSignals, setSimulationLoading, setSimulationError } from '$lib/stores';
|
||||||
|
import { api } from '$lib/api';
|
||||||
|
|
||||||
|
let botId = $derived($page.params.id);
|
||||||
|
let token = $state('PEPE');
|
||||||
|
let intervalSeconds = $state(60);
|
||||||
|
let autoExecute = $state(false);
|
||||||
|
let isRunning = $state(false);
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if (!$isAuthenticated && !$isLoading) {
|
||||||
|
goto('/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ($isAuthenticated && botId) {
|
||||||
|
await loadBot();
|
||||||
|
await loadSimulations();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadBot() {
|
||||||
|
try {
|
||||||
|
const bot = await api.bots.get(botId);
|
||||||
|
setCurrentBot(bot);
|
||||||
|
} catch (e) {
|
||||||
|
goto('/dashboard');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSimulations() {
|
||||||
|
try {
|
||||||
|
const simulations = await api.simulate.list(botId);
|
||||||
|
if (simulations.length > 0) {
|
||||||
|
const latest = simulations[0];
|
||||||
|
setCurrentSimulation(latest);
|
||||||
|
if (latest.signals) {
|
||||||
|
addSignals(latest.signals);
|
||||||
|
}
|
||||||
|
if (latest.status === 'running') {
|
||||||
|
isRunning = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load simulations:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startSimulation() {
|
||||||
|
setSimulationError(null);
|
||||||
|
setSimulationLoading(true);
|
||||||
|
isRunning = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const simulation = await api.simulate.start(botId, {
|
||||||
|
token,
|
||||||
|
interval_seconds: intervalSeconds,
|
||||||
|
auto_execute: autoExecute
|
||||||
|
});
|
||||||
|
setCurrentSimulation(simulation);
|
||||||
|
clearSignals();
|
||||||
|
} catch (e) {
|
||||||
|
setSimulationError(e instanceof Error ? e.message : 'Failed to start simulation');
|
||||||
|
isRunning = false;
|
||||||
|
} finally {
|
||||||
|
setSimulationLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopSimulation() {
|
||||||
|
if (!$simulationStore.currentSimulation) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.simulate.stop(botId, $simulationStore.currentSimulation.id);
|
||||||
|
await loadSimulations();
|
||||||
|
isRunning = false;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to stop simulation:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Simulate - {$currentBotStore?.name || 'Bot'} - Randebu</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<header>
|
||||||
|
<div class="header-left">
|
||||||
|
<a href="/bot/{botId}" class="back-link">← Back to Chat</a>
|
||||||
|
<h1>Simulation</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="notice">
|
||||||
|
<span class="notice-icon">⚠️</span>
|
||||||
|
<span>Simulation Mode - Using REST polling (every {intervalSeconds}s). For real-time signals, consider upgrading to Pro tier.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<section class="config-section">
|
||||||
|
<h2>Configure Simulation</h2>
|
||||||
|
|
||||||
|
{#if $simulationStore.error}
|
||||||
|
<div class="error">{$simulationStore.error}</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<form onsubmit={(e) => { e.preventDefault(); startSimulation(); }}>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="field">
|
||||||
|
<label for="token">Token</label>
|
||||||
|
<input type="text" id="token" bind:value={token} required disabled={isRunning} />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="interval">Check Interval (seconds)</label>
|
||||||
|
<select id="interval" bind:value={intervalSeconds} disabled={isRunning}>
|
||||||
|
<option value={30}>30 seconds</option>
|
||||||
|
<option value={60}>60 seconds</option>
|
||||||
|
<option value={120}>2 minutes</option>
|
||||||
|
<option value={300}>5 minutes</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field checkbox-field">
|
||||||
|
<input type="checkbox" id="autoExecute" bind:checked={autoExecute} disabled={isRunning} />
|
||||||
|
<label for="autoExecute">Auto-execute trades (requires Pro tier)</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if isRunning}
|
||||||
|
<button type="button" onclick={stopSimulation} class="btn btn-danger">
|
||||||
|
Stop Simulation
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button type="submit" disabled={$simulationStore.isLoading}>
|
||||||
|
{$simulationStore.isLoading ? 'Starting...' : 'Start Simulation'}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="signals-section">
|
||||||
|
<h2>Signals ({$simulationStore.signals.length})</h2>
|
||||||
|
|
||||||
|
{#if $simulationStore.signals.length === 0}
|
||||||
|
<p class="empty-state">No signals yet. Start a simulation to see trading signals.</p>
|
||||||
|
{:else}
|
||||||
|
<div class="signals-list">
|
||||||
|
{#each $simulationStore.signals as signal}
|
||||||
|
<div class="signal-card">
|
||||||
|
<div class="signal-header">
|
||||||
|
<span class="signal-type type-{signal.signal_type}">{signal.signal_type}</span>
|
||||||
|
<span class="signal-token">{signal.token}</span>
|
||||||
|
<span class="signal-price">${signal.price.toFixed(6)}</span>
|
||||||
|
</div>
|
||||||
|
{#if signal.confidence}
|
||||||
|
<div class="signal-confidence">
|
||||||
|
<span>Confidence: {(signal.confidence * 100).toFixed(1)}%</span>
|
||||||
|
<div class="confidence-bar">
|
||||||
|
<div class="confidence-fill" style="width: {signal.confidence * 100}%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if signal.reasoning}
|
||||||
|
<p class="signal-reasoning">{signal.reasoning}</p>
|
||||||
|
{/if}
|
||||||
|
<div class="signal-time">
|
||||||
|
{new Date(signal.created_at).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(body) {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #0f0f0f;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
min-height: 100vh;
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link {
|
||||||
|
color: #667eea;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
background: rgba(251, 191, 36, 0.1);
|
||||||
|
border: 1px solid rgba(251, 191, 36, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #fbbf24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-icon {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: grid;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background: rgba(239, 68, 68, 0.2);
|
||||||
|
border: 1px solid #ef4444;
|
||||||
|
color: #fca5a5;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-field {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-field input {
|
||||||
|
width: auto;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-field label {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, select {
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus, select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.875rem;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: rgba(239, 68, 68, 0.2);
|
||||||
|
color: #fca5a5;
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
color: #888;
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signals-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-card {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-type {
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-buy {
|
||||||
|
background: rgba(34, 197, 94, 0.2);
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-sell {
|
||||||
|
background: rgba(239, 68, 68, 0.2);
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-hold {
|
||||||
|
background: rgba(251, 191, 36, 0.2);
|
||||||
|
color: #fbbf24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-token {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-price {
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-confidence {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confidence-bar {
|
||||||
|
flex: 1;
|
||||||
|
height: 4px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confidence-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #667eea, #764ba2);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-reasoning {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #ccc;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-time {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
359
src/frontend/src/routes/dashboard/+page.svelte
Normal file
359
src/frontend/src/routes/dashboard/+page.svelte
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { isAuthenticated, isLoading, botsStore, setBots, addBot, removeBot, userStore, logout } from '$lib/stores';
|
||||||
|
import { api } from '$lib/api';
|
||||||
|
|
||||||
|
let showCreateModal = $state(false);
|
||||||
|
let newBotName = $state('');
|
||||||
|
let newBotDescription = $state('');
|
||||||
|
let isCreating = $state(false);
|
||||||
|
let createError = $state('');
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if (!$isAuthenticated && !$isLoading) {
|
||||||
|
goto('/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ($isAuthenticated) {
|
||||||
|
await loadBots();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadBots() {
|
||||||
|
try {
|
||||||
|
const bots = await api.bots.list();
|
||||||
|
setBots(bots);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load bots:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createBot() {
|
||||||
|
if (!$newBotName.trim()) return;
|
||||||
|
createError = '';
|
||||||
|
isCreating = true;
|
||||||
|
try {
|
||||||
|
const bot = await api.bots.create(newBotName, newBotDescription);
|
||||||
|
addBot(bot);
|
||||||
|
showCreateModal = false;
|
||||||
|
newBotName = '';
|
||||||
|
newBotDescription = '';
|
||||||
|
goto(`/bot/${bot.id}`);
|
||||||
|
} catch (e) {
|
||||||
|
createError = e instanceof Error ? e.message : 'Failed to create bot';
|
||||||
|
} finally {
|
||||||
|
isCreating = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteBot(botId: string) {
|
||||||
|
if (!confirm('Are you sure you want to delete this bot?')) return;
|
||||||
|
try {
|
||||||
|
await api.bots.delete(botId);
|
||||||
|
removeBot(botId);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to delete bot:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLogout() {
|
||||||
|
logout();
|
||||||
|
goto('/');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Dashboard - Randebu</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<header>
|
||||||
|
<h1>Dashboard</h1>
|
||||||
|
<div class="header-actions">
|
||||||
|
<span class="user-email">{$userStore?.email}</span>
|
||||||
|
<a href="/settings" class="btn btn-secondary">Settings</a>
|
||||||
|
<button onclick={handleLogout} class="btn btn-secondary">Logout</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="bots-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>Your Bots ({$botsStore.length}/3)</h2>
|
||||||
|
{#if $botsStore.length < 3}
|
||||||
|
<button onclick={() => showCreateModal = true} class="btn btn-primary">
|
||||||
|
Create New Bot
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if $botsStore.length === 0}
|
||||||
|
<div class="empty-state">
|
||||||
|
<p>You haven't created any bots yet.</p>
|
||||||
|
<p>Create your first bot to start trading!</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="bots-grid">
|
||||||
|
{#each $botsStore as bot}
|
||||||
|
<div class="bot-card">
|
||||||
|
<div class="bot-info">
|
||||||
|
<h3>{bot.name}</h3>
|
||||||
|
{#if bot.description}
|
||||||
|
<p class="bot-description">{bot.description}</p>
|
||||||
|
{/if}
|
||||||
|
<span class="bot-status status-{bot.status}">{bot.status}</span>
|
||||||
|
</div>
|
||||||
|
<div class="bot-actions">
|
||||||
|
<a href="/bot/{bot.id}" class="btn btn-primary">Open</a>
|
||||||
|
<button onclick={() => deleteBot(bot.id)} class="btn btn-danger">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{#if showCreateModal}
|
||||||
|
<div class="modal-overlay" onclick={() => showCreateModal = false}>
|
||||||
|
<div class="modal" onclick={(e) => e.stopPropagation()}>
|
||||||
|
<h2>Create New Bot</h2>
|
||||||
|
{#if createError}
|
||||||
|
<div class="error">{createError}</div>
|
||||||
|
{/if}
|
||||||
|
<form onsubmit={(e) => { e.preventDefault(); createBot(); }}>
|
||||||
|
<div class="field">
|
||||||
|
<label for="botName">Bot Name</label>
|
||||||
|
<input type="text" id="botName" bind:value={newBotName} required />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="botDescription">Description (optional)</label>
|
||||||
|
<textarea id="botDescription" bind:value={newBotDescription} rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button type="button" onclick={() => showCreateModal = false} class="btn btn-secondary">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type="submit" disabled={isCreating}>
|
||||||
|
{isCreating ? 'Creating...' : 'Create'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(body) {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #0f0f0f;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 2rem;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-email {
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bots-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
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;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
transition: transform 0.2s, opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: white;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 2rem;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 450px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal h2 {
|
||||||
|
margin: 0 0 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background: rgba(239, 68, 68, 0.2);
|
||||||
|
border: 1px solid #ef4444;
|
||||||
|
color: #fca5a5;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus, textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
176
src/frontend/src/routes/login/+page.svelte
Normal file
176
src/frontend/src/routes/login/+page.svelte
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { login, isAuthenticated } from '$lib/stores';
|
||||||
|
|
||||||
|
let email = $state('');
|
||||||
|
let password = $state('');
|
||||||
|
let error = $state('');
|
||||||
|
let isLoading = $state(false);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if ($isAuthenticated) {
|
||||||
|
goto('/dashboard');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
error = '';
|
||||||
|
isLoading = true;
|
||||||
|
try {
|
||||||
|
await login(email, password);
|
||||||
|
goto('/dashboard');
|
||||||
|
} catch (e) {
|
||||||
|
error = e instanceof Error ? e.message : 'Login failed';
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Login - Randebu</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="auth-card">
|
||||||
|
<h1>Login</h1>
|
||||||
|
<p class="subtitle">Welcome back</p>
|
||||||
|
|
||||||
|
{#if error}
|
||||||
|
<div class="error">{error}</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
|
||||||
|
<div class="field">
|
||||||
|
<label for="email">Email</label>
|
||||||
|
<input type="email" id="email" bind:value={email} required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" id="password" bind:value={password} required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" disabled={isLoading}>
|
||||||
|
{isLoading ? 'Logging in...' : 'Login'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="footer">
|
||||||
|
Don't have an account? <a href="/register">Register</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(body) {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #0f0f0f;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 2.5rem;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: #888;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0.5rem 0 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background: rgba(239, 68, 68, 0.2);
|
||||||
|
border: 1px solid #ef4444;
|
||||||
|
color: #fca5a5;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.875rem;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a {
|
||||||
|
color: #667eea;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
193
src/frontend/src/routes/register/+page.svelte
Normal file
193
src/frontend/src/routes/register/+page.svelte
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { register, isAuthenticated } from '$lib/stores';
|
||||||
|
|
||||||
|
let email = $state('');
|
||||||
|
let password = $state('');
|
||||||
|
let confirmPassword = $state('');
|
||||||
|
let error = $state('');
|
||||||
|
let isLoading = $state(false);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if ($isAuthenticated) {
|
||||||
|
goto('/dashboard');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
error = '';
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
error = 'Passwords do not match';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 6) {
|
||||||
|
error = 'Password must be at least 6 characters';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = true;
|
||||||
|
try {
|
||||||
|
await register(email, password);
|
||||||
|
goto('/dashboard');
|
||||||
|
} catch (e) {
|
||||||
|
error = e instanceof Error ? e.message : 'Registration failed';
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Register - Randebu</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="auth-card">
|
||||||
|
<h1>Create Account</h1>
|
||||||
|
<p class="subtitle">Start creating trading bots</p>
|
||||||
|
|
||||||
|
{#if error}
|
||||||
|
<div class="error">{error}</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
|
||||||
|
<div class="field">
|
||||||
|
<label for="email">Email</label>
|
||||||
|
<input type="email" id="email" bind:value={email} required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" id="password" bind:value={password} required minlength="6" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="confirmPassword">Confirm Password</label>
|
||||||
|
<input type="password" id="confirmPassword" bind:value={confirmPassword} required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" disabled={isLoading}>
|
||||||
|
{isLoading ? 'Creating account...' : 'Create Account'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="footer">
|
||||||
|
Already have an account? <a href="/login">Login</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(body) {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #0f0f0f;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 2.5rem;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: #888;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0.5rem 0 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background: rgba(239, 68, 68, 0.2);
|
||||||
|
border: 1px solid #ef4444;
|
||||||
|
color: #fca5a5;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.875rem;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a {
|
||||||
|
color: #667eea;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
281
src/frontend/src/routes/settings/+page.svelte
Normal file
281
src/frontend/src/routes/settings/+page.svelte
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { isAuthenticated, isLoading, userStore, logout } from '$lib/stores';
|
||||||
|
import { api } from '$lib/api';
|
||||||
|
|
||||||
|
let email = $state('');
|
||||||
|
let currentPassword = $state('');
|
||||||
|
let newPassword = $state('');
|
||||||
|
let confirmPassword = $state('');
|
||||||
|
let isUpdating = $state(false);
|
||||||
|
let updateSuccess = $state('');
|
||||||
|
let updateError = $state('');
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if (!$isAuthenticated && !$isLoading) {
|
||||||
|
goto('/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ($userStore) {
|
||||||
|
email = $userStore.email;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function updateEmail() {
|
||||||
|
updateSuccess = '';
|
||||||
|
updateError = '';
|
||||||
|
isUpdating = true;
|
||||||
|
try {
|
||||||
|
await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:8000/api'}/auth/settings`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email })
|
||||||
|
});
|
||||||
|
updateSuccess = 'Email updated successfully';
|
||||||
|
} catch (e) {
|
||||||
|
updateError = e instanceof Error ? e.message : 'Failed to update email';
|
||||||
|
} finally {
|
||||||
|
isUpdating = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updatePassword() {
|
||||||
|
updateSuccess = '';
|
||||||
|
updateError = '';
|
||||||
|
|
||||||
|
if (newPassword !== confirmPassword) {
|
||||||
|
updateError = 'Passwords do not match';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPassword.length < 6) {
|
||||||
|
updateError = 'Password must be at least 6 characters';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isUpdating = true;
|
||||||
|
try {
|
||||||
|
await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:8000/api'}/auth/settings`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ password: newPassword, current_password: currentPassword })
|
||||||
|
});
|
||||||
|
updateSuccess = 'Password updated successfully';
|
||||||
|
currentPassword = '';
|
||||||
|
newPassword = '';
|
||||||
|
confirmPassword = '';
|
||||||
|
} catch (e) {
|
||||||
|
updateError = e instanceof Error ? e.message : 'Failed to update password';
|
||||||
|
} finally {
|
||||||
|
isUpdating = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLogout() {
|
||||||
|
logout();
|
||||||
|
goto('/');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Settings - Randebu</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<header>
|
||||||
|
<div class="header-left">
|
||||||
|
<a href="/dashboard" class="back-link">← Dashboard</a>
|
||||||
|
<h1>Settings</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<section class="settings-section">
|
||||||
|
<h2>Profile</h2>
|
||||||
|
|
||||||
|
{#if updateSuccess}
|
||||||
|
<div class="success">{updateSuccess}</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if updateError}
|
||||||
|
<div class="error">{updateError}</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<form onsubmit={(e) => { e.preventDefault(); updateEmail(); }}>
|
||||||
|
<div class="field">
|
||||||
|
<label for="email">Email</label>
|
||||||
|
<input type="email" id="email" bind:value={email} required />
|
||||||
|
</div>
|
||||||
|
<button type="submit" disabled={isUpdating}>
|
||||||
|
{isUpdating ? 'Updating...' : 'Update Email'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="settings-section">
|
||||||
|
<h2>Change Password</h2>
|
||||||
|
|
||||||
|
<form onsubmit={(e) => { e.preventDefault(); updatePassword(); }}>
|
||||||
|
<div class="field">
|
||||||
|
<label for="currentPassword">Current Password</label>
|
||||||
|
<input type="password" id="currentPassword" bind:value={currentPassword} required />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="newPassword">New Password</label>
|
||||||
|
<input type="password" id="newPassword" bind:value={newPassword} required minlength="6" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="confirmPassword">Confirm New Password</label>
|
||||||
|
<input type="password" id="confirmPassword" bind:value={confirmPassword} required />
|
||||||
|
</div>
|
||||||
|
<button type="submit" disabled={isUpdating}>
|
||||||
|
{isUpdating ? 'Updating...' : 'Update Password'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="settings-section danger-section">
|
||||||
|
<h2>Account</h2>
|
||||||
|
<button onclick={handleLogout} class="btn btn-danger">
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(body) {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #0f0f0f;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
min-height: 100vh;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link {
|
||||||
|
color: #667eea;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success {
|
||||||
|
background: rgba(34, 197, 94, 0.2);
|
||||||
|
border: 1px solid #22c55e;
|
||||||
|
color: #86efac;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background: rgba(239, 68, 68, 0.2);
|
||||||
|
border: 1px solid #ef4444;
|
||||||
|
color: #fca5a5;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.875rem;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-section button {
|
||||||
|
background: rgba(239, 68, 68, 0.2);
|
||||||
|
color: #fca5a5;
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.4);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
3
src/frontend/static/robots.txt
Normal file
3
src/frontend/static/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# allow crawling everything by default
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
17
src/frontend/svelte.config.js
Normal file
17
src/frontend/svelte.config.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import adapter from '@sveltejs/adapter-auto';
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
const config = {
|
||||||
|
compilerOptions: {
|
||||||
|
// Force runes mode for the project, except for libraries. Can be removed in svelte 6.
|
||||||
|
runes: ({ filename }) => (filename.split(/[/\\]/).includes('node_modules') ? undefined : true)
|
||||||
|
},
|
||||||
|
kit: {
|
||||||
|
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
||||||
|
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||||
|
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
||||||
|
adapter: adapter()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
20
src/frontend/tsconfig.json
Normal file
20
src/frontend/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rewriteRelativeImportExtensions": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"moduleResolution": "bundler"
|
||||||
|
}
|
||||||
|
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||||
|
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||||
|
//
|
||||||
|
// To make changes to top-level options such as include and exclude, we recommend extending
|
||||||
|
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
|
||||||
|
}
|
||||||
6
src/frontend/vite.config.ts
Normal file
6
src/frontend/vite.config.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [sveltekit()]
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user