feat: show thinking above response with expand/collapse, first line preview
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user