diff --git a/app-frontend/react/src/components/Chat_Assistant/ChatAssistant.tsx b/app-frontend/react/src/components/Chat_Assistant/ChatAssistant.tsx index 9f00433..44a47b0 100644 --- a/app-frontend/react/src/components/Chat_Assistant/ChatAssistant.tsx +++ b/app-frontend/react/src/components/Chat_Assistant/ChatAssistant.tsx @@ -148,7 +148,10 @@ const ChatAssistant: React.FC = ({ } else { return ( - + ); } diff --git a/app-frontend/react/src/components/Chat_Markdown/ChatMarkdown.tsx b/app-frontend/react/src/components/Chat_Markdown/ChatMarkdown.tsx index 464320e..e3ef18e 100644 --- a/app-frontend/react/src/components/Chat_Markdown/ChatMarkdown.tsx +++ b/app-frontend/react/src/components/Chat_Markdown/ChatMarkdown.tsx @@ -5,7 +5,7 @@ import remarkGfm from "remark-gfm"; import remarkFrontmatter from "remark-frontmatter"; import remarkBreaks from "remark-breaks"; import ThinkCard from "./ThinkRender/ThinkCard"; -import { Button, Collapse, Box } from "@mui/material"; +import { Button, Collapse, Box, CircularProgress, Typography } from "@mui/material"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import ExpandLessIcon from "@mui/icons-material/ExpandLess"; @@ -14,24 +14,158 @@ import CodeRender from "./CodeRender/CodeRender"; type MarkdownProps = { content: string; + isStreaming?: boolean; }; const extractThinkBlocks = (markdown: string): { cleaned: string; thinks: string[] } => { - const thinkRegex = /([\s\S]*?)<\/think>/g; const thinks: string[] = []; - let cleaned = markdown; - let match; + let text = markdown; - while ((match = thinkRegex.exec(markdown)) !== null) { - thinks.push(match[1].trim()); + // Extract JSON tool responses first + const toolResponseRegex = /\{"tool_name":\s*"[^"]+",\s*"tool_content":\s*\[[\s\S]*?\]\}/g; + let toolMatch; + while ((toolMatch = toolResponseRegex.exec(text)) !== null) { + try { + const toolResponse = JSON.parse(toolMatch[0]); + if (toolResponse.tool_content && Array.isArray(toolResponse.tool_content)) { + const toolContent = toolResponse.tool_content.join('\n'); + thinks.push(`**Tool: ${toolResponse.tool_name}**\n\n${toolContent}`); + } + } catch (e) { + thinks.push(toolMatch[0]); + } } + text = text.replace(toolResponseRegex, ""); - cleaned = markdown.replace(thinkRegex, "").trim(); + // Handle edge case where content appears twice on the same line with artifacts + // Pattern: content"}content or similar + const duplicatePattern = /^(.+?)["}\]]*<\/think>(.+)$/; + const duplicateMatch = text.match(duplicatePattern); + + if (duplicateMatch) { + const [, beforeThink, afterThink] = duplicateMatch; + + // If the content before and after is similar/identical, just return the cleaner version + const cleanBefore = beforeThink.trim().replace(/[{}"\]]+$/, ''); + const cleanAfter = afterThink.trim(); + + // If they're the same or very similar, just return the after version + if (cleanBefore === cleanAfter || cleanAfter.includes(cleanBefore)) { + return { cleaned: cleanAfter, thinks: [] }; + } + } + + // More aggressive approach for specific patterns + const specificPatterns = [ + /The Chinook database contains a total of \d+ employees\.$/ + ]; + + for (const pattern of specificPatterns) { + const lastOccurrenceMatch = text.match(new RegExp(`.*${pattern.source}`)); + + if (lastOccurrenceMatch) { + const fullMatch = lastOccurrenceMatch[0]; + const finalSentenceMatch = fullMatch.match(pattern); + + if (finalSentenceMatch) { + const finalAnswer = finalSentenceMatch[0]; + const beforeFinalAnswer = text.substring(0, text.lastIndexOf(finalAnswer)); + + // Only add to thinks if there's meaningful content after cleaning + if (beforeFinalAnswer.trim().length > 0) { + let thinkContent = beforeFinalAnswer; + + // Extract complete think blocks + const completeThinkRegex = /([\s\S]*?)<\/think>/g; + let thinkMatch; + while ((thinkMatch = completeThinkRegex.exec(thinkContent)) !== null) { + thinks.push(thinkMatch[1].trim()); + } + thinkContent = thinkContent.replace(completeThinkRegex, ''); + + // Handle unclosed think blocks + const unClosedThinkMatch = thinkContent.match(/([\s\S]*)$/); + if (unClosedThinkMatch) { + thinks.push(unClosedThinkMatch[1].trim()); + thinkContent = thinkContent.replace(unClosedThinkMatch[0], ''); + } + + // Clean up any remaining content that might be leftover artifacts + let remaining = thinkContent + .replace(/<\/?think>/g, '') // Remove any remaining think tags + .replace(/[{}"\]]+/g, ' ') // Remove JSON artifacts + .replace(/\s*}\s*$/g, '') // Remove trailing } + .replace(/\s+/g, ' ') + .trim(); + + // If the remaining content is just a duplicate of the final answer, don't include it + if (remaining && remaining !== finalAnswer && remaining.length > 0) { + thinks.push(remaining); + } + } + + return { cleaned: finalAnswer, thinks }; + } + } + } + + // Fallback: use the previous logic if no final answer pattern is found + const finalAnswerPatterns = [ + /^([\s\S]*?)(\s*The .+ contains a total of \d+ .+\.\s*)$/, + /^([\s\S]*?)(\s*The .+ (is|are) .+\.\s*)$/, + /^([\s\S]*?)(\s*There (is|are) .+\.\s*)$/ + ]; + + for (const pattern of finalAnswerPatterns) { + const finalAnswerMatch = text.match(pattern); + + if (finalAnswerMatch) { + const beforeFinalAnswer = finalAnswerMatch[1]; + const finalAnswer = finalAnswerMatch[2].trim(); + + let thinkContent = beforeFinalAnswer; + + const completeThinkRegex = /([\s\S]*?)<\/think>/g; + let thinkMatch; + while ((thinkMatch = completeThinkRegex.exec(thinkContent)) !== null) { + thinks.push(thinkMatch[1].trim()); + } + thinkContent = thinkContent.replace(completeThinkRegex, ''); + + const unClosedThinkMatch = thinkContent.match(/([\s\S]*)$/); + if (unClosedThinkMatch) { + thinks.push(unClosedThinkMatch[1].trim()); + thinkContent = thinkContent.replace(unClosedThinkMatch[0], ''); + } + + const remaining = thinkContent.replace(/\s+/g, ' ').trim(); + if (remaining && remaining.length > 0) { + thinks.push(remaining); + } + + return { cleaned: finalAnswer, thinks }; + } + } + + // Final fallback: process normally + const completeThinkRegex = /([\s\S]*?)<\/think>/g; + let thinkMatch; + while ((thinkMatch = completeThinkRegex.exec(text)) !== null) { + thinks.push(thinkMatch[1].trim()); + } + text = text.replace(completeThinkRegex, ''); + + const unClosedThinkRegex = /([\s\S]*)$/; + const unClosedMatch = unClosedThinkRegex.exec(text); + if (unClosedMatch) { + thinks.push(unClosedMatch[1].trim()); + text = text.replace(unClosedMatch[0], ''); + } - return { cleaned, thinks }; + return { cleaned: text.trim(), thinks }; }; -const ChatMarkdown = ({ content }: MarkdownProps) => { +const ChatMarkdown = ({ content, isStreaming = false }: MarkdownProps) => { useEffect(() => { import("./CodeRender/CodeRender"); }, []); @@ -40,6 +174,105 @@ const ChatMarkdown = ({ content }: MarkdownProps) => { content.replace(/\\\\n/g, "\n").replace(/\\n/g, "\n") ); + // Safety net: if is leaked in the cleaned content, remove everything before it + const safeCleanedContent = (text: string): string => { + const thinkEndIndex = text.lastIndexOf(''); + if (thinkEndIndex !== -1) { + // Return everything after the last tag + return text.substring(thinkEndIndex + 8).trim(); + } + return text; + }; + + const finalCleanedContent = safeCleanedContent(cleaned); + + // Handle different display states based on streaming and content + const getDisplayComponent = () => { + const hasContent = finalCleanedContent.trim().length > 0; + + if (hasContent) { + // Show content if available + return ( + { + const hasBlockElement = React.Children.toArray(children).some( + (child) => + React.isValidElement(child) && + typeof child.type === "string" && + ["div", "h1", "h2", "h3", "ul", "ol", "table"].includes(child.type) + ); + return hasBlockElement ? ( + <>{children} + ) : ( +

+ {children} +

+ ); + }, + a: ({ children, ...props }) => ( + //@ts-ignore + + {children} + + ), + table: ({ children, ...props }) => ( +
+ {children}
+
+ ), + code({ inline, className, children }) { + const lang = /language-(\w+)/.exec(className || ""); + return ( + Loading Code Block...}> + {/*@ts-ignore*/} + + + ); + }, + }} + /> + ); + } else if (isStreaming) { + // Show spinner when streaming with no content + return ( + + + + Generating response... + + + ); + } else { + // Show fallback message when streaming ended with no content + return ( + + ); + } + }; + const [showThinks, setShowThinks] = useState(false); return ( @@ -64,63 +297,25 @@ const ChatMarkdown = ({ content }: MarkdownProps) => { - {thinks.map((block, idx) => ( - - ))} + {thinks + .filter((block) => { + // Filter out blocks that would be empty after ThinkCard's cleaning + const thinkEndIndex = block.lastIndexOf('
'); + const cleanedBlock = thinkEndIndex !== -1 + ? block.substring(thinkEndIndex + 8).trim() + : block.trim(); + return cleanedBlock.length > 0; + }) + .map((block, idx) => ( + + )) + } )} - { - const hasBlockElement = React.Children.toArray(children).some( - (child) => - React.isValidElement(child) && - typeof child.type === "string" && - ["div", "h1", "h2", "h3", "ul", "ol", "table"].includes(child.type) - ); - return hasBlockElement ? ( - <>{children} - ) : ( -

- {children} -

- ); - }, - a: ({ children, ...props }) => ( - //@ts-ignore - - {children} - - ), - table: ({ children, ...props }) => ( -
- {children}
-
- ), - code({ inline, className, children }) { - const lang = /language-(\w+)/.exec(className || ""); - return ( - Loading Code Block...}> - {/*@ts-ignore*/} - - - ); - }, - }} - /> + {getDisplayComponent()} ); }; diff --git a/app-frontend/react/src/components/Chat_Markdown/ThinkRender/ThinkCard.tsx b/app-frontend/react/src/components/Chat_Markdown/ThinkRender/ThinkCard.tsx index 74db261..198f4f8 100644 --- a/app-frontend/react/src/components/Chat_Markdown/ThinkRender/ThinkCard.tsx +++ b/app-frontend/react/src/components/Chat_Markdown/ThinkRender/ThinkCard.tsx @@ -6,6 +6,23 @@ type ThinkCardProps = { }; const ThinkCard = ({ content }: ThinkCardProps) => { + // Safety net: if
is leaked, do not display the text before it + const cleanContent = (text: string): string => { + const thinkEndIndex = text.lastIndexOf('
'); + if (thinkEndIndex !== -1) { + // Return everything after the last tag + return text.substring(thinkEndIndex + 8).trim(); + } + return text; + }; + + const safeContent = cleanContent(content); + + // Don't render the card if there's no content after filtering + if (!safeContent.trim()) { + return null; + } + return ( { > - {content} + {safeContent} diff --git a/app-frontend/react/src/components/Chat_User/ChatUser.tsx b/app-frontend/react/src/components/Chat_User/ChatUser.tsx index 8f08436..11a92a8 100644 --- a/app-frontend/react/src/components/Chat_User/ChatUser.tsx +++ b/app-frontend/react/src/components/Chat_User/ChatUser.tsx @@ -30,7 +30,7 @@ const ChatUser: React.FC = ({ content }) => { return (
- + {/* diff --git a/setup-scripts/build-image-to-registry/buildpush-genaicomps-images.yml b/setup-scripts/build-image-to-registry/buildpush-genaicomps-images.yml index 8020f76..55207b0 100755 --- a/setup-scripts/build-image-to-registry/buildpush-genaicomps-images.yml +++ b/setup-scripts/build-image-to-registry/buildpush-genaicomps-images.yml @@ -45,4 +45,4 @@ - name: Push image command: docker push {{ container_registry }}/{{ item.name }}:{{ container_tag }} - loop: "{{ genaicomp_images }}" \ No newline at end of file + loop: "{{ genaicomp_images }}"