diff --git a/src/frontend/src/lib/api/client.ts b/src/frontend/src/lib/api/client.ts index ea13104..a435e3b 100644 --- a/src/frontend/src/lib/api/client.ts +++ b/src/frontend/src/lib/api/client.ts @@ -8,7 +8,10 @@ import type { AuthResponse, BotChatRequest, BotChatResponse, - StrategyConfig + StrategyConfig, + Conversation, + ConversationWithMessages, + Message } from './types'; const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000/api'; @@ -237,5 +240,58 @@ export const api = { }); return handleResponse<{ symbol: string; chain: string; name: string }[]>(response); } + }, + + conversations: { + async list(): Promise { + const response = await fetch(`${API_URL}/conversations`, { + headers: getAuthHeaders() + }); + return handleResponse(response); + }, + + async create(): Promise { + const response = await fetch(`${API_URL}/conversations`, { + method: 'POST', + headers: getAuthHeaders() + }); + return handleResponse(response); + }, + + async get(id: string): Promise { + const response = await fetch(`${API_URL}/conversations/${id}`, { + headers: getAuthHeaders() + }); + return handleResponse(response); + }, + + async delete(id: string): Promise { + const response = await fetch(`${API_URL}/conversations/${id}`, { + method: 'DELETE', + headers: getAuthHeaders() + }); + if (!response.ok) { + throw new Error(`HTTP error ${response.status}`); + } + }, + + async chat(id: string, message: string, signal?: AbortSignal): Promise { + const response = await fetch(`${API_URL}/conversations/${id}/chat`, { + method: 'POST', + headers: getAuthHeaders(), + body: JSON.stringify({ message }), + signal + }); + return handleResponse(response); + }, + + async setBot(id: string, botId: string): Promise { + const response = await fetch(`${API_URL}/conversations/${id}/set-bot`, { + method: 'POST', + headers: getAuthHeaders(), + body: JSON.stringify({ bot_id: botId }) + }); + return handleResponse(response); + } } }; diff --git a/src/frontend/src/lib/api/types.ts b/src/frontend/src/lib/api/types.ts index ea3cbd5..6b8f7d9 100644 --- a/src/frontend/src/lib/api/types.ts +++ b/src/frontend/src/lib/api/types.ts @@ -186,3 +186,24 @@ export interface TokenSearchResult { address: string; chain: string; } + +export interface Conversation { + id: string; + user_id: string | null; + bot_id: string | null; + title: string; + created_at: string; + updated_at: string; +} + +export interface Message { + id: string; + conversation_id: string; + role: 'user' | 'assistant'; + content: string; + created_at: string; +} + +export interface ConversationWithMessages extends Conversation { + messages: Message[]; +} diff --git a/src/frontend/src/lib/components/AnonymousBanner.svelte b/src/frontend/src/lib/components/AnonymousBanner.svelte new file mode 100644 index 0000000..848c48a --- /dev/null +++ b/src/frontend/src/lib/components/AnonymousBanner.svelte @@ -0,0 +1,59 @@ + + +
+ {#if showWarning} + + Warning: You've used {chatCount}/{limit} messages. + Login to continue + + {:else} + + Your progress is not saved. + Login to save + + {/if} +
+ + \ No newline at end of file diff --git a/src/frontend/src/lib/components/BotInfoPanel.svelte b/src/frontend/src/lib/components/BotInfoPanel.svelte new file mode 100644 index 0000000..ff50c5f --- /dev/null +++ b/src/frontend/src/lib/components/BotInfoPanel.svelte @@ -0,0 +1,125 @@ + + +
+ {#if !bot} +

No bot selected

+ + {:else} +

{bot.name}

+ +
+
+ Chain: {bot.chain || 'bsc'} +
+
+ Status: + + {bot.status} + +
+ {#if bot.strategy_config && bot.strategy_config.conditions && bot.strategy_config.conditions.length > 0} +
+ Strategy: +

{bot.strategy_config.conditions.length} condition(s) configured

+
+ {/if} +
+ + {#if onSelectBot} + + {/if} + {/if} +
+ + \ No newline at end of file diff --git a/src/frontend/src/lib/components/CandlestickLoader.svelte b/src/frontend/src/lib/components/CandlestickLoader.svelte new file mode 100644 index 0000000..7e9fa4b --- /dev/null +++ b/src/frontend/src/lib/components/CandlestickLoader.svelte @@ -0,0 +1,47 @@ + + +
+ {#each bars as i} +
+ {/each} +
+ + \ No newline at end of file diff --git a/src/frontend/src/lib/components/ChatArea.svelte b/src/frontend/src/lib/components/ChatArea.svelte new file mode 100644 index 0000000..6d3fd83 --- /dev/null +++ b/src/frontend/src/lib/components/ChatArea.svelte @@ -0,0 +1,218 @@ + + +
+ {#if !conversationId} +
+ Select a conversation or start a new one +
+ {:else if messages.length === 0 && !isLoading} +
+ Send a message to start the conversation +
+ {:else} + {#each messages as msg (msg.id)} +
+
+ {#each renderContent(msg.content) as segment} + {#if segment.type === 'bold'} + {segment.content} + {:else if segment.type === 'italic'} + {segment.content} + {:else if segment.type === 'code'} + {segment.content} + {:else if segment.type === 'codeBlock'} +
{segment.content}
+ {:else if segment.type === 'link'} + {segment.content} + {:else if segment.type === 'list' && segment.items} +
    + {#each segment.items as item} +
  • {@html renderInline(parseInlineElements(item))}
  • + {/each} +
+ {:else if segment.type === 'lineBreak'} +
+ {:else} + {segment.content} + {/if} + {/each} +
+
+ {new Date(msg.created_at).toLocaleTimeString()} +
+
+ {/each} + {/if} + + {#if isLoading} +
+
+ +
+
+ {/if} +
+ + \ No newline at end of file diff --git a/src/frontend/src/lib/components/ChatInput.svelte b/src/frontend/src/lib/components/ChatInput.svelte new file mode 100644 index 0000000..fe9395c --- /dev/null +++ b/src/frontend/src/lib/components/ChatInput.svelte @@ -0,0 +1,118 @@ + + +
+ + +
+ + \ No newline at end of file diff --git a/src/frontend/src/lib/components/ChatLayout.svelte b/src/frontend/src/lib/components/ChatLayout.svelte new file mode 100644 index 0000000..397bb20 --- /dev/null +++ b/src/frontend/src/lib/components/ChatLayout.svelte @@ -0,0 +1,67 @@ + + +
+ {#if leftPane} +
+ {@render leftPane()} +
+ {/if} + +
+ {#if children} + {@render children()} + {/if} +
+ + {#if rightPane} +
+ {@render rightPane()} +
+ {/if} +
+ + \ No newline at end of file diff --git a/src/frontend/src/lib/components/ConversationList.svelte b/src/frontend/src/lib/components/ConversationList.svelte new file mode 100644 index 0000000..4ce2794 --- /dev/null +++ b/src/frontend/src/lib/components/ConversationList.svelte @@ -0,0 +1,204 @@ + + +
+
+ +
+ +
+ {#if loading} +
Loading...
+ {:else if error} +
{error}
+ {:else if conversations.length === 0} +
No conversations yet
+ {:else} + {#each conversations as conv (conv.id)} +
goto(`/chat/${conv.id}`)} + role="button" + tabindex="0" + onkeydown={(e) => e.key === 'Enter' && goto(`/chat/${conv.id}`)} + > +
{conv.title || 'New Chat'}
+
{formatDate(conv.updated_at)}
+
+ {/each} + {/if} +
+
+ + \ No newline at end of file diff --git a/src/frontend/src/lib/components/index.ts b/src/frontend/src/lib/components/index.ts index 4a6173d..555c567 100644 --- a/src/frontend/src/lib/components/index.ts +++ b/src/frontend/src/lib/components/index.ts @@ -8,4 +8,11 @@ export { default as PortfolioSummary } from './PortfolioSummary.svelte'; export { default as BacktestChart } from './BacktestChart.svelte'; export { default as ProUpgradeBanner } from './ProUpgradeBanner.svelte'; export { default as TokenPicker } from './TokenPicker.svelte'; -export { default as ConditionBuilder } from './ConditionBuilder.svelte'; \ No newline at end of file +export { default as ConditionBuilder } from './ConditionBuilder.svelte'; +export { default as ChatLayout } from './ChatLayout.svelte'; +export { default as ConversationList } from './ConversationList.svelte'; +export { default as ChatArea } from './ChatArea.svelte'; +export { default as ChatInput } from './ChatInput.svelte'; +export { default as BotInfoPanel } from './BotInfoPanel.svelte'; +export { default as AnonymousBanner } from './AnonymousBanner.svelte'; +export { default as CandlestickLoader } from './CandlestickLoader.svelte'; \ No newline at end of file diff --git a/src/frontend/src/lib/stores/conversationStore.ts b/src/frontend/src/lib/stores/conversationStore.ts new file mode 100644 index 0000000..02958b7 --- /dev/null +++ b/src/frontend/src/lib/stores/conversationStore.ts @@ -0,0 +1,96 @@ +import { writable, derived } from 'svelte/store'; +import type { Conversation, Message } from '$lib/api'; + +export interface ConversationState { + conversations: Conversation[]; + currentConversationId: string | null; + messages: Message[]; + isLoading: boolean; + error: string | null; + anonymousChatCount: number; +} + +const initialState: ConversationState = { + conversations: [], + currentConversationId: null, + messages: [], + isLoading: false, + error: null, + anonymousChatCount: 0 +}; + +export const conversationStore = writable(initialState); + +export function setConversations(conversations: Conversation[]) { + conversationStore.update(state => ({ ...state, conversations })); +} + +export function addConversation(conversation: Conversation) { + conversationStore.update(state => ({ + ...state, + conversations: [conversation, ...state.conversations] + })); +} + +export function removeConversation(conversationId: string) { + conversationStore.update(state => ({ + ...state, + conversations: state.conversations.filter(c => c.id !== conversationId), + currentConversationId: state.currentConversationId === conversationId ? null : state.currentConversationId + })); +} + +export function setCurrentConversation(conversationId: string | null) { + conversationStore.update(state => ({ + ...state, + currentConversationId: conversationId, + messages: [] + })); +} + +export function setMessages(messages: Message[]) { + conversationStore.update(state => ({ ...state, messages })); +} + +export function addMessage(message: Message) { + conversationStore.update(state => ({ + ...state, + messages: [...state.messages, message] + })); +} + +export function setLoading(isLoading: boolean) { + conversationStore.update(state => ({ ...state, isLoading })); +} + +export function setError(error: string | null) { + conversationStore.update(state => ({ ...state, error })); +} + +export function incrementAnonymousChatCount() { + conversationStore.update(state => ({ + ...state, + anonymousChatCount: state.anonymousChatCount + 1 + })); +} + +export function resetAnonymousChatCount() { + conversationStore.update(state => ({ + ...state, + anonymousChatCount: 0 + })); +} + +export function updateConversationTitle(conversationId: string, title: string) { + conversationStore.update(state => ({ + ...state, + conversations: state.conversations.map(c => + c.id === conversationId ? { ...c, title, updated_at: new Date().toISOString() } : c + ) + })); +} + +export const currentConversation = derived( + conversationStore, + $state => $state.conversations.find(c => c.id === $state.currentConversationId) ?? null +); \ No newline at end of file diff --git a/src/frontend/src/lib/stores/index.ts b/src/frontend/src/lib/stores/index.ts index 2ce2e2d..00a4407 100644 --- a/src/frontend/src/lib/stores/index.ts +++ b/src/frontend/src/lib/stores/index.ts @@ -28,3 +28,18 @@ export { register, logout } from './authStore'; +export { + conversationStore, + setConversations, + addConversation, + removeConversation, + setCurrentConversation, + setMessages as setConversationMessages, + addMessage as addConversationMessage, + setLoading as setConversationLoading, + setError as setConversationError, + incrementAnonymousChatCount, + resetAnonymousChatCount, + updateConversationTitle, + currentConversation +} from './conversationStore'; diff --git a/src/frontend/src/routes/chat/+page.svelte b/src/frontend/src/routes/chat/+page.svelte new file mode 100644 index 0000000..3c8f16c --- /dev/null +++ b/src/frontend/src/routes/chat/+page.svelte @@ -0,0 +1,41 @@ + + + + Chat - Randebu + + +{#if !$isAuthenticated} + +{/if} + + +
+

Select a conversation or start a new one

+
+
+ + \ No newline at end of file diff --git a/src/frontend/src/routes/chat/[conversationId]/+page.svelte b/src/frontend/src/routes/chat/[conversationId]/+page.svelte new file mode 100644 index 0000000..66765d3 --- /dev/null +++ b/src/frontend/src/routes/chat/[conversationId]/+page.svelte @@ -0,0 +1,185 @@ + + + + Chat - Randebu + + +{#if !$isAuthenticated} + +{/if} + + +
+ {#if error} +
+ {error} +
+ {/if} + + {#if isLoading} +
+ +
+ {:else if conversationId} + + + {:else} +
+ Select a conversation or start a new one +
+ {/if} +
+
+ + \ No newline at end of file diff --git a/src/frontend/src/routes/home/+page.svelte b/src/frontend/src/routes/home/+page.svelte new file mode 100644 index 0000000..947bfec --- /dev/null +++ b/src/frontend/src/routes/home/+page.svelte @@ -0,0 +1,28 @@ + + + +
+

Select a conversation or start a new one

+
+
+ + \ No newline at end of file