diff --git a/README.md b/README.md index b589376b7b..7ce7ada20f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,33 @@ +
-
- {tokenLines.map((lineTokens, lineIndex) => (
-
+ return (
+
+
+ {tokenLines.map((lineTokens, lineIndex) => (
+
+ {showLineNumbers && {lineIndex + 1}}
+
{lineTokens.map((token, tokenIndex) => (
{token.content}
))}
- ))}
-
-
- )
- }
-)
+
+ ))}
+
+
+ )
+})
const ContentContainer = styled.div<{
- $lineNumbers: boolean
$wrap: boolean
$fadeIn: boolean
}>`
position: relative;
overflow: auto;
- border: 0.5px solid transparent;
- border-radius: 5px;
+ border-radius: inherit;
margin-top: 0;
+ /* gutter 宽度默认值 */
+ --gutter-width: 0.6rem;
+
.shiki {
padding: 1em;
+ border-radius: inherit;
code {
display: flex;
flex-direction: column;
.line {
- display: block;
+ display: flex;
+ align-items: flex-start;
min-height: 1.3rem;
- padding-left: ${(props) => (props.$lineNumbers ? '2rem' : '0')};
- * {
- overflow-wrap: ${(props) => (props.$wrap ? 'break-word' : 'normal')};
- white-space: ${(props) => (props.$wrap ? 'pre-wrap' : 'pre')};
+ .line-number {
+ width: var(--gutter-width);
+ text-align: right;
+ opacity: 0.35;
+ margin-right: 1rem;
+ user-select: none;
+ flex-shrink: 0;
+ overflow: hidden;
+ line-height: inherit;
+ font-family: inherit;
+ font-variant-numeric: tabular-nums;
+ }
+
+ .line-content {
+ flex: 1;
+
+ * {
+ overflow-wrap: ${(props) => (props.$wrap ? 'break-word' : 'normal')};
+ white-space: ${(props) => (props.$wrap ? 'pre-wrap' : 'pre')};
+ }
}
}
}
}
- ${(props) =>
- props.$lineNumbers &&
- `
- code {
- counter-reset: step;
- counter-increment: step 0;
- position: relative;
- }
-
- code .line::before {
- content: counter(step);
- counter-increment: step;
- width: 1rem;
- position: absolute;
- left: 0;
- text-align: right;
- opacity: 0.35;
- }
- `}
-
@keyframes contentFadeIn {
from {
opacity: 0;
@@ -291,7 +294,7 @@ const ContentContainer = styled.div<{
}
}
- animation: ${(props) => (props.$fadeIn ? 'contentFadeIn 0.3s ease-in-out forwards' : 'none')};
+ animation: ${(props) => (props.$fadeIn ? 'contentFadeIn 0.1s ease-in forwards' : 'none')};
`
const CodePlaceholder = styled.div`
diff --git a/src/renderer/src/components/CodeBlockView/index.tsx b/src/renderer/src/components/CodeBlockView/index.tsx
index 811b8665cc..c25ab3079d 100644
--- a/src/renderer/src/components/CodeBlockView/index.tsx
+++ b/src/renderer/src/components/CodeBlockView/index.tsx
@@ -273,6 +273,7 @@ const CodeHeader = styled.div<{ $isInSpecialView: boolean }>`
align-items: center;
color: var(--color-text);
font-size: 14px;
+ line-height: 1;
font-weight: bold;
padding: 0 10px;
border-top-left-radius: 8px;
@@ -288,6 +289,10 @@ const SplitViewWrapper = styled.div`
flex: 1 1 auto;
width: 100%;
}
+
+ &:not(:has(+ [class*='Container'])) {
+ border-radius: 0 0 8px 8px;
+ }
`
export default memo(CodeBlockView)
diff --git a/src/renderer/src/components/CodeEditor/index.tsx b/src/renderer/src/components/CodeEditor/index.tsx
index d92fd91e8e..db699fa030 100644
--- a/src/renderer/src/components/CodeEditor/index.tsx
+++ b/src/renderer/src/components/CodeEditor/index.tsx
@@ -227,10 +227,10 @@ const CodeEditor = ({
...customBasicSetup // override basicSetup
}}
style={{
- ...style,
fontSize: `${fontSize - 1}px`,
- border: '0.5px solid transparent',
- marginTop: 0
+ marginTop: 0,
+ borderRadius: 'inherit',
+ ...style
}}
/>
)
diff --git a/src/renderer/src/components/ContentSearch.tsx b/src/renderer/src/components/ContentSearch.tsx
index 08a1fd415a..1f895e348b 100644
--- a/src/renderer/src/components/ContentSearch.tsx
+++ b/src/renderer/src/components/ContentSearch.tsx
@@ -3,13 +3,10 @@ import NarrowLayout from '@renderer/pages/home/Messages/NarrowLayout'
import { Tooltip } from 'antd'
import { debounce } from 'lodash'
import { CaseSensitive, ChevronDown, ChevronUp, User, WholeWord, X } from 'lucide-react'
-import React, { useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'
+import React, { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
-const HIGHLIGHT_CLASS = 'highlight'
-const HIGHLIGHT_SELECT_CLASS = 'selected'
-
interface Props {
children?: React.ReactNode
searchTarget: React.RefObject
+
{children}
)
diff --git a/src/renderer/src/pages/home/Markdown/Markdown.tsx b/src/renderer/src/pages/home/Markdown/Markdown.tsx
index 2a6446fec7..454550c5c8 100644
--- a/src/renderer/src/pages/home/Markdown/Markdown.tsx
+++ b/src/renderer/src/pages/home/Markdown/Markdown.tsx
@@ -8,8 +8,8 @@ import { useSettings } from '@renderer/hooks/useSettings'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import type { MainTextMessageBlock, ThinkingMessageBlock, TranslationMessageBlock } from '@renderer/types/newMessage'
import { parseJSON } from '@renderer/utils'
-import { escapeBrackets, removeSvgEmptyLines } from '@renderer/utils/formats'
-import { findCitationInChildren, getCodeBlockId } from '@renderer/utils/markdown'
+import { removeSvgEmptyLines } from '@renderer/utils/formats'
+import { findCitationInChildren, getCodeBlockId, processLatexBrackets } from '@renderer/utils/markdown'
import { isEmpty } from 'lodash'
import { type FC, memo, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
@@ -52,7 +52,7 @@ const Markdown: FC = ({ block }) => {
const empty = isEmpty(block.content)
const paused = block.status === 'paused'
const content = empty && paused ? t('message.chat.completion.paused') : block.content
- return removeSvgEmptyLines(escapeBrackets(content))
+ return removeSvgEmptyLines(processLatexBrackets(content))
}, [block, t])
const rehypePlugins = useMemo(() => {
diff --git a/src/renderer/src/pages/home/Markdown/__tests__/CitationTooltip.test.tsx b/src/renderer/src/pages/home/Markdown/__tests__/CitationTooltip.test.tsx
index 06a390c06a..072bf3047e 100644
--- a/src/renderer/src/pages/home/Markdown/__tests__/CitationTooltip.test.tsx
+++ b/src/renderer/src/pages/home/Markdown/__tests__/CitationTooltip.test.tsx
@@ -93,7 +93,7 @@ describe('CitationTooltip', () => {
const tooltip = screen.getByTestId('tooltip-wrapper')
expect(tooltip).toHaveAttribute('data-placement', 'top')
- expect(tooltip).toHaveAttribute('data-color', 'var(--color-background-mute)')
+ expect(tooltip).toHaveAttribute('data-color', 'var(--color-background)')
const styles = JSON.parse(tooltip.getAttribute('data-styles') || '{}')
expect(styles.body).toEqual({
diff --git a/src/renderer/src/pages/home/Markdown/__tests__/Markdown.test.tsx b/src/renderer/src/pages/home/Markdown/__tests__/Markdown.test.tsx
index abd7067ab0..be9b18c13b 100644
--- a/src/renderer/src/pages/home/Markdown/__tests__/Markdown.test.tsx
+++ b/src/renderer/src/pages/home/Markdown/__tests__/Markdown.test.tsx
@@ -42,13 +42,13 @@ vi.mock('@renderer/utils', () => ({
}))
vi.mock('@renderer/utils/formats', () => ({
- escapeBrackets: vi.fn((str) => str),
removeSvgEmptyLines: vi.fn((str) => str)
}))
vi.mock('@renderer/utils/markdown', () => ({
findCitationInChildren: vi.fn(() => '{"id": 1, "url": "https://example.com"}'),
- getCodeBlockId: vi.fn(() => 'code-block-1')
+ getCodeBlockId: vi.fn(() => 'code-block-1'),
+ processLatexBrackets: vi.fn((str) => str)
}))
// Mock components with more realistic behavior
@@ -212,16 +212,6 @@ describe('Markdown', () => {
expect(markdown).not.toHaveTextContent('Paused')
})
- it('should process content through format utilities', async () => {
- const { escapeBrackets, removeSvgEmptyLines } = await import('@renderer/utils/formats')
- const content = 'Content with [brackets] and SVG'
-
- render( )
-
- expect(escapeBrackets).toHaveBeenCalledWith(content)
- expect(removeSvgEmptyLines).toHaveBeenCalledWith(content)
- })
-
it('should match snapshot', () => {
const { container } = render( )
expect(container.firstChild).toMatchSnapshot()
diff --git a/src/renderer/src/pages/home/Markdown/__tests__/__snapshots__/CitationTooltip.test.tsx.snap b/src/renderer/src/pages/home/Markdown/__tests__/__snapshots__/CitationTooltip.test.tsx.snap
index ff5c69767e..e9c6def351 100644
--- a/src/renderer/src/pages/home/Markdown/__tests__/__snapshots__/CitationTooltip.test.tsx.snap
+++ b/src/renderer/src/pages/home/Markdown/__tests__/__snapshots__/CitationTooltip.test.tsx.snap
@@ -47,7 +47,7 @@ exports[`CitationTooltip > basic rendering > should match snapshot 1`] = `
}
selectFormattedCitationsByBlockId(state, block.id))
+ const { websearch } = useSelector((state: RootState) => state.runtime)
+ const message = useSelector((state: RootState) => state.messages.entities[block.messageId])
+ const userMessageId = message?.askId || block.messageId // 如果没有 askId 则回退到 messageId
+
const hasGeminiBlock = block.response?.source === WebSearchSource.GEMINI
const hasCitations = useMemo(() => {
return (
@@ -21,8 +27,32 @@ function CitationBlock({ block }: { block: CitationMessageBlock }) {
)
}, [formattedCitations, block.knowledge, hasGeminiBlock])
+ const getWebSearchStatusText = (requestId: string) => {
+ const status = websearch.activeSearches[requestId] ?? { phase: 'default' }
+
+ switch (status.phase) {
+ case 'fetch_complete':
+ return t('message.websearch.fetch_complete', {
+ count: status.countAfter ?? 0
+ })
+ case 'rag':
+ return t('message.websearch.rag')
+ case 'rag_complete':
+ return t('message.websearch.rag_complete', {
+ countBefore: status.countBefore ?? 0,
+ countAfter: status.countAfter ?? 0
+ })
+ case 'rag_failed':
+ return t('message.websearch.rag_failed')
+ case 'cutoff':
+ return t('message.websearch.cutoff')
+ default:
+ return t('message.searching')
+ }
+ }
+
if (block.status === MessageBlockStatus.PROCESSING) {
- return
+ return
}
if (!hasCitations) {
diff --git a/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx b/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx
index 2cdc9a684c..b20b62bbc9 100644
--- a/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx
+++ b/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx
@@ -31,7 +31,7 @@ const MessageErrorInfo: React.FC<{ block: ErrorMessageBlock }> = ({ block }) =>
}
const Alert = styled(AntdAlert)`
- margin: 0.5rem 0;
+ margin: 0.5rem 0 !important;
padding: 10px;
font-size: 12px;
`
diff --git a/src/renderer/src/pages/home/Messages/Blocks/ImageBlock.tsx b/src/renderer/src/pages/home/Messages/Blocks/ImageBlock.tsx
index 8cecea1ad8..db4b35efa8 100644
--- a/src/renderer/src/pages/home/Messages/Blocks/ImageBlock.tsx
+++ b/src/renderer/src/pages/home/Messages/Blocks/ImageBlock.tsx
@@ -18,12 +18,12 @@ const ImageBlock: React.FC = ({ block }) => {
? [`file://${block?.file?.path}`]
: []
return (
-
+
{images.map((src, index) => (
))}
@@ -34,6 +34,5 @@ const Container = styled.div`
display: flex;
flex-direction: row;
gap: 10px;
- margin-top: 8px;
`
export default React.memo(ImageBlock)
diff --git a/src/renderer/src/pages/home/Messages/Blocks/ThinkingBlock.tsx b/src/renderer/src/pages/home/Messages/Blocks/ThinkingBlock.tsx
index 72f2871586..336287a130 100644
--- a/src/renderer/src/pages/home/Messages/Blocks/ThinkingBlock.tsx
+++ b/src/renderer/src/pages/home/Messages/Blocks/ThinkingBlock.tsx
@@ -3,7 +3,7 @@ import { useSettings } from '@renderer/hooks/useSettings'
import { MessageBlockStatus, type ThinkingMessageBlock } from '@renderer/types/newMessage'
import { lightbulbVariants } from '@renderer/utils/motionVariants'
import { Collapse, message as antdMessage, Tooltip } from 'antd'
-import { Lightbulb } from 'lucide-react'
+import { ChevronRight, Lightbulb } from 'lucide-react'
import { motion } from 'motion/react'
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -57,6 +57,14 @@ const ThinkingBlock: React.FC = ({ block }) => {
size="small"
onChange={() => setActiveKey((key) => (key ? '' : 'thought'))}
className="message-thought-container"
+ expandIcon={({ isActive }) => (
+
+ )}
expandIconPosition="end"
items={[
{
@@ -142,7 +150,7 @@ const ThinkingTimeSeconds = memo(
)
const CollapseContainer = styled(Collapse)`
- margin-bottom: 15px;
+ margin: 15px 0;
`
const MessageTitleLabel = styled.div`
diff --git a/src/renderer/src/pages/home/Messages/Blocks/__tests__/__snapshots__/ThinkingBlock.test.tsx.snap b/src/renderer/src/pages/home/Messages/Blocks/__tests__/__snapshots__/ThinkingBlock.test.tsx.snap
index 7f1f866b8b..805e7d2a90 100644
--- a/src/renderer/src/pages/home/Messages/Blocks/__tests__/__snapshots__/ThinkingBlock.test.tsx.snap
+++ b/src/renderer/src/pages/home/Messages/Blocks/__tests__/__snapshots__/ThinkingBlock.test.tsx.snap
@@ -2,7 +2,7 @@
exports[`ThinkingBlock > basic rendering > should match snapshot 1`] = `
.c0 {
- margin-bottom: 15px;
+ margin: 15px 0;
}
.c1 {
diff --git a/src/renderer/src/pages/home/Messages/Blocks/index.tsx b/src/renderer/src/pages/home/Messages/Blocks/index.tsx
index b469f03264..9f4d6e838a 100644
--- a/src/renderer/src/pages/home/Messages/Blocks/index.tsx
+++ b/src/renderer/src/pages/home/Messages/Blocks/index.tsx
@@ -164,17 +164,7 @@ export default React.memo(MessageBlockRenderer)
const ImageBlockGroup = styled.div`
display: grid;
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ grid-template-columns: repeat(3, minmax(200px, 1fr));
gap: 8px;
max-width: 960px;
- /* > * {
- min-width: 200px;
- } */
- @media (min-width: 1536px) {
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
- max-width: 1280px;
- > * {
- min-width: 250px;
- }
- }
`
diff --git a/src/renderer/src/pages/home/Messages/CitationsList.tsx b/src/renderer/src/pages/home/Messages/CitationsList.tsx
index 672587fff5..f147619adc 100644
--- a/src/renderer/src/pages/home/Messages/CitationsList.tsx
+++ b/src/renderer/src/pages/home/Messages/CitationsList.tsx
@@ -1,10 +1,9 @@
import ContextMenu from '@renderer/components/ContextMenu'
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, message, Skeleton } from 'antd'
+import { Button, message, Popover, Skeleton } from 'antd'
import { Check, Copy, FileSearch } from 'lucide-react'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -48,16 +47,53 @@ const truncateText = (text: string, maxLength = 100) => {
const CitationsList: React.FC = ({ citations }) => {
const { t } = useTranslation()
- const [open, setOpen] = useState(false)
const previewItems = citations.slice(0, 3)
const count = citations.length
if (!count) return null
+ const popoverContent = (
+
+ {citations.map((citation) => (
+
+ {citation.type === 'websearch' ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+ ))}
+
+ )
+
return (
- <>
- setOpen(true)}>
+
+ {t('message.citations')}
+
+ }
+ placement="right"
+ trigger="hover"
+ styles={{
+ body: {
+ padding: '0 0 8px 0'
+ }
+ }}>
+
{previewItems.map((c, i) => (
@@ -71,27 +107,7 @@ const CitationsList: React.FC = ({ citations }) => {
{t('message.citation', { count })}
-
- setOpen(false)}
- open={open}
- width={680}
- styles={{ header: { border: 'none' }, body: { paddingTop: 0 } }}
- destroyOnClose={false}>
- {open &&
- citations.map((citation) => (
-
- {citation.type === 'websearch' ? (
-
- ) : (
-
- )}
-
- ))}
-
- >
+