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:
自由的世界人 2025-05-07 14:21:58 +08:00 committed by GitHub
parent fbf47fc943
commit 2be55e1d44
8 changed files with 185 additions and 105 deletions

View File

@ -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",

View File

@ -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": "コピーしました!",

View File

@ -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": "Скопировано!",

View File

@ -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": "复制失败",

View File

@ -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": "已複製!",

View File

@ -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) {

View File

@ -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

View File

@ -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
// }) // })