diff --git a/web/screens/Thread/ThreadCenterPanel/SimpleTextMessage/index.tsx b/web/screens/Thread/ThreadCenterPanel/SimpleTextMessage/index.tsx index d84ae57d7..a8d2cdbf2 100644 --- a/web/screens/Thread/ThreadCenterPanel/SimpleTextMessage/index.tsx +++ b/web/screens/Thread/ThreadCenterPanel/SimpleTextMessage/index.tsx @@ -1,16 +1,11 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable @typescript-eslint/naming-convention */ -import React, { useEffect, useState } from 'react' +import React, { memo, useMemo } from 'react' import Markdown from 'react-markdown' -import { - ChatCompletionRole, - ContentType, - MessageStatus, - ThreadMessage, -} from '@janhq/core' +import { ChatCompletionRole, ContentType, ThreadMessage } from '@janhq/core' import { Tooltip } from '@janhq/joi' @@ -50,193 +45,29 @@ import { import { activeThreadAtom } from '@/helpers/atoms/Thread.atom' const SimpleTextMessage: React.FC = (props) => { - let text = '' const isUser = props.role === ChatCompletionRole.User const isSystem = props.role === ChatCompletionRole.System const editMessage = useAtomValue(editMessageAtom) const activeThread = useAtomValue(activeThreadAtom) - if (props.content && props.content.length > 0) { - text = props.content[0]?.text?.value ?? '' - } - - const clipboard = useClipboard({ timeout: 1000 }) - - function extractCodeLines(node: { children: { children: any[] }[] }) { - const codeLines: any[] = [] - - // Helper function to extract text recursively from children - function getTextFromNode(node: { - type: string - value: any - children: any[] - }): string { - if (node.type === 'text') { - return node.value - } else if (node.children) { - return node.children.map(getTextFromNode).join('') - } - return '' - } - - // Traverse each line in the block - node.children[0].children.forEach( - (lineNode: { - type: string - tagName: string - value: any - children: any[] - }) => { - if (lineNode.type === 'element' && lineNode.tagName === 'span') { - const lineContent = getTextFromNode(lineNode) - codeLines.push(lineContent) - } - } - ) - - // Join the lines with newline characters for proper formatting - return codeLines.join('\n') - } - function wrapCodeBlocksWithoutVisit() { - return (tree: { children: any[] }) => { - tree.children = tree.children.map((node) => { - if (node.tagName === 'pre' && node.children[0]?.tagName === 'code') { - const language = node.children[0].properties.className?.[1]?.replace( - 'language-', - '' - ) - - if (extractCodeLines(node) === '') { - return node - } - - return { - type: 'element', - tagName: 'div', - properties: { - className: ['code-block-wrapper'], - }, - children: [ - { - type: 'element', - tagName: 'div', - properties: { - className: [ - 'code-block', - 'group/item', - 'relative', - 'my-4', - 'overflow-auto', - ], - }, - children: [ - { - type: 'element', - tagName: 'div', - properties: { - className: - 'code-header bg-[hsla(var(--app-code-block))] flex justify-between items-center py-2 px-3 code-header--border rounded-t-lg', - }, - children: [ - { - type: 'element', - tagName: 'span', - properties: { - className: 'text-xs font-medium text-gray-300', - }, - children: [ - { - type: 'text', - value: language - ? `${getLanguageFromExtension(language)}` - : '', - }, - ], - }, - { - type: 'element', - tagName: 'button', - properties: { - className: - 'copy-button ml-auto flex items-center gap-1 text-xs font-medium text-gray-400 hover:text-gray-600 focus:outline-none', - onClick: (event: Event) => { - clipboard.copy(extractCodeLines(node)) - - const button = event.currentTarget as HTMLElement - button.innerHTML = ` - - Copied - ` - - setTimeout(() => { - button.innerHTML = ` - - Copy - ` - }, 2000) - }, - }, - children: [ - { - type: 'element', - tagName: 'svg', - properties: { - xmlns: 'http://www.w3.org/2000/svg', - width: '16', - height: '16', - viewBox: '0 0 24 24', - fill: 'none', - stroke: 'currentColor', - strokeWidth: '2', - strokeLinecap: 'round', - strokeLinejoin: 'round', - className: - 'lucide lucide-copy pointer-events-none text-gray-400', - }, - children: [ - { - type: 'element', - tagName: 'rect', - properties: { - width: '14', - height: '14', - x: '8', - y: '8', - rx: '2', - ry: '2', - }, - children: [], - }, - { - type: 'element', - tagName: 'path', - properties: { - d: 'M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2', - }, - children: [], - }, - ], - }, - { type: 'text', value: 'Copy' }, - ], - }, - ], - }, - node, - ], - }, - ], - } - } - return node - }) - } - } - const { onViewFile, onViewFileContainer } = usePath() const tokenSpeed = useAtomValue(tokenSpeedAtom) const messages = useAtomValue(getCurrentChatMessagesAtom) + const text = useMemo( + () => props.content[0]?.text?.value ?? '', + [props.content] + ) + const messageType = useMemo( + () => props.content[0]?.type ?? '', + [props.content] + ) + + const annotation = useMemo( + () => props.content[0]?.text?.annotations[0] ?? '', + [props.content] + ) + return (
= (props) => { )} > <> - {props.content[0]?.type === ContentType.Image && ( + {messageType === ContentType.Image && (
- onViewFile(`${props.content[0]?.text.annotations[0]}`) - } + onClick={() => onViewFile(annotation)} />
= (props) => {
)} - {props.content[0]?.type === ContentType.Pdf && ( + {messageType === ContentType.Pdf && (
- onViewFile(`${props.id}.${props.content[0]?.type}`) - } + onClick={() => onViewFile(`${props.id}.${messageType}`)} /> = (props) => { )} dir="ltr" > - - {text} - +
)} @@ -399,4 +208,209 @@ const SimpleTextMessage: React.FC = (props) => { ) } +export const MarkdownTextMessage = memo( + ({ text, id }: { id: string; text: string }) => { + console.log('rerender', id) + const clipboard = useClipboard({ timeout: 1000 }) + + function extractCodeLines(node: { children: { children: any[] }[] }) { + const codeLines: any[] = [] + + // Helper function to extract text recursively from children + function getTextFromNode(node: { + type: string + value: any + children: any[] + }): string { + if (node.type === 'text') { + return node.value + } else if (node.children) { + return node.children.map(getTextFromNode).join('') + } + return '' + } + + // Traverse each line in the block + node.children[0].children.forEach( + (lineNode: { + type: string + tagName: string + value: any + children: any[] + }) => { + if (lineNode.type === 'element' && lineNode.tagName === 'span') { + const lineContent = getTextFromNode(lineNode) + codeLines.push(lineContent) + } + } + ) + + // Join the lines with newline characters for proper formatting + return codeLines.join('\n') + } + function wrapCodeBlocksWithoutVisit() { + return (tree: { children: any[] }) => { + tree.children = tree.children.map((node) => { + if (node.tagName === 'pre' && node.children[0]?.tagName === 'code') { + const language = + node.children[0].properties.className?.[1]?.replace( + 'language-', + '' + ) + + if (extractCodeLines(node) === '') { + return node + } + + return { + type: 'element', + tagName: 'div', + properties: { + className: ['code-block-wrapper'], + }, + children: [ + { + type: 'element', + tagName: 'div', + properties: { + className: [ + 'code-block', + 'group/item', + 'relative', + 'my-4', + 'overflow-auto', + ], + }, + children: [ + { + type: 'element', + tagName: 'div', + properties: { + className: + 'code-header bg-[hsla(var(--app-code-block))] flex justify-between items-center py-2 px-3 code-header--border rounded-t-lg', + }, + children: [ + { + type: 'element', + tagName: 'span', + properties: { + className: 'text-xs font-medium text-gray-300', + }, + children: [ + { + type: 'text', + value: language + ? `${getLanguageFromExtension(language)}` + : '', + }, + ], + }, + { + type: 'element', + tagName: 'button', + properties: { + className: + 'copy-button ml-auto flex items-center gap-1 text-xs font-medium text-gray-400 hover:text-gray-600 focus:outline-none', + onClick: (event: Event) => { + clipboard.copy(extractCodeLines(node)) + + const button = event.currentTarget as HTMLElement + button.innerHTML = ` + + Copied + ` + + setTimeout(() => { + button.innerHTML = ` + + Copy + ` + }, 2000) + }, + }, + children: [ + { + type: 'element', + tagName: 'svg', + properties: { + xmlns: 'http://www.w3.org/2000/svg', + width: '16', + height: '16', + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + strokeWidth: '2', + strokeLinecap: 'round', + strokeLinejoin: 'round', + className: + 'lucide lucide-copy pointer-events-none text-gray-400', + }, + children: [ + { + type: 'element', + tagName: 'rect', + properties: { + width: '14', + height: '14', + x: '8', + y: '8', + rx: '2', + ry: '2', + }, + children: [], + }, + { + type: 'element', + tagName: 'path', + properties: { + d: 'M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2', + }, + children: [], + }, + ], + }, + { type: 'text', value: 'Copy' }, + ], + }, + ], + }, + node, + ], + }, + ], + } + } + return node + }) + } + } + return ( + <> + + {text} + + + ) + } +) + export default React.memo(SimpleTextMessage)