feat: show thinking above response with expand/collapse, first line preview

This commit is contained in:
shokollm
2026-04-10 09:56:21 +00:00
parent ae612ad725
commit 2b875cfa27
3 changed files with 88 additions and 72 deletions

View File

@@ -6,8 +6,6 @@
interface Props { interface Props {
bot: Bot | null; bot: Bot | null;
messages: ChatMessage[]; messages: ChatMessage[];
isThinking?: boolean;
thinkingContent?: string;
onSendMessage: (message: string) => void; onSendMessage: (message: string) => void;
onSelectBot?: (botId: string) => void; onSelectBot?: (botId: string) => void;
availableBots?: Bot[]; availableBots?: Bot[];
@@ -17,8 +15,6 @@
let { let {
bot, bot,
messages, messages,
isThinking = false,
thinkingContent = '',
onSendMessage, onSendMessage,
onSelectBot, onSelectBot,
availableBots = [], availableBots = [],
@@ -27,13 +23,12 @@
let messageInput = $state(''); let messageInput = $state('');
let chatContainer: HTMLDivElement; let chatContainer: HTMLDivElement;
let showThinking = $state(false); let expandedThinking: Record<string, boolean> = $state({});
function handleSend() { function handleSend() {
if (!messageInput.trim()) return; if (!messageInput.trim()) return;
onSendMessage(messageInput); onSendMessage(messageInput);
messageInput = ''; messageInput = '';
showThinking = false;
} }
function handleKeydown(e: KeyboardEvent) { function handleKeydown(e: KeyboardEvent) {
@@ -50,8 +45,8 @@
} }
} }
function toggleThinking() { function toggleThinkingExpand(messageId: string) {
showThinking = !showThinking; expandedThinking[messageId] = !expandedThinking[messageId];
} }
$effect(() => { $effect(() => {
@@ -62,13 +57,6 @@
} }
}); });
// Watch for thinking state changes
$effect(() => {
if (isThinking && thinkingContent) {
showThinking = true;
}
});
function renderContent(content: string) { function renderContent(content: string) {
return parseMarkdown(content); return parseMarkdown(content);
} }
@@ -89,7 +77,7 @@
{/if} {/if}
<div class="chat-messages" bind:this={chatContainer}> <div class="chat-messages" bind:this={chatContainer}>
{#if messages.length === 0 && !isThinking} {#if messages.length === 0}
<div class="welcome-message"> <div class="welcome-message">
<p>Welcome to {bot?.name || 'your bot'}! Describe your trading strategy in plain English.</p> <p>Welcome to {bot?.name || 'your bot'}! Describe your trading strategy in plain English.</p>
<p class="hint">Example: "Buy PEPE when the price drops by 5% within 1 hour"</p> <p class="hint">Example: "Buy PEPE when the price drops by 5% within 1 hour"</p>
@@ -98,6 +86,24 @@
{#each messages as message} {#each messages as message}
<div class="message {message.role}"> <div class="message {message.role}">
{#if message.role === 'assistant' && message.thinking}
{@const firstLine = message.thinking.split('\n')[0]}
{@const isExpanded = expandedThinking[message.id] ?? false}
<div class="thinking-section">
<button class="thinking-toggle" onclick={() => toggleThinkingExpand(message.id)}>
<span class="thinking-icon">{isExpanded ? '▼' : '▶'}</span>
<span class="thinking-label">{isExpanded ? 'Hide reasoning' : 'Show reasoning'}</span>
{#if !isExpanded}
<span class="thinking-preview">{firstLine.slice(0, 60)}{firstLine.length > 60 ? '...' : ''}</span>
{/if}
</button>
{#if isExpanded}
<div class="thinking-content">
{message.thinking}
</div>
{/if}
</div>
{/if}
<div class="message-content"> <div class="message-content">
{#each renderContent(message.content) as segment} {#each renderContent(message.content) as segment}
{#if segment.type === 'bold'} {#if segment.type === 'bold'}
@@ -125,51 +131,7 @@
{message.timestamp.toLocaleTimeString()} {message.timestamp.toLocaleTimeString()}
</div> </div>
</div> </div>
{/each} {/each}
{#if isThinking}
<div class="message assistant thinking">
<div class="message-content">
{#if thinkingContent}
<div class="thinking-header">
<button class="thinking-toggle" onclick={toggleThinking}>
<span class="thinking-icon">{showThinking ? '▼' : '▶'}</span>
<span class="thinking-label">Thinking</span>
</button>
</div>
{#if showThinking}
<div class="thinking-content">
{#each renderContent(thinkingContent) as segment}
{#if segment.type === 'bold'}
<strong>{segment.content}</strong>
{:else if segment.type === 'italic'}
<em>{segment.content}</em>
{:else if segment.type === 'code'}
<code class="inline-code">{segment.content}</code>
{:else if segment.type === 'codeBlock'}
<pre class="code-block"><code>{segment.content}</code></pre>
{:else if segment.type === 'list' && segment.items}
<ul>
{#each segment.items as item}
<li>{item}</li>
{/each}
</ul>
{:else}
{segment.content}
{/if}
{/each}
</div>
{/if}
{:else}
<div class="typing">
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
</div>
{/if}
</div>
</div>
{/if}
</div> </div>
{#if bot} {#if bot}
@@ -179,9 +141,8 @@
onkeydown={handleKeydown} onkeydown={handleKeydown}
placeholder="Describe your trading strategy..." placeholder="Describe your trading strategy..."
rows="1" rows="1"
disabled={isThinking}
></textarea> ></textarea>
<button onclick={handleSend} disabled={isThinking || !messageInput.trim()}> <button onclick={handleSend}>
Send Send
</button> </button>
</div> </div>
@@ -280,6 +241,64 @@
border-bottom-left-radius: 4px; border-bottom-left-radius: 4px;
} }
.thinking-section {
margin-bottom: 0.5rem;
padding: 0.5rem 0.75rem;
background: rgba(255, 255, 255, 0.03);
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.thinking-toggle {
display: flex;
align-items: center;
gap: 0.5rem;
background: none;
border: none;
color: #888;
cursor: pointer;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
transition: background 0.2s;
width: 100%;
text-align: left;
}
.thinking-toggle:hover {
background: rgba(255, 255, 255, 0.1);
}
.thinking-icon {
font-size: 0.6rem;
color: #667eea;
}
.thinking-label {
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #667eea;
}
.thinking-preview {
color: #666;
font-style: italic;
font-weight: normal;
text-transform: none;
letter-spacing: normal;
}
.thinking-content {
color: #888;
font-size: 0.85rem;
padding: 0.75rem 0.5rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
margin-top: 0.5rem;
white-space: pre-wrap;
line-height: 1.6;
}
.message.system .message-content { .message.system .message-content {
background: rgba(251, 191, 36, 0.1); background: rgba(251, 191, 36, 0.1);
color: #fbbf24; color: #fbbf24;

View File

@@ -5,6 +5,7 @@ export interface ChatMessage {
id: string; id: string;
role: 'user' | 'assistant' | 'system'; role: 'user' | 'assistant' | 'system';
content: string; content: string;
thinking: string | null;
timestamp: Date; timestamp: Date;
} }
@@ -37,6 +38,7 @@ export function setMessages(messages: BotConversation[]) {
id: m.id, id: m.id,
role: m.role, role: m.role,
content: m.content, content: m.content,
thinking: null,
timestamp: new Date(m.created_at) timestamp: new Date(m.created_at)
}))); })));
} }

View File

@@ -9,7 +9,6 @@
let botId = $derived($page.params.id); let botId = $derived($page.params.id);
let isSending = $state(false); let isSending = $state(false);
let showStrategy = $state(false); let showStrategy = $state(false);
let thinkingContent = $state('');
onMount(async () => { onMount(async () => {
if (!$isAuthenticated && !$isLoading) { if (!$isAuthenticated && !$isLoading) {
@@ -44,7 +43,6 @@
if (isSending) return; if (isSending) return;
isSending = true; isSending = true;
thinkingContent = '';
// Add user's message immediately so it shows even before API response // Add user's message immediately so it shows even before API response
addMessage({ role: 'user', content: message }); addMessage({ role: 'user', content: message });
@@ -57,9 +55,8 @@
const response = await api.bots.chat(botId, message, controller.signal); const response = await api.bots.chat(botId, message, controller.signal);
clearTimeout(timeoutId); clearTimeout(timeoutId);
// Set thinking content for display // Add assistant response with thinking
thinkingContent = response.thinking || ''; addMessage({ role: 'assistant', content: response.response, thinking: response.thinking || null });
addMessage({ role: 'assistant', content: response.response });
if (response.strategy_config) { if (response.strategy_config) {
const bot = await api.bots.get(botId); const bot = await api.bots.get(botId);
@@ -67,9 +64,9 @@
} }
} catch (e) { } catch (e) {
if (e instanceof Error && e.name === 'AbortError') { if (e instanceof Error && e.name === 'AbortError') {
addMessage({ role: 'assistant', content: 'Request timed out. Please try again.' }); addMessage({ role: 'assistant', content: 'Request timed out. Please try again.', thinking: null });
} else { } else {
addMessage({ role: 'assistant', content: 'Sorry, I encountered an error. Please try again.' }); addMessage({ role: 'assistant', content: 'Sorry, I encountered an error. Please try again.', thinking: null });
} }
} finally { } finally {
isSending = false; isSending = false;
@@ -112,8 +109,6 @@
<ChatInterface <ChatInterface
bot={$currentBotStore} bot={$currentBotStore}
messages={$chatStore} messages={$chatStore}
isThinking={isSending}
{thinkingContent}
onSendMessage={handleSendMessage} onSendMessage={handleSendMessage}
/> />
</div> </div>