From 12118192efc3748ee226d82f4d7293b226907ded Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Mon, 1 Sep 2025 13:02:53 +0700 Subject: [PATCH 1/2] chore: fix id codeblock for avoid duplicate same state --- web-app/src/containers/ChatInput.tsx | 182 +++++++++++----------- web-app/src/containers/RenderMarkdown.tsx | 31 ++-- 2 files changed, 108 insertions(+), 105 deletions(-) diff --git a/web-app/src/containers/ChatInput.tsx b/web-app/src/containers/ChatInput.tsx index 8efd5e6a7..f799f6b50 100644 --- a/web-app/src/containers/ChatInput.tsx +++ b/web-app/src/containers/ChatInput.tsx @@ -375,106 +375,108 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => { } const handlePaste = async (e: React.ClipboardEvent) => { - // Only allow paste if model supports mmproj - if (!hasMmproj) { - return - } + // Only process images if model supports mmproj + if (hasMmproj) { + const clipboardItems = e.clipboardData?.items + let hasProcessedImage = false - const clipboardItems = e.clipboardData?.items - let hasProcessedImage = false + // Try clipboardData.items first (traditional method) + if (clipboardItems && clipboardItems.length > 0) { + const imageItems = Array.from(clipboardItems).filter((item) => + item.type.startsWith('image/') + ) - // Try clipboardData.items first (traditional method) - if (clipboardItems && clipboardItems.length > 0) { - const imageItems = Array.from(clipboardItems).filter((item) => - item.type.startsWith('image/') - ) + if (imageItems.length > 0) { + e.preventDefault() - if (imageItems.length > 0) { - e.preventDefault() + const files: File[] = [] + let processedCount = 0 - const files: File[] = [] - let processedCount = 0 - - imageItems.forEach((item) => { - const file = item.getAsFile() - if (file) { - files.push(file) - } - processedCount++ - - // When all items are processed, handle the valid files - if (processedCount === imageItems.length) { - if (files.length > 0) { - const syntheticEvent = { - target: { - files: files, - }, - } as unknown as React.ChangeEvent - - handleFileChange(syntheticEvent) - hasProcessedImage = true - } - } - }) - - // If we found image items but couldn't get files, fall through to modern API - if (processedCount === imageItems.length && !hasProcessedImage) { - // Continue to modern clipboard API fallback below - } else { - return // Successfully processed with traditional method - } - } - } - - // Modern Clipboard API fallback (for Linux, images copied from web, etc.) - if (navigator.clipboard && 'read' in navigator.clipboard) { - e.preventDefault() - - try { - const clipboardContents = await navigator.clipboard.read() - const files: File[] = [] - - for (const item of clipboardContents) { - const imageTypes = item.types.filter((type) => - type.startsWith('image/') - ) - - for (const type of imageTypes) { - try { - const blob = await item.getType(type) - // Convert blob to File with better naming - const extension = type.split('/')[1] || 'png' - const file = new File( - [blob], - `pasted-image-${Date.now()}.${extension}`, - { type } - ) + imageItems.forEach((item) => { + const file = item.getAsFile() + if (file) { files.push(file) - } catch (error) { - console.error('Error reading clipboard item:', error) } + processedCount++ + + // When all items are processed, handle the valid files + if (processedCount === imageItems.length) { + if (files.length > 0) { + const syntheticEvent = { + target: { + files: files, + }, + } as unknown as React.ChangeEvent + + handleFileChange(syntheticEvent) + hasProcessedImage = true + } + } + }) + + // If we found image items but couldn't get files, fall through to modern API + if (processedCount === imageItems.length && !hasProcessedImage) { + // Continue to modern clipboard API fallback below + } else { + return // Successfully processed with traditional method } } - - if (files.length > 0) { - const syntheticEvent = { - target: { - files: files, - }, - } as unknown as React.ChangeEvent - - handleFileChange(syntheticEvent) - return - } - } catch (error) { - console.error('Clipboard API access failed:', error) } - } - // If we reach here, no image was found or processed - if (!hasProcessedImage) { - console.log('No image data found in clipboard or clipboard access failed') + // Modern Clipboard API fallback (for Linux, images copied from web, etc.) + if ( + navigator.clipboard && + 'read' in navigator.clipboard && + !hasProcessedImage + ) { + try { + const clipboardContents = await navigator.clipboard.read() + const files: File[] = [] + + for (const item of clipboardContents) { + const imageTypes = item.types.filter((type) => + type.startsWith('image/') + ) + + for (const type of imageTypes) { + try { + const blob = await item.getType(type) + // Convert blob to File with better naming + const extension = type.split('/')[1] || 'png' + const file = new File( + [blob], + `pasted-image-${Date.now()}.${extension}`, + { type } + ) + files.push(file) + } catch (error) { + console.error('Error reading clipboard item:', error) + } + } + } + + if (files.length > 0) { + e.preventDefault() + const syntheticEvent = { + target: { + files: files, + }, + } as unknown as React.ChangeEvent + + handleFileChange(syntheticEvent) + return + } + } catch (error) { + console.error('Clipboard API access failed:', error) + } + } + + // If we reach here, no image was found - allow normal text pasting to continue + console.log( + 'No image data found in clipboard, allowing normal text paste' + ) } + // If hasMmproj is false or no images found, allow normal text pasting to continue } return ( @@ -569,7 +571,7 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => { // When Shift+Enter is pressed, a new line is added (default behavior) } }} - onPaste={hasMmproj ? handlePaste : undefined} + onPaste={handlePaste} placeholder={t('common:placeholder.chatInput')} autoFocus spellCheck={spellCheckChatInput} diff --git a/web-app/src/containers/RenderMarkdown.tsx b/web-app/src/containers/RenderMarkdown.tsx index fb5189220..667a37653 100644 --- a/web-app/src/containers/RenderMarkdown.tsx +++ b/web-app/src/containers/RenderMarkdown.tsx @@ -6,7 +6,7 @@ import remarkMath from 'remark-math' import rehypeKatex from 'rehype-katex' import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' import * as prismStyles from 'react-syntax-highlighter/dist/cjs/styles/prism' -import { memo, useState, useMemo } from 'react' +import { memo, useState, useMemo, useRef } from 'react' import { getReadableLanguageName } from '@/lib/utils' import { cn } from '@/lib/utils' import { useCodeblock } from '@/hooks/useCodeblock' @@ -37,6 +37,13 @@ function RenderMarkdownComponent({ // State for tracking which code block has been copied const [copiedId, setCopiedId] = useState(null) + // Map to store unique IDs for code blocks based on content and position + const codeBlockIds = useRef(new Map()) + + // Clear ID map when content changes + useMemo(() => { + codeBlockIds.current.clear() + }, [content]) // Function to handle copying code to clipboard const handleCopy = (code: string, id: string) => { @@ -49,17 +56,6 @@ function RenderMarkdownComponent({ }, 2000) } - // Simple hash function for strings - const hashString = (str: string): string => { - let hash = 0 - for (let i = 0; i < str.length; i++) { - const char = str.charCodeAt(i) - hash = (hash << 5) - hash + char - hash = hash & hash // Convert to 32bit integer - } - return Math.abs(hash).toString(36) - } - // Default components for syntax highlighting and emoji rendering const defaultComponents: Components = useMemo( () => ({ @@ -70,8 +66,13 @@ function RenderMarkdownComponent({ const code = String(children).replace(/\n$/, '') - // Generate a stable ID based on code content and language - const codeId = `code-${hashString(code.substring(0, 40) + language)}` + // Generate a unique ID based on content and language + const contentKey = `${code}-${language}` + let codeId = codeBlockIds.current.get(contentKey) + if (!codeId) { + codeId = `code-${codeBlockIds.current.size}` + codeBlockIds.current.set(contentKey, codeId) + } return !isInline && !isUser ? (
@@ -155,7 +156,7 @@ function RenderMarkdownComponent({ ) }, }), - [codeBlockStyle, showLineNumbers, copiedId, handleCopy, hashString] + [codeBlockStyle, showLineNumbers, copiedId] ) // Memoize the remarkPlugins to prevent unnecessary re-renders From 1544aac663fc4619b710ff6a45e5dcce4abed509 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Tue, 2 Sep 2025 09:40:29 +0700 Subject: [PATCH 2/2] chore: change useMemo to useEffect to avoid side effect --- web-app/src/containers/RenderMarkdown.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web-app/src/containers/RenderMarkdown.tsx b/web-app/src/containers/RenderMarkdown.tsx index 667a37653..125994eab 100644 --- a/web-app/src/containers/RenderMarkdown.tsx +++ b/web-app/src/containers/RenderMarkdown.tsx @@ -6,7 +6,7 @@ import remarkMath from 'remark-math' import rehypeKatex from 'rehype-katex' import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' import * as prismStyles from 'react-syntax-highlighter/dist/cjs/styles/prism' -import { memo, useState, useMemo, useRef } from 'react' +import { memo, useState, useMemo, useRef, useEffect } from 'react' import { getReadableLanguageName } from '@/lib/utils' import { cn } from '@/lib/utils' import { useCodeblock } from '@/hooks/useCodeblock' @@ -41,7 +41,7 @@ function RenderMarkdownComponent({ const codeBlockIds = useRef(new Map()) // Clear ID map when content changes - useMemo(() => { + useEffect(() => { codeBlockIds.current.clear() }, [content])