1- import React , { useCallback , useEffect , useRef , useState } from 'react' ;
1+ import React , { lazy , useCallback , useEffect , useRef , useState } from 'react' ;
22import { useQuery } from '@tanstack/react-query' ;
33import { ChevronDown , ChevronUp } from 'lucide-react' ;
4- import Markdown from 'react-markdown' ;
54import { useParams } from 'react-router' ;
65
7- import AiThinking from '@/components/AiThinking' ;
86import { Avatar , AvatarImage } from '@/components/ui/avatar' ;
97import { useChatStore } from '@/hooks/useChat' ;
108import { API } from '@/lib/api' ;
9+ import { ComponentLoader } from '../ComponentLoader' ;
10+
11+ const Markdown = lazy ( ( ) => import ( 'react-markdown' ) ) ;
12+ const AiThinking = lazy ( ( ) => import ( '@/components/AiThinking' ) ) ;
1113
1214const MemoMarkdown = React . memo ( Markdown ) ;
1315
@@ -39,16 +41,18 @@ export default function ChatScreen() {
3941 const {
4042 data : conversationData ,
4143 error,
44+ isLoading,
4245 isError,
4346 } = useQuery ( {
4447 queryFn : ( ) => API . conversation . getConversationId ( conversationId ! ) ,
4548 queryKey : [ 'conversation' , conversationId ] ,
4649 refetchOnWindowFocus : false ,
4750 enabled : ! ! conversationId ,
51+ staleTime : Infinity ,
4852 } ) ;
4953
5054 useEffect ( ( ) => {
51- if ( conversationData ?. messages ) {
55+ if ( conversationData ?. messages && ! messages . some ( ( e ) => e . isTemporary ) ) {
5256 setMessages ( ( prev ) => {
5357 const newMessages = conversationData . messages ?? [ ] ;
5458 const existingMessageIds = new Set ( prev . map ( ( msg ) => msg . id ) ) ;
@@ -63,6 +67,7 @@ export default function ChatScreen() {
6367 return prev ;
6468 } ) ;
6569 }
70+ // eslint-disable-next-line react-hooks/exhaustive-deps
6671 } , [ conversationData ?. messages , setMessages ] ) ;
6772
6873 // Efek untuk mengatur isThinkingOpen pada pesan AI terbaru
@@ -114,122 +119,128 @@ export default function ChatScreen() {
114119 { isError && < div > { error . message } </ div > }
115120
116121 < div className = "mx-auto w-full max-w-3xl px-4 pt-4" >
117- { messages . map ( ( { id, role, content } ) => {
118- const isUser = role === 'human' ;
119-
120- function extractThinkContentFromStream ( content : string ) {
121- let thinkContent = '' ; // Content inside <think>...</think>
122- let cleanContent = '' ; // Content outside <think>...</think>
123-
124- // Find the start and end of the <think> block
125- const startThinkTagIndex = content . indexOf ( '<think>' ) ;
126- const endThinkTagIndex = content . indexOf ( '</think>' ) ;
127-
128- if ( startThinkTagIndex === - 1 ) {
129- // No <think> tag found, all content is clean
130- cleanContent = content . trim ( ) ;
131- } else if ( endThinkTagIndex === - 1 ) {
132- // <think> tag found but no </think>, treat everything after <think> as thinkContent
133- cleanContent = content . slice ( 0 , startThinkTagIndex ) . trim ( ) ;
134- thinkContent = content . slice ( startThinkTagIndex + 7 ) . trim ( ) ;
135- } else {
136- // Both <think> and </think> tags found, split accordingly
137- cleanContent =
138- content . slice ( 0 , startThinkTagIndex ) . trim ( ) +
139- ' ' +
140- content . slice ( endThinkTagIndex + 8 ) . trim ( ) ;
141- thinkContent = content
142- . slice ( startThinkTagIndex + 7 , endThinkTagIndex )
143- . trim ( ) ;
122+ { isLoading && ! messages . map ( ( e ) => e . isTemporary ) . includes ( true ) ? (
123+ < ComponentLoader />
124+ ) : (
125+ messages . map ( ( { id, role, content } ) => {
126+ const isUser = role === 'human' ;
127+
128+ function extractThinkContentFromStream ( content : string ) {
129+ let thinkContent = '' ; // Content inside <think>...</think>
130+ let cleanContent = '' ; // Content outside <think>...</think>
131+
132+ // Find the start and end of the <think> block
133+ const startThinkTagIndex = content . indexOf ( '<think>' ) ;
134+ const endThinkTagIndex = content . indexOf ( '</think>' ) ;
135+
136+ if ( startThinkTagIndex === - 1 ) {
137+ // No <think> tag found, all content is clean
138+ cleanContent = content . trim ( ) ;
139+ } else if ( endThinkTagIndex === - 1 ) {
140+ // <think> tag found but no </think>, treat everything after <think> as thinkContent
141+ cleanContent = content . slice ( 0 , startThinkTagIndex ) . trim ( ) ;
142+ thinkContent = content . slice ( startThinkTagIndex + 7 ) . trim ( ) ;
143+ } else {
144+ // Both <think> and </think> tags found, split accordingly
145+ cleanContent =
146+ content . slice ( 0 , startThinkTagIndex ) . trim ( ) +
147+ ' ' +
148+ content . slice ( endThinkTagIndex + 8 ) . trim ( ) ;
149+ thinkContent = content
150+ . slice ( startThinkTagIndex + 7 , endThinkTagIndex )
151+ . trim ( ) ;
152+ }
153+
154+ return {
155+ thinkContent : thinkContent ,
156+ cleanContent : cleanContent . trim ( ) ,
157+ } ;
144158 }
145159
146- return {
147- thinkContent : thinkContent ,
148- cleanContent : cleanContent . trim ( ) ,
149- } ;
150- }
151-
152- const { thinkContent, cleanContent } =
153- extractThinkContentFromStream ( content ) ;
154-
155- return (
156- < div
157- key = { id }
158- className = { `mb-4 flex items-start gap-3 ${ isUser ? 'flex-row-reverse' : 'flex-row' } ` } >
159- { isUser ? (
160- < Avatar className = "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full" >
161- < AvatarImage src = { 'https://avatar.vercel.sh/human' } />
162- </ Avatar >
163- ) : (
164- < div className = "bg-muted relative flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded-full" >
165- < svg
166- xmlns = "http://www.w3.org/2000/svg"
167- width = "24"
168- height = "24"
169- viewBox = "0 0 24 24"
170- fill = "none"
171- stroke = "currentColor"
172- strokeWidth = "2"
173- strokeLinecap = "round"
174- strokeLinejoin = "round"
175- className = "lucide lucide-bot" >
176- < path d = "M12 8V4H8" />
177- < rect width = "16" height = "12" x = "4" y = "8" rx = "2" />
178- < path d = "M2 14h2" />
179- < path d = "M20 14h2" />
180- < path d = "M15 13v2" />
181- < path d = "M9 13v2" />
182- </ svg >
183- </ div >
184- ) }
185-
186- { /* AI Thinking loading */ }
187- { ! isUser && content . trim ( ) === '' ? (
188- < AiThinking />
189- ) : isUser ? (
190- < div
191- className = { `prose dark:prose-invert bg-accent text-accent-foreground min-w-0 max-w-none rounded-lg px-4 py-2 shadow-md` } >
192- { cleanContent && < MemoMarkdown > { cleanContent } </ MemoMarkdown > }
193- </ div >
194- ) : (
195- < div className = "min-w-0 max-w-none" >
196- { thinkContent && (
197- < div className = "mb-2 w-full" >
198- { /* Toggle Button */ }
199- < button
200- onClick = { ( ) => toggleThinking ( id ) }
201- className = "bg-muted flex items-center gap-1 rounded-full p-2 text-xs font-medium" >
202- < span > AI Thought</ span >
203- { isThinkingOpen [ id ] ? (
204- < ChevronUp className = "h-4 w-4" />
205- ) : (
206- < ChevronDown className = "h-4 w-4" />
207- ) }
208- </ button >
209-
210- { /* AI Thought Process (Collapsible) */ }
211- < div
212- className = { `text-muted-foreground prose dark:prose-invert custom-scrollbar min-w-0 max-w-none overflow-hidden overflow-y-auto p-2 text-sm backdrop-blur-md transition-all ${
213- isThinkingOpen [ id ]
214- ? 'max-h-[1000px] opacity-100'
215- : 'hidden max-h-0 opacity-0'
216- } `} >
217- < MemoMarkdown > { thinkContent } </ MemoMarkdown >
218- </ div >
219- </ div >
220- ) }
160+ const { thinkContent, cleanContent } =
161+ extractThinkContentFromStream ( content ) ;
162+
163+ return (
164+ < div
165+ key = { id }
166+ className = { `mb-4 flex items-start gap-3 ${ isUser ? 'flex-row-reverse' : 'flex-row' } ` } >
167+ { isUser ? (
168+ < Avatar className = "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full" >
169+ < AvatarImage src = { 'https://avatar.vercel.sh/human' } />
170+ </ Avatar >
171+ ) : (
172+ < div className = "bg-muted relative flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded-full" >
173+ < svg
174+ xmlns = "http://www.w3.org/2000/svg"
175+ width = "24"
176+ height = "24"
177+ viewBox = "0 0 24 24"
178+ fill = "none"
179+ stroke = "currentColor"
180+ strokeWidth = "2"
181+ strokeLinecap = "round"
182+ strokeLinejoin = "round"
183+ className = "lucide lucide-bot" >
184+ < path d = "M12 8V4H8" />
185+ < rect width = "16" height = "12" x = "4" y = "8" rx = "2" />
186+ < path d = "M2 14h2" />
187+ < path d = "M20 14h2" />
188+ < path d = "M15 13v2" />
189+ < path d = "M9 13v2" />
190+ </ svg >
191+ </ div >
192+ ) }
221193
194+ { /* AI Thinking loading */ }
195+ { ! isUser && content . trim ( ) === '' ? (
196+ < AiThinking />
197+ ) : isUser ? (
222198 < div
223- className = { `prose dark:prose-invert bg-muted min-w-0 max-w-none rounded-lg px-4 py-2 shadow-md` } >
199+ className = { `prose dark:prose-invert bg-accent text-accent-foreground min-w-0 max-w-none rounded-lg px-4 py-2 shadow-md` } >
224200 { cleanContent && (
225201 < MemoMarkdown > { cleanContent } </ MemoMarkdown >
226202 ) }
227203 </ div >
228- </ div >
229- ) }
230- </ div >
231- ) ;
232- } ) }
204+ ) : (
205+ < div className = "min-w-0 max-w-none" >
206+ { thinkContent && (
207+ < div className = "mb-2 w-full" >
208+ { /* Toggle Button */ }
209+ < button
210+ onClick = { ( ) => toggleThinking ( id ) }
211+ className = "bg-muted flex items-center gap-1 rounded-full p-2 text-xs font-medium" >
212+ < span > AI Thought</ span >
213+ { isThinkingOpen [ id ] ? (
214+ < ChevronUp className = "h-4 w-4" />
215+ ) : (
216+ < ChevronDown className = "h-4 w-4" />
217+ ) }
218+ </ button >
219+
220+ { /* AI Thought Process (Collapsible) */ }
221+ < div
222+ className = { `text-muted-foreground prose dark:prose-invert custom-scrollbar min-w-0 max-w-none overflow-hidden overflow-y-auto p-2 text-sm backdrop-blur-md transition-all ${
223+ isThinkingOpen [ id ]
224+ ? 'max-h-[1000px] opacity-100'
225+ : 'hidden max-h-0 opacity-0'
226+ } `} >
227+ < MemoMarkdown > { thinkContent } </ MemoMarkdown >
228+ </ div >
229+ </ div >
230+ ) }
231+
232+ < div
233+ className = { `prose dark:prose-invert bg-muted min-w-0 max-w-none rounded-lg px-4 py-2 shadow-md` } >
234+ { cleanContent && (
235+ < MemoMarkdown > { cleanContent } </ MemoMarkdown >
236+ ) }
237+ </ div >
238+ </ div >
239+ ) }
240+ </ div >
241+ ) ;
242+ } )
243+ ) }
233244 < div ref = { messagesEndRef } />
234245 </ div >
235246 </ div >
0 commit comments