diff --git a/src/frontend/src/lib/components/ChatInterface.svelte b/src/frontend/src/lib/components/ChatInterface.svelte index ed83b7a..63d06e6 100644 --- a/src/frontend/src/lib/components/ChatInterface.svelte +++ b/src/frontend/src/lib/components/ChatInterface.svelte @@ -1,11 +1,13 @@
@@ -69,7 +89,7 @@ {/if}
- {#if messages.length === 0} + {#if messages.length === 0 && !isThinking}

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

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

@@ -79,7 +99,27 @@ {#each messages as message}
- {message.content} + {#each renderContent(message.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} +
  • {item}
  • + {/each} +
+ {:else} + {segment.content} + {/if} + {/each}
{message.timestamp.toLocaleTimeString()} @@ -87,12 +127,46 @@
{/each} - {#if isSending} -
-
- - - + {#if isThinking} +
+
+ {#if thinkingContent} +
+ +
+ {#if showThinking} +
+ {#each renderContent(thinkingContent) 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 === 'list' && segment.items} +
    + {#each segment.items as item} +
  • {item}
  • + {/each} +
+ {:else} + {segment.content} + {/if} + {/each} +
+ {/if} + {:else} +
+ + + +
+ {/if}
{/if} @@ -105,9 +179,9 @@ onkeydown={handleKeydown} placeholder="Describe your trading strategy..." rows="1" - disabled={isSending} + disabled={isThinking} > -
@@ -213,6 +287,11 @@ border: 1px solid rgba(251, 191, 36, 0.3); } + .message.thinking .message-content { + background: rgba(255, 255, 255, 0.05); + border: 1px dashed rgba(255, 255, 255, 0.2); + } + .message-time { font-size: 0.7rem; color: #666; @@ -220,10 +299,92 @@ padding: 0 0.5rem; } + .thinking-header { + display: flex; + align-items: center; + margin-bottom: 0.5rem; + } + + .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; + } + + .thinking-toggle:hover { + background: rgba(255, 255, 255, 0.1); + } + + .thinking-icon { + font-size: 0.6rem; + } + + .thinking-label { + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .thinking-content { + color: #999; + font-size: 0.9rem; + padding-top: 0.5rem; + border-top: 1px solid rgba(255, 255, 255, 0.1); + margin-top: 0.5rem; + } + + .inline-code { + background: rgba(0, 0, 0, 0.3); + padding: 0.15rem 0.4rem; + border-radius: 4px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 0.85em; + } + + .code-block { + background: rgba(0, 0, 0, 0.4); + padding: 0.75rem; + border-radius: 6px; + overflow-x: auto; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 0.85rem; + margin: 0.5rem 0; + } + + .code-block code { + white-space: pre; + } + + ul { + margin: 0.5rem 0; + padding-left: 1.5rem; + } + + li { + margin: 0.25rem 0; + } + + a { + color: #667eea; + text-decoration: none; + } + + a:hover { + text-decoration: underline; + } + .typing { display: flex; gap: 4px; - padding: 1rem 1.25rem; + padding: 0.5rem; } .dot { @@ -297,4 +458,4 @@ opacity: 0.5; cursor: not-allowed; } - \ No newline at end of file + diff --git a/src/frontend/src/lib/utils/markdown.ts b/src/frontend/src/lib/utils/markdown.ts new file mode 100644 index 0000000..0c72325 --- /dev/null +++ b/src/frontend/src/lib/utils/markdown.ts @@ -0,0 +1,92 @@ +/** + * Simple markdown parser for rendering AI responses + * Supports: bold, italic, code blocks, inline code, links, lists, headings + */ + +interface ParsedSegment { + type: 'text' | 'bold' | 'italic' | 'code' | 'codeBlock' | 'link' | 'list'; + content: string; + items?: string[]; +} + +export function parseMarkdown(text: string): ParsedSegment[] { + const segments: ParsedSegment[] = []; + + // Split by code blocks first (they can contain other markdown) + const codeBlockRegex = /```[\s\S]*?```/g; + const parts = text.split(codeBlockRegex); + const codeBlocks = text.match(codeBlockRegex) || []; + + let partIndex = 0; + + while (partIndex < parts.length) { + const part = parts[partIndex]; + + if (part) { + // Process inline formatting + segments.push(...parseInlineMarkdown(part)); + } + + // Add code block if there's one after this part + if (partIndex < codeBlocks.length) { + const codeContent = codeBlocks[partIndex].replace(/^```\w*\n?/, '').replace(/```$/, ''); + segments.push({ type: 'codeBlock', content: codeContent }); + } + + partIndex++; + } + + return segments; +} + +function parseInlineMarkdown(text: string): ParsedSegment[] { + const segments: ParsedSegment[] = []; + + // Combined regex for bold, italic, inline code, links + const inlineRegex = /(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`|\\\[.*?\]\(.*?\))/g; + const parts = text.split(inlineRegex); + + for (const part of parts) { + if (!part) continue; + + if (part.startsWith('**') && part.endsWith('**')) { + segments.push({ type: 'bold', content: part.slice(2, -2) }); + } else if (part.startsWith('*') && part.endsWith('*')) { + segments.push({ type: 'italic', content: part.slice(1, -1) }); + } else if (part.startsWith('`') && part.endsWith('`')) { + segments.push({ type: 'code', content: part.slice(1, -1) }); + } else if (part.startsWith('[') && part.includes('](')) { + const linkMatch = part.match(/\[(.*?)\]\((.*?)\)/); + if (linkMatch) { + segments.push({ type: 'link', content: linkMatch[1] }); + } + } else if (part.includes('\n')) { + // Handle newlines and lists + const lines = part.split('\n'); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.match(/^[\-\*]\s/)) { + // List item + if (segments.length > 0 && segments[segments.length - 1].type === 'list') { + segments[segments.length - 1].items?.push(line.slice(2)); + } else { + segments.push({ type: 'list', content: '', items: [line.slice(2)] }); + } + } else if (line.match(/^#{1,6}\s/)) { + // Heading + segments.push({ type: 'text', content: line }); + } else if (line) { + if (i > 0) { + segments.push({ type: 'text', content: '\n' + line }); + } else { + segments.push({ type: 'text', content: line }); + } + } + } + } else { + segments.push({ type: 'text', content: part }); + } + } + + return segments; +} diff --git a/src/frontend/src/routes/bot/[id]/+page.svelte b/src/frontend/src/routes/bot/[id]/+page.svelte index 0193e04..bc759ad 100644 --- a/src/frontend/src/routes/bot/[id]/+page.svelte +++ b/src/frontend/src/routes/bot/[id]/+page.svelte @@ -108,8 +108,8 @@