chore: fix id codeblock for avoid duplicate same state
This commit is contained in:
parent
c19a0df16e
commit
12118192ef
@ -375,106 +375,108 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handlePaste = async (e: React.ClipboardEvent) => {
|
const handlePaste = async (e: React.ClipboardEvent) => {
|
||||||
// Only allow paste if model supports mmproj
|
// Only process images if model supports mmproj
|
||||||
if (!hasMmproj) {
|
if (hasMmproj) {
|
||||||
return
|
const clipboardItems = e.clipboardData?.items
|
||||||
}
|
let hasProcessedImage = false
|
||||||
|
|
||||||
const clipboardItems = e.clipboardData?.items
|
// Try clipboardData.items first (traditional method)
|
||||||
let hasProcessedImage = false
|
if (clipboardItems && clipboardItems.length > 0) {
|
||||||
|
const imageItems = Array.from(clipboardItems).filter((item) =>
|
||||||
|
item.type.startsWith('image/')
|
||||||
|
)
|
||||||
|
|
||||||
// Try clipboardData.items first (traditional method)
|
if (imageItems.length > 0) {
|
||||||
if (clipboardItems && clipboardItems.length > 0) {
|
e.preventDefault()
|
||||||
const imageItems = Array.from(clipboardItems).filter((item) =>
|
|
||||||
item.type.startsWith('image/')
|
|
||||||
)
|
|
||||||
|
|
||||||
if (imageItems.length > 0) {
|
const files: File[] = []
|
||||||
e.preventDefault()
|
let processedCount = 0
|
||||||
|
|
||||||
const files: File[] = []
|
imageItems.forEach((item) => {
|
||||||
let processedCount = 0
|
const file = item.getAsFile()
|
||||||
|
if (file) {
|
||||||
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<HTMLInputElement>
|
|
||||||
|
|
||||||
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 }
|
|
||||||
)
|
|
||||||
files.push(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<HTMLInputElement>
|
||||||
|
|
||||||
|
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<HTMLInputElement>
|
|
||||||
|
|
||||||
handleFileChange(syntheticEvent)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Clipboard API access failed:', error)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// If we reach here, no image was found or processed
|
// Modern Clipboard API fallback (for Linux, images copied from web, etc.)
|
||||||
if (!hasProcessedImage) {
|
if (
|
||||||
console.log('No image data found in clipboard or clipboard access failed')
|
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<HTMLInputElement>
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
@ -569,7 +571,7 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
|
|||||||
// When Shift+Enter is pressed, a new line is added (default behavior)
|
// When Shift+Enter is pressed, a new line is added (default behavior)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onPaste={hasMmproj ? handlePaste : undefined}
|
onPaste={handlePaste}
|
||||||
placeholder={t('common:placeholder.chatInput')}
|
placeholder={t('common:placeholder.chatInput')}
|
||||||
autoFocus
|
autoFocus
|
||||||
spellCheck={spellCheckChatInput}
|
spellCheck={spellCheckChatInput}
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import remarkMath from 'remark-math'
|
|||||||
import rehypeKatex from 'rehype-katex'
|
import rehypeKatex from 'rehype-katex'
|
||||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||||
import * as prismStyles from 'react-syntax-highlighter/dist/cjs/styles/prism'
|
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 { getReadableLanguageName } from '@/lib/utils'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useCodeblock } from '@/hooks/useCodeblock'
|
import { useCodeblock } from '@/hooks/useCodeblock'
|
||||||
@ -37,6 +37,13 @@ function RenderMarkdownComponent({
|
|||||||
|
|
||||||
// State for tracking which code block has been copied
|
// State for tracking which code block has been copied
|
||||||
const [copiedId, setCopiedId] = useState<string | null>(null)
|
const [copiedId, setCopiedId] = useState<string | null>(null)
|
||||||
|
// Map to store unique IDs for code blocks based on content and position
|
||||||
|
const codeBlockIds = useRef(new Map<string, string>())
|
||||||
|
|
||||||
|
// Clear ID map when content changes
|
||||||
|
useMemo(() => {
|
||||||
|
codeBlockIds.current.clear()
|
||||||
|
}, [content])
|
||||||
|
|
||||||
// Function to handle copying code to clipboard
|
// Function to handle copying code to clipboard
|
||||||
const handleCopy = (code: string, id: string) => {
|
const handleCopy = (code: string, id: string) => {
|
||||||
@ -49,17 +56,6 @@ function RenderMarkdownComponent({
|
|||||||
}, 2000)
|
}, 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
|
// Default components for syntax highlighting and emoji rendering
|
||||||
const defaultComponents: Components = useMemo(
|
const defaultComponents: Components = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@ -70,8 +66,13 @@ function RenderMarkdownComponent({
|
|||||||
|
|
||||||
const code = String(children).replace(/\n$/, '')
|
const code = String(children).replace(/\n$/, '')
|
||||||
|
|
||||||
// Generate a stable ID based on code content and language
|
// Generate a unique ID based on content and language
|
||||||
const codeId = `code-${hashString(code.substring(0, 40) + 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 ? (
|
return !isInline && !isUser ? (
|
||||||
<div className="relative overflow-hidden border rounded-md border-main-view-fg/2">
|
<div className="relative overflow-hidden border rounded-md border-main-view-fg/2">
|
||||||
@ -155,7 +156,7 @@ function RenderMarkdownComponent({
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[codeBlockStyle, showLineNumbers, copiedId, handleCopy, hashString]
|
[codeBlockStyle, showLineNumbers, copiedId]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Memoize the remarkPlugins to prevent unnecessary re-renders
|
// Memoize the remarkPlugins to prevent unnecessary re-renders
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user