|
1 | 1 | import React, { useEffect, useRef, useState } from "react"; |
2 | 2 | import Image from "next/image"; |
3 | 3 | import Logo from "../../../public/logo.png"; |
4 | | -import CodeDisplayBlock from "../code-display-block"; |
5 | | -import Markdown from "react-markdown"; |
6 | | -import remarkGfm from "remark-gfm"; |
7 | | -import remarkMath from "remark-math"; |
8 | | -import rehypeKatex from "rehype-katex"; |
9 | | -import "katex/dist/katex.min.css"; |
10 | 4 | import { Message } from "ai"; |
11 | | -import ThinkBlock from "./think-block"; |
12 | 5 | import { cn } from "@/lib/utils"; |
13 | 6 | import { ArrowDownIcon } from "@radix-ui/react-icons"; |
14 | 7 | import { Button } from "../ui/button"; |
| 8 | +import ChatMessage from "./chat-message"; // Import the new component |
15 | 9 |
|
16 | 10 | interface ChatListProps { |
17 | 11 | messages: Message[]; |
18 | 12 | isLoading: boolean; |
19 | 13 | } |
20 | 14 |
|
21 | | -const parseMessageContent = ( |
22 | | - content: string, |
23 | | - isLastMessage: boolean, |
24 | | - isLoading: boolean |
25 | | -) => { |
26 | | - let thinkMatch = /<think>([\s\S]*?)(?:<\/think>|$)/i.exec(content); |
27 | | - if (!thinkMatch && content.includes("</think>")) { |
28 | | - thinkMatch = /^([\s\S]*?)(?:<\/think>)/i.exec(content); |
29 | | - } |
30 | | - |
31 | | - let thinkContent = null; |
32 | | - let mainContent = content; |
33 | | - |
34 | | - if (thinkMatch) { |
35 | | - thinkContent = thinkMatch[1].trim(); |
36 | | - mainContent = content.replace(thinkMatch[0], "").trim(); |
37 | | - mainContent = mainContent.replace(/<\/think>/gi, "").trim(); |
38 | | - } |
39 | | - |
40 | | - const isThinkingLive = |
41 | | - isLoading && |
42 | | - isLastMessage && |
43 | | - (content.includes("<think>") || !content.includes("</think>")) && |
44 | | - !content.includes("</think>"); |
45 | | - |
46 | | - return { thinkContent, mainContent, isThinkingLive }; |
47 | | -}; |
48 | | - |
49 | 15 | export default function ChatList({ messages, isLoading }: ChatListProps) { |
50 | 16 | const scrollRef = useRef<HTMLDivElement>(null); |
51 | 17 | const bottomRef = useRef<HTMLDivElement>(null); |
@@ -75,6 +41,7 @@ export default function ChatList({ messages, isLoading }: ChatListProps) { |
75 | 41 | if (messages.length > 0) { |
76 | 42 | const lastMessage = messages[messages.length - 1]; |
77 | 43 |
|
| 44 | + // Auto-scroll if we are already at the bottom OR if the last message is from the user |
78 | 45 | if (isAtBottom || lastMessage.role === "user") { |
79 | 46 | bottomRef.current?.scrollIntoView({ behavior: "auto", block: "end" }); |
80 | 47 | } |
@@ -112,113 +79,16 @@ export default function ChatList({ messages, isLoading }: ChatListProps) { |
112 | 79 | {messages |
113 | 80 | .filter((message) => message.role !== "system") |
114 | 81 | .map((message, index) => { |
| 82 | + // We pass "isLast" so the component knows if it should be watching for updates |
115 | 83 | const isLastMessage = index === messages.length - 1; |
116 | | - const { thinkContent, mainContent, isThinkingLive } = |
117 | | - parseMessageContent(message.content, isLastMessage, isLoading); |
118 | | - |
119 | | - const isUser = message.role === "user"; |
120 | | - |
| 84 | + |
121 | 85 | return ( |
122 | | - <div |
123 | | - key={index} |
124 | | - className={cn( |
125 | | - "flex w-full", |
126 | | - isUser ? "justify-end" : "justify-start" |
127 | | - )} |
128 | | - > |
129 | | - <div |
130 | | - className={cn( |
131 | | - "flex flex-col", |
132 | | - // Added min-w-0 to prevent flex item from growing beyond parent |
133 | | - isUser ? "items-end max-w-[85%] md:max-w-[75%] min-w-0" : "w-full items-start min-w-0" |
134 | | - )} |
135 | | - > |
136 | | - <div |
137 | | - className={cn( |
138 | | - "relative text-[15px] leading-relaxed", |
139 | | - isUser |
140 | | - ? "px-5 py-3 rounded-2xl rounded-tr-sm bg-blue-600 text-white dark:bg-zinc-800 dark:text-foreground shadow-sm break-words" // Added break-words |
141 | | - : "w-full px-0 py-0" |
142 | | - )} |
143 | | - > |
144 | | - {!isUser && thinkContent && ( |
145 | | - <ThinkBlock |
146 | | - content={thinkContent} |
147 | | - isLive={isThinkingLive} |
148 | | - /> |
149 | | - )} |
150 | | - |
151 | | - <div className={cn(!isUser && thinkContent && "mt-4")}> |
152 | | - <Markdown |
153 | | - remarkPlugins={[remarkGfm, remarkMath]} |
154 | | - rehypePlugins={[rehypeKatex]} |
155 | | - components={{ |
156 | | - code({ node, inline, className, children, ...props }: any) { |
157 | | - const match = /language-(\w+)/.exec(className || ""); |
158 | | - const lang = match ? match[1] : ""; |
159 | | - return !inline && match ? ( |
160 | | - <div className="my-6 rounded-md overflow-hidden border border-border"> |
161 | | - <CodeDisplayBlock |
162 | | - code={String(children).replace(/\n$/, "")} |
163 | | - lang={lang} |
164 | | - /> |
165 | | - </div> |
166 | | - ) : ( |
167 | | - <code |
168 | | - className={cn( |
169 | | - "px-1.5 py-0.5 rounded-md font-mono text-[13px] border break-all", // break-all for inline code |
170 | | - isUser |
171 | | - ? "bg-white/20 text-white border-transparent" |
172 | | - : "bg-muted text-foreground border-border" |
173 | | - )} |
174 | | - {...props} |
175 | | - > |
176 | | - {children} |
177 | | - </code> |
178 | | - ); |
179 | | - }, |
180 | | - h1: ({children}) => <h1 className="text-3xl font-bold mt-8 mb-4 break-words">{children}</h1>, |
181 | | - h2: ({children}) => <h2 className="text-2xl font-semibold mt-8 mb-4 border-b pb-2 break-words">{children}</h2>, |
182 | | - h3: ({children}) => <h3 className="text-xl font-semibold mt-6 mb-3 break-words">{children}</h3>, |
183 | | - h4: ({children}) => <h4 className="text-lg font-semibold mt-6 mb-3 break-words">{children}</h4>, |
184 | | - |
185 | | - // Paragraphs: whitespace-pre-wrap ensures formatting is kept, break-words handles long strings |
186 | | - p: ({children}) => <p className="mb-5 last:mb-0 leading-7 whitespace-pre-wrap break-words">{children}</p>, |
187 | | - |
188 | | - a: ({children, ...props}: any) => ( |
189 | | - <a className="text-blue-500 hover:underline cursor-pointer font-medium break-all" {...props}>{children}</a> |
190 | | - ), |
191 | | - ul: ({children}) => <ul className="list-disc pl-6 mb-5 space-y-2">{children}</ul>, |
192 | | - ol: ({children}) => <ol className="list-decimal pl-6 mb-5 space-y-2">{children}</ol>, |
193 | | - li: ({children}) => <li className="pl-1 leading-7">{children}</li>, |
194 | | - blockquote: ({children}) => ( |
195 | | - <blockquote className="border-l-4 border-primary/20 bg-muted/40 pl-4 py-2 my-4 rounded-r-md italic"> |
196 | | - {children} |
197 | | - </blockquote> |
198 | | - ), |
199 | | - table: ({children}) => <div className="overflow-x-auto my-6 border rounded-md"><table className="w-full text-sm text-left">{children}</table></div>, |
200 | | - th: ({children}) => <th className="bg-muted px-4 py-3 border-b font-semibold">{children}</th>, |
201 | | - td: ({children}) => <td className="px-4 py-3 border-b last:border-0">{children}</td>, |
202 | | - }} |
203 | | - > |
204 | | - {isUser ? message.content : mainContent} |
205 | | - </Markdown> |
206 | | - </div> |
207 | | - |
208 | | - {isLoading && |
209 | | - isLastMessage && |
210 | | - !isUser && |
211 | | - !isThinkingLive && |
212 | | - mainContent.length === 0 && ( |
213 | | - <div className="flex items-center gap-1 h-6 mt-2 opacity-50"> |
214 | | - <span className="w-1.5 h-1.5 bg-foreground rounded-full animate-bounce [animation-delay:-0.3s]"></span> |
215 | | - <span className="w-1.5 h-1.5 bg-foreground rounded-full animate-bounce [animation-delay:-0.15s]"></span> |
216 | | - <span className="w-1.5 h-1.5 bg-foreground rounded-full animate-bounce"></span> |
217 | | - </div> |
218 | | - )} |
219 | | - </div> |
220 | | - </div> |
221 | | - </div> |
| 86 | + <ChatMessage |
| 87 | + key={index} // Ideally use message.id if available, but index works for simple lists |
| 88 | + message={message} |
| 89 | + isLast={isLastMessage} |
| 90 | + isLoading={isLoading} |
| 91 | + /> |
222 | 92 | ); |
223 | 93 | })} |
224 | 94 | </div> |
|
0 commit comments