diff --git a/sim/app/chat/[subdomain]/components/chat-client.tsx b/sim/app/chat/[subdomain]/components/chat-client.tsx
index 9075f52262..50f80d8010 100644
--- a/sim/app/chat/[subdomain]/components/chat-client.tsx
+++ b/sim/app/chat/[subdomain]/components/chat-client.tsx
@@ -1,13 +1,21 @@
'use client'
-import { KeyboardEvent, useEffect, useMemo, useRef, useState } from 'react'
+import {
+ Children,
+ isValidElement,
+ KeyboardEvent,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react'
import { ArrowUp, Loader2, Lock, Mail } from 'lucide-react'
-import ReactMarkdown from 'react-markdown'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { OTPInputForm } from '@/components/ui/input-otp-form'
import { ScrollArea } from '@/components/ui/scroll-area'
import { cn } from '@/lib/utils'
+import MarkdownRenderer from './components/markdown-renderer/markdown-renderer'
// Define message type
interface ChatMessage {
@@ -31,40 +39,6 @@ interface ChatConfig {
authType?: 'public' | 'password' | 'email'
}
-// Markdown renderer component with proper styling
-function MarkdownRenderer({ content }: { content: string }) {
- return (
-
- {content}
-
- )
-}
-
// ChatGPT-style message component
function ClientChatMessage({ message }: { message: ChatMessage }) {
// Check if content is a JSON object
@@ -427,6 +401,9 @@ export default function ChatClient({ subdomain }: { subdomain: string }) {
},
])
+ // Stop showing loading indicator once streaming begins
+ setIsLoading(false)
+
// Ensure the response body exists and is a ReadableStream
const reader = response.body?.getReader()
if (reader) {
@@ -453,7 +430,11 @@ export default function ChatClient({ subdomain }: { subdomain: string }) {
console.log('Message response:', responseData)
// Handle different response formats from API
- if (responseData.multipleOutputs && responseData.contents && Array.isArray(responseData.contents)) {
+ if (
+ responseData.multipleOutputs &&
+ responseData.contents &&
+ Array.isArray(responseData.contents)
+ ) {
// For multiple outputs, create separate assistant messages for each
const assistantMessages = responseData.contents.map((content: any) => {
// Format the content appropriately
@@ -566,17 +547,65 @@ export default function ChatClient({ subdomain }: { subdomain: string }) {
{authRequired === 'password' ? (
-
-
-
setPassword(e.target.value)}
- onKeyDown={handleAuthKeyDown}
- placeholder="Enter password"
- className="pl-10"
- disabled={isAuthenticating}
- />
+
+
+
+
+
Password Required
+
+ Enter the password to access this chat
+
+
+
+
) : (
@@ -704,14 +733,14 @@ export default function ChatClient({ subdomain }: { subdomain: string }) {
@keyframes growShrink {
0%,
100% {
- transform: scale(0.9)
+ transform: scale(0.9);
}
50% {
- transform: scale(1.1)
+ transform: scale(1.1);
}
}
.loading-dot {
- animation: growShrink 1.5s infinite ease-in-out
+ animation: growShrink 1.5s infinite ease-in-out;
}
`}
diff --git a/sim/app/chat/[subdomain]/components/components/markdown-renderer/markdown-renderer.tsx b/sim/app/chat/[subdomain]/components/components/markdown-renderer/markdown-renderer.tsx
new file mode 100644
index 0000000000..9cbfbf4acf
--- /dev/null
+++ b/sim/app/chat/[subdomain]/components/components/markdown-renderer/markdown-renderer.tsx
@@ -0,0 +1,153 @@
+import ReactMarkdown from 'react-markdown'
+
+export default function MarkdownRenderer({ content }: { content: string }) {
+ const customComponents = {
+ // Paragraph
+ p: ({ children }: React.HTMLAttributes
) => (
+ {children}
+ ),
+
+ // Headings
+ h1: ({ children }: React.HTMLAttributes) => (
+ {children}
+ ),
+ h2: ({ children }: React.HTMLAttributes) => (
+ {children}
+ ),
+ h3: ({ children }: React.HTMLAttributes) => (
+ {children}
+ ),
+ h4: ({ children }: React.HTMLAttributes) => (
+ {children}
+ ),
+
+ // Lists
+ ul: ({ children }: React.HTMLAttributes) => (
+
+ ),
+ ol: ({ children }: React.HTMLAttributes) => (
+ {children}
+ ),
+ li: ({ children }: React.HTMLAttributes) => (
+ {children}
+ ),
+
+ // Code blocks
+ pre: ({ children }: React.HTMLAttributes) => (
+
+ {children}
+
+ ),
+
+ // Inline code
+ code: ({
+ inline,
+ className,
+ children,
+ ...props
+ }: React.HTMLAttributes & { className?: string; inline?: boolean }) => {
+ if (inline) {
+ return (
+
+ {children}
+
+ )
+ }
+
+ // Extract language from className (format: language-xxx)
+ const match = /language-(\w+)/.exec(className || '')
+ const language = match ? match[1] : ''
+
+ return (
+
+ {language && (
+
+ {language}
+
+ )}
+
+ {children}
+
+
+ )
+ },
+
+ // Blockquotes
+ blockquote: ({ children }: React.HTMLAttributes) => (
+
+ {children}
+
+ ),
+
+ // Horizontal rule
+ hr: () =>
,
+
+ // Links
+ a: ({ href, children, ...props }: React.AnchorHTMLAttributes) => (
+
+ {children}
+
+ ),
+
+ // Tables
+ table: ({ children }: React.TableHTMLAttributes) => (
+
+ ),
+ thead: ({ children }: React.HTMLAttributes) => (
+
+ {children}
+
+ ),
+ tbody: ({ children }: React.HTMLAttributes) => (
+
+ {children}
+
+ ),
+ tr: ({ children, ...props }: React.HTMLAttributes) => (
+
+ {children}
+
+ ),
+ th: ({ children }: React.ThHTMLAttributes) => (
+
+ {children}
+ |
+ ),
+ td: ({ children }: React.TdHTMLAttributes) => (
+ {children} |
+ ),
+
+ // Images
+ img: ({ src, alt, ...props }: React.ImgHTMLAttributes) => (
+
+ ),
+ }
+
+ // Process text to clean up unnecessary whitespace and formatting issues
+ const processedContent = content
+ .replace(/\n{2,}/g, '\n\n') // Replace multiple newlines with exactly double newlines
+ .replace(/^(#{1,6})\s+(.+?)\n{2,}/gm, '$1 $2\n') // Reduce space after headings to single newline
+ .trim()
+
+ return (
+
+ {processedContent}
+
+ )
+}