mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-24 10:40:07 +08:00
feat: add citation content copy button (#5966)
* feat: add citation content copy button * fix: build error
This commit is contained in:
parent
71cd2def2e
commit
e7c0bbb348
@ -5,6 +5,7 @@ import type { RootState } from '@renderer/store'
|
||||
import { selectFormattedCitationsByBlockId } from '@renderer/store/messageBlock'
|
||||
import { type Model, WebSearchSource } from '@renderer/types'
|
||||
import type { MainTextMessageBlock, Message } from '@renderer/types/newMessage'
|
||||
import { cleanMarkdownContent } from '@renderer/utils/formats'
|
||||
import { Flex } from 'antd'
|
||||
import React, { useMemo } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
@ -37,9 +38,13 @@ const MainTextBlock: React.FC<Props> = ({ block, citationBlockId, role, mentions
|
||||
// Use the passed citationBlockId directly in the selector
|
||||
const { renderInputMessageAsMarkdown } = useSettings()
|
||||
|
||||
const formattedCitations = useSelector((state: RootState) =>
|
||||
selectFormattedCitationsByBlockId(state, citationBlockId)
|
||||
)
|
||||
const formattedCitations = useSelector((state: RootState) => {
|
||||
const citations = selectFormattedCitationsByBlockId(state, citationBlockId)
|
||||
return citations.map((citation) => ({
|
||||
...citation,
|
||||
content: citation.content ? cleanMarkdownContent(citation.content) : citation.content
|
||||
}))
|
||||
})
|
||||
|
||||
const processedContent = useMemo(() => {
|
||||
let content = block.content
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import Favicon from '@renderer/components/Icons/FallbackFavicon'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { fetchWebContent } from '@renderer/utils/fetch'
|
||||
import { cleanMarkdownContent } from '@renderer/utils/formats'
|
||||
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query'
|
||||
import { Button, Drawer, Skeleton } from 'antd'
|
||||
import { FileSearch } from 'lucide-react'
|
||||
import { Button, Drawer, message, Skeleton } from 'antd'
|
||||
import { Check, Copy, FileSearch } from 'lucide-react'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@ -44,21 +45,6 @@ const truncateText = (text: string, maxLength = 100) => {
|
||||
return text.length > maxLength ? text.slice(0, maxLength) + '...' : text
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理Markdown内容
|
||||
* @param text
|
||||
*/
|
||||
const cleanMarkdownContent = (text: string): string => {
|
||||
if (!text) return ''
|
||||
let cleaned = text.replace(/!\[.*?]\(.*?\)/g, '')
|
||||
cleaned = cleaned.replace(/\[(.*?)]\(.*?\)/g, '$1')
|
||||
cleaned = cleaned.replace(/https?:\/\/\S+/g, '')
|
||||
cleaned = cleaned.replace(/[-—–_=+]{3,}/g, ' ')
|
||||
cleaned = cleaned.replace(/[¥$€£¥%@#&*^()[\]{}<>~`'"\\|/_.]+/g, '')
|
||||
cleaned = cleaned.replace(/\s+/g, ' ').trim()
|
||||
return cleaned
|
||||
}
|
||||
|
||||
const CitationsList: React.FC<CitationsListProps> = ({ citations }) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
@ -115,6 +101,27 @@ const handleLinkClick = (url: string, event: React.MouseEvent) => {
|
||||
else window.api.file.openPath(url)
|
||||
}
|
||||
|
||||
const CopyButton: React.FC<{ content: string }> = ({ content }) => {
|
||||
const [copied, setCopied] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleCopy = () => {
|
||||
if (!content) return
|
||||
navigator.clipboard
|
||||
.writeText(content)
|
||||
.then(() => {
|
||||
setCopied(true)
|
||||
message.success(t('common.copied'))
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
})
|
||||
.catch(() => {
|
||||
message.error(t('message.copy.failed'))
|
||||
})
|
||||
}
|
||||
|
||||
return <CopyIconWrapper onClick={handleCopy}>{copied ? <Check size={14} /> : <Copy size={14} />}</CopyIconWrapper>
|
||||
}
|
||||
|
||||
const WebSearchCitation: React.FC<{ citation: Citation }> = ({ citation }) => {
|
||||
const { data: fetchedContent, isLoading } = useQuery({
|
||||
queryKey: ['webContent', citation.url],
|
||||
@ -136,6 +143,7 @@ const WebSearchCitation: React.FC<{ citation: Citation }> = ({ citation }) => {
|
||||
<CitationLink className="text-nowrap" href={citation.url} onClick={(e) => handleLinkClick(citation.url, e)}>
|
||||
{citation.title || <span className="hostname">{citation.hostname}</span>}
|
||||
</CitationLink>
|
||||
{fetchedContent && <CopyButton content={fetchedContent} />}
|
||||
</WebSearchCardHeader>
|
||||
{isLoading ? (
|
||||
<Skeleton active paragraph={{ rows: 1 }} title={false} />
|
||||
@ -153,6 +161,7 @@ const KnowledgeCitation: React.FC<{ citation: Citation }> = ({ citation }) => (
|
||||
<CitationLink className="text-nowrap" href={citation.url} onClick={(e) => handleLinkClick(citation.url, e)}>
|
||||
{citation.title}
|
||||
</CitationLink>
|
||||
{citation.content && <CopyButton content={citation.content} />}
|
||||
</WebSearchCardHeader>
|
||||
<WebSearchCardContent>{citation.content && truncateText(citation.content, 100)}</WebSearchCardContent>
|
||||
</WebSearchCard>
|
||||
@ -203,6 +212,23 @@ const CitationLink = styled.a`
|
||||
}
|
||||
`
|
||||
|
||||
const CopyIconWrapper = styled.div`
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-text-2);
|
||||
opacity: 0.6;
|
||||
margin-left: auto;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
background-color: var(--color-background-soft);
|
||||
}
|
||||
`
|
||||
|
||||
const WebSearchCard = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -219,6 +245,7 @@ const WebSearchCardHeader = styled.div`
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
const WebSearchCardContent = styled.div`
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { XMLParser } from 'fast-xml-parser'
|
||||
|
||||
export interface ExtractResults {
|
||||
websearch?: WebsearchExtractResults
|
||||
knowledge?: KnowledgeExtractResults
|
||||
@ -27,7 +28,6 @@ export const extractInfoFromXML = (text: string): ExtractResults => {
|
||||
return name === 'question' || name === 'links'
|
||||
}
|
||||
})
|
||||
const extractResults: ExtractResults = parser.parse(text)
|
||||
// Logger.log('Extracted results:', extractResults)
|
||||
return extractResults
|
||||
return parser.parse(text)
|
||||
}
|
||||
|
||||
@ -2,6 +2,22 @@ import type { Message } from '@renderer/types/newMessage'
|
||||
|
||||
import { findImageBlocks, getMainTextContent } from './messageUtils/find'
|
||||
|
||||
/**
|
||||
* 清理Markdown内容
|
||||
* @param text 要清理的文本
|
||||
* @returns 清理后的文本
|
||||
*/
|
||||
export function cleanMarkdownContent(text: string): string {
|
||||
if (!text) return ''
|
||||
let cleaned = text.replace(/!\[.*?]\(.*?\)/g, '') // 移除图片
|
||||
cleaned = cleaned.replace(/\[(.*?)]\(.*?\)/g, '$1') // 替换链接为纯文本
|
||||
cleaned = cleaned.replace(/https?:\/\/\S+/g, '') // 移除URL
|
||||
cleaned = cleaned.replace(/[-—–_=+]{3,}/g, ' ') // 替换分隔符为空格
|
||||
cleaned = cleaned.replace(/[¥$€£¥%@#&*^()[\]{}<>~`'"\\|/_.]+/g, '') // 移除特殊字符
|
||||
cleaned = cleaned.replace(/\s+/g, ' ').trim() // 规范化空白
|
||||
return cleaned
|
||||
}
|
||||
|
||||
export function escapeDollarNumber(text: string) {
|
||||
let escapedText = ''
|
||||
|
||||
@ -20,7 +36,7 @@ export function escapeDollarNumber(text: string) {
|
||||
}
|
||||
|
||||
export function escapeBrackets(text: string) {
|
||||
const pattern = /(```[\s\S]*?```|`.*?`)|\\\[([\s\S]*?[^\\])\\\]|\\\((.*?)\\\)/g
|
||||
const pattern = /(```[\s\S]*?```|`.*?`)|\\\[([\s\S]*?[^\\])\\]|\\\((.*?)\\\)/g
|
||||
return text.replace(pattern, (match, codeBlock, squareBracket, roundBracket) => {
|
||||
if (codeBlock) {
|
||||
return codeBlock
|
||||
@ -102,7 +118,7 @@ export function withGenerateImage(message: Message): { content: string; images?:
|
||||
const originalContent = getMainTextContent(message)
|
||||
const imagePattern = new RegExp(`!\\[[^\\]]*\\]\\((.*?)\\s*("(?:.*[^"])")?\\s*\\)`)
|
||||
const images: string[] = []
|
||||
let processedContent = originalContent
|
||||
let processedContent: string
|
||||
|
||||
processedContent = originalContent.replace(imagePattern, (_, url) => {
|
||||
if (url) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user