mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-03 19:30:04 +08:00
feat: change citation list style (#5516)
* feat: change citation list style * feat: add link preview * fix: model inside searching * fix: change button word
This commit is contained in:
parent
fbf47fc943
commit
2be55e1d44
@ -313,6 +313,7 @@
|
|||||||
"fullscreen": "Entered fullscreen mode. Press F11 to exit",
|
"fullscreen": "Entered fullscreen mode. Press F11 to exit",
|
||||||
"knowledge_base": "Knowledge Base",
|
"knowledge_base": "Knowledge Base",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
|
"loading": "Loading...",
|
||||||
"model": "Model",
|
"model": "Model",
|
||||||
"models": "Models",
|
"models": "Models",
|
||||||
"more": "More",
|
"more": "More",
|
||||||
@ -533,6 +534,7 @@
|
|||||||
"backup.start.success": "Backup started",
|
"backup.start.success": "Backup started",
|
||||||
"backup.success": "Backup successful",
|
"backup.success": "Backup successful",
|
||||||
"chat.completion.paused": "Chat completion paused",
|
"chat.completion.paused": "Chat completion paused",
|
||||||
|
"citation": "{{count}} citations",
|
||||||
"citations": "References",
|
"citations": "References",
|
||||||
"copied": "Copied!",
|
"copied": "Copied!",
|
||||||
"copy.failed": "Copy failed",
|
"copy.failed": "Copy failed",
|
||||||
|
|||||||
@ -313,6 +313,7 @@
|
|||||||
"fullscreen": "全画面モードに入りました。F11キーで終了します",
|
"fullscreen": "全画面モードに入りました。F11キーで終了します",
|
||||||
"knowledge_base": "ナレッジベース",
|
"knowledge_base": "ナレッジベース",
|
||||||
"language": "言語",
|
"language": "言語",
|
||||||
|
"loading": "読み込み中...",
|
||||||
"model": "モデル",
|
"model": "モデル",
|
||||||
"models": "モデル",
|
"models": "モデル",
|
||||||
"more": "もっと",
|
"more": "もっと",
|
||||||
@ -533,7 +534,8 @@
|
|||||||
"backup.start.success": "バックアップを開始しました",
|
"backup.start.success": "バックアップを開始しました",
|
||||||
"backup.success": "バックアップに成功しました",
|
"backup.success": "バックアップに成功しました",
|
||||||
"chat.completion.paused": "チャットの完了が一時停止されました",
|
"chat.completion.paused": "チャットの完了が一時停止されました",
|
||||||
"citations": "参考文献",
|
"citation": "{{count}}個の引用内容",
|
||||||
|
"citations": "引用内容",
|
||||||
"copied": "コピーしました!",
|
"copied": "コピーしました!",
|
||||||
"copy.failed": "コピーに失敗しました",
|
"copy.failed": "コピーに失敗しました",
|
||||||
"copy.success": "コピーしました!",
|
"copy.success": "コピーしました!",
|
||||||
|
|||||||
@ -313,6 +313,7 @@
|
|||||||
"fullscreen": "Вы вошли в полноэкранный режим. Нажмите F11 для выхода",
|
"fullscreen": "Вы вошли в полноэкранный режим. Нажмите F11 для выхода",
|
||||||
"knowledge_base": "База знаний",
|
"knowledge_base": "База знаний",
|
||||||
"language": "Язык",
|
"language": "Язык",
|
||||||
|
"loading": "Загрузка...",
|
||||||
"model": "Модель",
|
"model": "Модель",
|
||||||
"models": "Модели",
|
"models": "Модели",
|
||||||
"more": "Ещё",
|
"more": "Ещё",
|
||||||
@ -533,7 +534,8 @@
|
|||||||
"backup.start.success": "Создание резервной копии начато",
|
"backup.start.success": "Создание резервной копии начато",
|
||||||
"backup.success": "Резервная копия успешно создана",
|
"backup.success": "Резервная копия успешно создана",
|
||||||
"chat.completion.paused": "Завершение чата приостановлено",
|
"chat.completion.paused": "Завершение чата приостановлено",
|
||||||
"citations": "Источники",
|
"citation": "{{count}} цитат",
|
||||||
|
"citations": "Содержание цитат",
|
||||||
"copied": "Скопировано!",
|
"copied": "Скопировано!",
|
||||||
"copy.failed": "Не удалось скопировать",
|
"copy.failed": "Не удалось скопировать",
|
||||||
"copy.success": "Скопировано!",
|
"copy.success": "Скопировано!",
|
||||||
|
|||||||
@ -312,6 +312,7 @@
|
|||||||
"fullscreen": "已进入全屏模式,按 F11 退出",
|
"fullscreen": "已进入全屏模式,按 F11 退出",
|
||||||
"knowledge_base": "知识库",
|
"knowledge_base": "知识库",
|
||||||
"language": "语言",
|
"language": "语言",
|
||||||
|
"loading": "加载中...",
|
||||||
"model": "模型",
|
"model": "模型",
|
||||||
"models": "模型",
|
"models": "模型",
|
||||||
"more": "更多",
|
"more": "更多",
|
||||||
@ -532,6 +533,7 @@
|
|||||||
"backup.start.success": "开始备份",
|
"backup.start.success": "开始备份",
|
||||||
"backup.success": "备份成功",
|
"backup.success": "备份成功",
|
||||||
"chat.completion.paused": "会话已停止",
|
"chat.completion.paused": "会话已停止",
|
||||||
|
"citation": "{{count}}个引用内容",
|
||||||
"citations": "引用内容",
|
"citations": "引用内容",
|
||||||
"copied": "已复制",
|
"copied": "已复制",
|
||||||
"copy.failed": "复制失败",
|
"copy.failed": "复制失败",
|
||||||
|
|||||||
@ -312,6 +312,7 @@
|
|||||||
"fullscreen": "已進入全螢幕模式,按 F11 結束",
|
"fullscreen": "已進入全螢幕模式,按 F11 結束",
|
||||||
"knowledge_base": "知識庫",
|
"knowledge_base": "知識庫",
|
||||||
"language": "語言",
|
"language": "語言",
|
||||||
|
"loading": "加載中...",
|
||||||
"model": "模型",
|
"model": "模型",
|
||||||
"models": "模型",
|
"models": "模型",
|
||||||
"more": "更多",
|
"more": "更多",
|
||||||
@ -532,7 +533,8 @@
|
|||||||
"backup.start.success": "開始備份",
|
"backup.start.success": "開始備份",
|
||||||
"backup.success": "備份成功",
|
"backup.success": "備份成功",
|
||||||
"chat.completion.paused": "聊天完成已暫停",
|
"chat.completion.paused": "聊天完成已暫停",
|
||||||
"citations": "參考文獻",
|
"citation": "{{count}} 個引用內容",
|
||||||
|
"citations": "引用內容",
|
||||||
"copied": "已複製!",
|
"copied": "已複製!",
|
||||||
"copy.failed": "複製失敗",
|
"copy.failed": "複製失敗",
|
||||||
"copy.success": "已複製!",
|
"copy.success": "已複製!",
|
||||||
|
|||||||
@ -66,13 +66,12 @@ const Markdown: FC<Props> = ({ block }) => {
|
|||||||
}, [mathEngine, messageContent])
|
}, [mathEngine, messageContent])
|
||||||
|
|
||||||
const components = useMemo(() => {
|
const components = useMemo(() => {
|
||||||
const baseComponents = {
|
return {
|
||||||
a: (props: any) => <Link {...props} citationData={parseJSON(findCitationInChildren(props.children))} />,
|
a: (props: any) => <Link {...props} citationData={parseJSON(findCitationInChildren(props.children))} />,
|
||||||
code: CodeBlock,
|
code: CodeBlock,
|
||||||
img: ImagePreview,
|
img: ImagePreview,
|
||||||
pre: (props: any) => <pre style={{ overflow: 'visible' }} {...props} />
|
pre: (props: any) => <pre style={{ overflow: 'visible' }} {...props} />
|
||||||
} as Partial<Components>
|
} as Partial<Components>
|
||||||
return baseComponents
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// if (role === 'user' && !renderInputMessageAsMarkdown) {
|
// if (role === 'user' && !renderInputMessageAsMarkdown) {
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import Favicon from '@renderer/components/Icons/FallbackFavicon'
|
import Favicon from '@renderer/components/Icons/FallbackFavicon'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
import { Collapse, theme } from 'antd'
|
import { fetchWebContent } from '@renderer/utils/fetch'
|
||||||
import { FileSearch, Info } from 'lucide-react'
|
import { Button, Drawer } from 'antd'
|
||||||
import React, { useMemo } from 'react'
|
import { FileSearch } from 'lucide-react'
|
||||||
|
import React, { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
@ -18,135 +19,205 @@ export interface Citation {
|
|||||||
|
|
||||||
interface CitationsListProps {
|
interface CitationsListProps {
|
||||||
citations: Citation[]
|
citations: Citation[]
|
||||||
hideTitle?: boolean
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 限制文本长度
|
||||||
|
* @param text
|
||||||
|
* @param maxLength
|
||||||
|
*/
|
||||||
|
const truncateText = (text: string, maxLength = 100) => {
|
||||||
|
if (!text) return ''
|
||||||
|
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 CitationsList: React.FC<CitationsListProps> = ({ citations }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
const { token } = theme.useToken()
|
const hasCitations = citations.length > 0
|
||||||
const items = useMemo(() => {
|
const count = citations.length
|
||||||
return !citations || citations.length === 0
|
const previewItems = citations.slice(0, 3)
|
||||||
? []
|
|
||||||
: [
|
|
||||||
{
|
|
||||||
key: '1',
|
|
||||||
label: (
|
|
||||||
<CitationsTitle>
|
|
||||||
<span>{t('message.citations')}</span>
|
|
||||||
<Info size={14} style={{ opacity: 0.6 }} />
|
|
||||||
</CitationsTitle>
|
|
||||||
),
|
|
||||||
style: {
|
|
||||||
backgroundColor: token.colorFillAlter
|
|
||||||
},
|
|
||||||
children: (
|
|
||||||
<>
|
|
||||||
{citations.map((citation) => (
|
|
||||||
<HStack key={citation.url || citation.number} style={{ alignItems: 'center', gap: 8 }}>
|
|
||||||
<span style={{ fontSize: 13, color: 'var(--color-text-2)' }}>{citation.number}.</span>
|
|
||||||
{citation.type === 'websearch' ? (
|
|
||||||
<WebSearchCitation citation={citation} />
|
|
||||||
) : (
|
|
||||||
<KnowledgeCitation citation={citation} />
|
|
||||||
)}
|
|
||||||
</HStack>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}, [citations, t])
|
|
||||||
|
|
||||||
if (!citations || citations.length === 0) return null
|
if (!hasCitations) return null
|
||||||
|
|
||||||
|
const handleOpen = () => {
|
||||||
|
setOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CitationsContainer>
|
<>
|
||||||
<Collapse items={items} size="small" bordered={false} style={{ background: token.colorBgContainer }} />
|
<OpenButton type="text" onClick={handleOpen}>
|
||||||
</CitationsContainer>
|
<PreviewIcons>
|
||||||
|
{previewItems.map((c, i) => (
|
||||||
|
<PreviewIcon key={i} style={{ zIndex: previewItems.length - i }}>
|
||||||
|
{c.type === 'websearch' && c.url ? (
|
||||||
|
<Favicon hostname={new URL(c.url).hostname} alt={''} />
|
||||||
|
) : (
|
||||||
|
<FileSearch width={16} />
|
||||||
|
)}
|
||||||
|
</PreviewIcon>
|
||||||
|
))}
|
||||||
|
</PreviewIcons>
|
||||||
|
{t('message.citation', { count: count })}
|
||||||
|
</OpenButton>
|
||||||
|
|
||||||
|
<Drawer
|
||||||
|
title={t('message.citations')}
|
||||||
|
placement="right"
|
||||||
|
onClose={handleClose}
|
||||||
|
open={open}
|
||||||
|
width={680}
|
||||||
|
destroyOnClose
|
||||||
|
styles={{
|
||||||
|
body: {
|
||||||
|
padding: 16,
|
||||||
|
height: 'calc(100% - 55px)'
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
{citations.map((citation) => (
|
||||||
|
<HStack key={citation.url || citation.number} style={{ alignItems: 'center', gap: 8, marginBottom: 12 }}>
|
||||||
|
{citation.type === 'websearch' ? (
|
||||||
|
<WebSearchCitation citation={citation} />
|
||||||
|
) : (
|
||||||
|
<KnowledgeCitation citation={citation} />
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
))}
|
||||||
|
</Drawer>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleLinkClick = (url: string, event: React.MouseEvent) => {
|
const handleLinkClick = (url: string, event: React.MouseEvent) => {
|
||||||
if (!url) return
|
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
if (url.startsWith('http')) window.open(url, '_blank', 'noopener,noreferrer')
|
||||||
// 检查是否是网络URL
|
else window.api.file.openPath(url)
|
||||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
||||||
window.open(url, '_blank', 'noopener,noreferrer')
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
window.api.file.openPath(url)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('打开本地文件失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 网络搜索引用组件
|
|
||||||
const WebSearchCitation: React.FC<{ citation: Citation }> = ({ citation }) => {
|
const WebSearchCitation: React.FC<{ citation: Citation }> = ({ citation }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [fetchedContent, setFetchedContent] = React.useState('')
|
||||||
|
const [isLoading, setIsLoading] = React.useState(false)
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (citation.url) {
|
||||||
|
setIsLoading(true)
|
||||||
|
fetchWebContent(citation.url, 'markdown')
|
||||||
|
.then((res) => {
|
||||||
|
const cleaned = cleanMarkdownContent(res.content)
|
||||||
|
setFetchedContent(truncateText(cleaned, 100))
|
||||||
|
})
|
||||||
|
.finally(() => setIsLoading(false))
|
||||||
|
}
|
||||||
|
}, [citation.url])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<WebSearchCard>
|
||||||
{citation.showFavicon && citation.url && (
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
|
||||||
<Favicon hostname={new URL(citation.url).hostname} alt={citation.title || citation.hostname || ''} />
|
{citation.showFavicon && citation.url && (
|
||||||
)}
|
<Favicon hostname={new URL(citation.url).hostname} alt={citation.title || citation.hostname || ''} />
|
||||||
<CitationLink href={citation.url} className="text-nowrap" onClick={(e) => handleLinkClick(citation.url, e)}>
|
)}
|
||||||
{citation.title ? citation.title : <span className="hostname">{citation.hostname}</span>}
|
<CitationLink href={citation.url} onClick={(e) => handleLinkClick(citation.url, e)}>
|
||||||
</CitationLink>
|
{citation.title || <span className="hostname">{citation.hostname}</span>}
|
||||||
</>
|
</CitationLink>
|
||||||
|
</div>
|
||||||
|
{isLoading ? <div>{t('common.loading')}</div> : fetchedContent}
|
||||||
|
</WebSearchCard>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 知识库引用组件
|
const KnowledgeCitation: React.FC<{ citation: Citation }> = ({ citation }) => (
|
||||||
const KnowledgeCitation: React.FC<{ citation: Citation }> = ({ citation }) => {
|
<>
|
||||||
return (
|
{citation.showFavicon && <FileSearch width={16} />}
|
||||||
<>
|
<CitationLink href={citation.url} onClick={(e) => handleLinkClick(citation.url, e)}>
|
||||||
{citation.showFavicon && citation.url && <FileSearch width={16} />}
|
{citation.title}
|
||||||
<CitationLink href={citation.url} className="text-nowrap" onClick={(e) => handleLinkClick(citation.url, e)}>
|
</CitationLink>
|
||||||
{citation.title}
|
</>
|
||||||
</CitationLink>
|
)
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const CitationsContainer = styled.div`
|
const OpenButton = styled(Button)`
|
||||||
background-color: rgb(242, 247, 253);
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
margin: 12px 0;
|
|
||||||
display: inline-block;
|
|
||||||
/* display: flex; */
|
|
||||||
/* flex-direction: column; */
|
|
||||||
gap: 4px;
|
|
||||||
|
|
||||||
body[theme-mode='dark'] & {
|
|
||||||
background-color: rgba(255, 255, 255, 0.05);
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const CitationsTitle = styled.div`
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
color: var(--color-text-1);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
padding: 2px 6px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
align-self: flex-start;
|
||||||
|
font-size: 12px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const PreviewIcons = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 8px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const PreviewIcon = styled.div`
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #fff;
|
||||||
|
margin-left: -8px;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const CitationLink = styled.a`
|
const CitationLink = styled.a`
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
text-decoration: none;
|
|
||||||
color: var(--color-text-1);
|
color: var(--color-text-1);
|
||||||
|
text-decoration: none;
|
||||||
.hostname {
|
|
||||||
color: var(--color-link);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hostname {
|
||||||
|
color: var(--color-link);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const WebSearchCard = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
background-color: var(--color-bg-2);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
background-color: var(--color-bg-3);
|
||||||
|
border-color: var(--color-primary-light);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export default CitationsList
|
export default CitationsList
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import MessageItem from './Message'
|
import MessageItem from './Message'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
assistant: Assistant
|
assistant: Assistant
|
||||||
route: string
|
route: string
|
||||||
@ -33,7 +34,6 @@ const Messages: FC<Props> = ({ assistant, route }) => {
|
|||||||
// setMessages((prev) => {
|
// setMessages((prev) => {
|
||||||
// const assistantMessage = getAssistantMessage({ assistant, topic: assistant.topics[0] })
|
// const assistantMessage = getAssistantMessage({ assistant, topic: assistant.topics[0] })
|
||||||
// store.dispatch(newMessagesActions.addMessage({ topicId: assistant.topics[0].id, message: assistantMessage }))
|
// store.dispatch(newMessagesActions.addMessage({ topicId: assistant.topics[0].id, message: assistantMessage }))
|
||||||
|
|
||||||
// const messages = prev.concat([message, assistantMessage])
|
// const messages = prev.concat([message, assistantMessage])
|
||||||
// return messages
|
// return messages
|
||||||
// })
|
// })
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user