Perf/optimize rendering (#3923)

* optimize useMessageOperations

* chore: update dependencies and refactor React imports

- Added @ant-design/v5-patch-for-react-19 and rc-virtual-list to package.json.
- Updated React and ReactDOM types to version 19 in package.json and yarn.lock.
- Refactored ReactDOM usage to createRoot in main.tsx for better compatibility with React 18+.
- Changed useContext to use in SyntaxHighlighterProvider and ThemeProvider components.
- Adjusted flex-direction in Messages components to column for improved layout.
- Removed unused state in CodeBlock component.

* refactor(Messages): enhance scrolling behavior and introduce scroll utilities

- Added createScrollHandler and scrollToBottom utilities for improved scroll management.
- Updated Messages component to utilize new scroll utilities for better user experience.
- Refactored scroll handling logic to ensure smooth scrolling when new messages are added.
- Changed containerRef type to HTMLElement for better type safety.

* refactor(Messages): streamline message handling and introduce useTopicMessages hook

- Removed direct message selection from useMessageOperations and created a new useTopicMessages hook for better separation of concerns.
- Updated Messages component to utilize the new useTopicMessages hook for fetching messages.
- Enhanced message display logic with computeDisplayMessages function for improved message rendering.
- Refactored scrolling behavior to maintain a smooth user experience during message updates.

* refactor(Message Operations): introduce useTopicLoading hook for improved loading state management

- Removed loading state from useMessageOperations and created a new useTopicLoading hook for better separation of concerns.
- Updated components to utilize the new useTopicLoading hook for fetching loading states related to topics.
- Enhanced code organization and readability by streamlining message operations and loading state handling.

* refactor(Messages): replace updateMessage with updateMessageThunk for improved async handling

- Updated useMessageOperations and MessageAnchorLine components to utilize updateMessageThunk for message updates.
- Enhanced error handling and database synchronization in the new thunk implementation.
- Streamlined message update logic to improve code clarity and maintainability.

* refactor(SyntaxHighlighterProvider, MessageTools, AddMcpServerPopup): update styles and improve type safety

- Changed import statements to use TypeScript's type imports for better clarity and type safety.
- Updated MessageTools and AddMcpServerPopup components to replace bodyStyle with styles prop for consistent styling approach.
- Enhanced overall code organization and maintainability by adhering to TypeScript best practices.

* refactor(Messages): update layout and remove unnecessary prop

- Removed the hasChildren prop from the Messages component for cleaner code.
- Adjusted flex-direction in the mini chat Messages component to column-reverse for improved layout consistency.

* refactor: enhance type safety and component return types

- Updated functional components to return React.ReactElement instead of JSX.Element for better type consistency.
- Changed import statements to use TypeScript's type imports for improved clarity.
- Initialized useRef hooks with null for better type safety in various components.
- Adjusted props types to use HTMLAttributes for more accurate type definitions.

* chore: update package dependencies

- Removed outdated dependencies: @agentic/exa, @agentic/searxng, @agentic/tavily, and rc-virtual-list.
- Added back @ant-design/v5-patch-for-react-19 and rc-virtual-list with specified versions for improved compatibility.

* fix(useMessageOperations): ensure message retrieval from store when updating content
This commit is contained in:
MyPrototypeWhat 2025-03-27 12:06:47 +08:00 committed by GitHub
parent 7fb85dc311
commit fb9c23c500
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 293 additions and 248 deletions

View File

@ -92,6 +92,7 @@
"@agentic/exa": "^7.3.3", "@agentic/exa": "^7.3.3",
"@agentic/searxng": "^7.3.3", "@agentic/searxng": "^7.3.3",
"@agentic/tavily": "^7.3.3", "@agentic/tavily": "^7.3.3",
"@ant-design/v5-patch-for-react-19": "^1.0.3",
"@anthropic-ai/sdk": "^0.38.0", "@anthropic-ai/sdk": "^0.38.0",
"@electron-toolkit/eslint-config-prettier": "^3.0.0", "@electron-toolkit/eslint-config-prettier": "^3.0.0",
"@electron-toolkit/eslint-config-ts": "^3.0.0", "@electron-toolkit/eslint-config-ts": "^3.0.0",
@ -114,8 +115,8 @@
"@types/md5": "^2.3.5", "@types/md5": "^2.3.5",
"@types/node": "^18.19.9", "@types/node": "^18.19.9",
"@types/pako": "^1.0.2", "@types/pako": "^1.0.2",
"@types/react": "^18.2.48", "@types/react": "^19.0.12",
"@types/react-dom": "^18.2.18", "@types/react-dom": "^19.0.4",
"@types/react-infinite-scroll-component": "^5.0.0", "@types/react-infinite-scroll-component": "^5.0.0",
"@types/tinycolor2": "^1", "@types/tinycolor2": "^1",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
@ -149,8 +150,9 @@
"openai": "patch:openai@npm%3A4.77.3#~/.yarn/patches/openai-npm-4.77.3-59c6d42e7a.patch", "openai": "patch:openai@npm%3A4.77.3#~/.yarn/patches/openai-npm-4.77.3-59c6d42e7a.patch",
"p-queue": "^8.1.0", "p-queue": "^8.1.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"react": "^18.2.0", "rc-virtual-list": "^3.18.5",
"react-dom": "^18.2.0", "react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hotkeys-hook": "^4.6.1", "react-hotkeys-hook": "^4.6.1",
"react-i18next": "^14.1.2", "react-i18next": "^14.1.2",
"react-infinite-scroll-component": "^6.1.0", "react-infinite-scroll-component": "^6.1.0",
@ -177,10 +179,6 @@
"uuid": "^10.0.0", "uuid": "^10.0.0",
"vite": "^5.0.12" "vite": "^5.0.12"
}, },
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0"
},
"resolutions": { "resolutions": {
"pdf-parse@npm:1.1.1": "patch:pdf-parse@npm%3A1.1.1#~/.yarn/patches/pdf-parse-npm-1.1.1-04a6109b2a.patch", "pdf-parse@npm:1.1.1": "patch:pdf-parse@npm%3A1.1.1#~/.yarn/patches/pdf-parse-npm-1.1.1-04a6109b2a.patch",
"@langchain/openai@npm:^0.3.16": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch", "@langchain/openai@npm:^0.3.16": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch",

View File

@ -21,7 +21,7 @@ import PaintingsPage from './pages/paintings/PaintingsPage'
import SettingsPage from './pages/settings/SettingsPage' import SettingsPage from './pages/settings/SettingsPage'
import TranslatePage from './pages/translate/TranslatePage' import TranslatePage from './pages/translate/TranslatePage'
function App(): JSX.Element { function App(): React.ReactElement {
return ( return (
<Provider store={store}> <Provider store={store}>
<StyleSheetManager> <StyleSheetManager>

View File

@ -8,6 +8,7 @@ import {
OnDragStartResponder, OnDragStartResponder,
ResponderProvided ResponderProvided
} from '@hello-pangea/dnd' } from '@hello-pangea/dnd'
import VirtualList from 'rc-virtual-list'
import { droppableReorder } from '@renderer/utils' import { droppableReorder } from '@renderer/utils'
import { FC } from 'react' import { FC } from 'react'
@ -47,7 +48,8 @@ const DragableList: FC<Props<any>> = ({
<Droppable droppableId="droppable" {...droppableProps}> <Droppable droppableId="droppable" {...droppableProps}>
{(provided) => ( {(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef} style={style}> <div {...provided.droppableProps} ref={provided.innerRef} style={style}>
{list.map((item, index) => { <VirtualList data={list} itemKey="id">
{(item, index) => {
const id = item.id || item const id = item.id || item
return ( return (
<Draggable key={`draggable_${id}_${index}`} draggableId={id} index={index}> <Draggable key={`draggable_${id}_${index}`} draggableId={id} index={index}>
@ -66,7 +68,8 @@ const DragableList: FC<Props<any>> = ({
)} )}
</Draggable> </Draggable>
) )
})} }}
</VirtualList>
{provided.placeholder} {provided.placeholder}
</div> </div>
)} )}

View File

@ -1,9 +1,10 @@
import { isMac } from '@renderer/config/constant' import { isMac } from '@renderer/config/constant'
import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor' import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor'
import { FC, PropsWithChildren } from 'react' import type { FC, PropsWithChildren } from 'react'
import type { HTMLAttributes } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
type Props = PropsWithChildren & JSX.IntrinsicElements['div'] type Props = PropsWithChildren & HTMLAttributes<HTMLDivElement>
export const Navbar: FC<Props> = ({ children, ...props }) => { export const Navbar: FC<Props> = ({ children, ...props }) => {
const backgroundColor = useNavBackgroundColor() const backgroundColor = useNavBackgroundColor()

View File

@ -1,12 +1,12 @@
import isPropValid from '@emotion/is-prop-valid' import isPropValid from '@emotion/is-prop-valid'
import { ReactNode } from 'react' import type { ReactNode } from 'react'
import { StyleSheetManager as StyledComponentsStyleSheetManager } from 'styled-components' import { StyleSheetManager as StyledComponentsStyleSheetManager } from 'styled-components'
interface StyleSheetManagerProps { interface StyleSheetManagerProps {
children: ReactNode children: ReactNode
} }
const StyleSheetManager = ({ children }: StyleSheetManagerProps): JSX.Element => { const StyleSheetManager = ({ children }: StyleSheetManagerProps): React.ReactElement => {
return ( return (
<StyledComponentsStyleSheetManager <StyledComponentsStyleSheetManager
shouldForwardProp={(prop, element) => { shouldForwardProp={(prop, element) => {

View File

@ -1,16 +1,11 @@
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useMermaid } from '@renderer/hooks/useMermaid' import { useMermaid } from '@renderer/hooks/useMermaid'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { CodeStyleVarious, ThemeMode } from '@renderer/types' import { type CodeStyleVarious, ThemeMode } from '@renderer/types'
import React, { createContext, PropsWithChildren, useContext, useEffect, useMemo, useState } from 'react' import type React from 'react'
import { import { createContext, type PropsWithChildren, use, useCallback, useEffect, useMemo, useState } from 'react'
BundledLanguage, import type { BundledLanguage, BundledTheme, HighlighterGeneric } from 'shiki'
bundledLanguages, import { bundledLanguages, bundledThemes, createHighlighter } from 'shiki'
BundledTheme,
bundledThemes,
createHighlighter,
HighlighterGeneric
} from 'shiki'
interface SyntaxHighlighterContextType { interface SyntaxHighlighterContextType {
codeToHtml: (code: string, language: string) => Promise<string> codeToHtml: (code: string, language: string) => Promise<string>
@ -51,7 +46,9 @@ export const SyntaxHighlighterProvider: React.FC<PropsWithChildren> = ({ childre
initHighlighter() initHighlighter()
}, [highlighterTheme]) }, [highlighterTheme])
const codeToHtml = async (code: string, language: string) => { const codeToHtml = useCallback(
async (_code: string, language: string) => {
{
if (!highlighter) return '' if (!highlighter) return ''
const languageMap: Record<string, string> = { const languageMap: Record<string, string> = {
@ -60,7 +57,7 @@ export const SyntaxHighlighterProvider: React.FC<PropsWithChildren> = ({ childre
const mappedLanguage = languageMap[language] || language const mappedLanguage = languageMap[language] || language
code = code?.trimEnd() ?? '' const code = _code?.trimEnd() ?? ''
const escapedCode = code?.replace(/[<>]/g, (char) => ({ '<': '&lt;', '>': '&gt;' })[char]!) const escapedCode = code?.replace(/[<>]/g, (char) => ({ '<': '&lt;', '>': '&gt;' })[char]!)
try { try {
@ -81,12 +78,15 @@ export const SyntaxHighlighterProvider: React.FC<PropsWithChildren> = ({ childre
return `<pre style="padding: 10px"><code>${escapedCode}</code></pre>` return `<pre style="padding: 10px"><code>${escapedCode}</code></pre>`
} }
} }
},
[highlighter, highlighterTheme]
)
return <SyntaxHighlighterContext.Provider value={{ codeToHtml }}>{children}</SyntaxHighlighterContext.Provider> return <SyntaxHighlighterContext.Provider value={{ codeToHtml }}>{children}</SyntaxHighlighterContext.Provider>
} }
export const useSyntaxHighlighter = () => { export const useSyntaxHighlighter = () => {
const context = useContext(SyntaxHighlighterContext) const context = use(SyntaxHighlighterContext)
if (!context) { if (!context) {
throw new Error('useSyntaxHighlighter must be used within a SyntaxHighlighterProvider') throw new Error('useSyntaxHighlighter must be used within a SyntaxHighlighterProvider')
} }

View File

@ -1,7 +1,7 @@
import { isMac } from '@renderer/config/constant' import { isMac } from '@renderer/config/constant'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { ThemeMode } from '@renderer/types' import { ThemeMode } from '@renderer/types'
import React, { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react' import React, { createContext, PropsWithChildren, use, useEffect, useState } from 'react'
interface ThemeContextType { interface ThemeContextType {
theme: ThemeMode theme: ThemeMode
@ -64,4 +64,4 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children, defaultT
) )
} }
export const useTheme = () => useContext(ThemeContext) export const useTheme = () => use(ThemeContext)

View File

@ -5,14 +5,15 @@ import {
clearStreamMessage, clearStreamMessage,
clearTopicMessages, clearTopicMessages,
commitStreamMessage, commitStreamMessage,
deleteMessageAction,
resendMessage, resendMessage,
selectDisplayCount, selectDisplayCount,
selectTopicLoading, selectTopicLoading,
selectTopicMessages, selectTopicMessages,
setStreamMessage, setStreamMessage,
setTopicLoading, setTopicLoading,
updateMessage, updateMessages,
updateMessages updateMessageThunk
} from '@renderer/store/messages' } from '@renderer/store/messages'
import type { Assistant, Message, Topic } from '@renderer/types' import type { Assistant, Message, Topic } from '@renderer/types'
import { abortCompletion } from '@renderer/utils/abortController' import { abortCompletion } from '@renderer/utils/abortController'
@ -27,17 +28,15 @@ import { TopicManager } from './useTopic'
*/ */
export function useMessageOperations(topic: Topic) { export function useMessageOperations(topic: Topic) {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const messages = useAppSelector((state) => selectTopicMessages(state, topic.id))
/** /**
* *
*/ */
const deleteMessage = useCallback( const deleteMessage = useCallback(
async (message: Message) => { async (id: string) => {
const newMessages = messages.filter((m) => m.id !== message.id) await dispatch(deleteMessageAction(topic, id))
await dispatch(updateMessages(topic, newMessages))
}, },
[dispatch, topic, messages] [dispatch, topic]
) )
/** /**
@ -45,10 +44,9 @@ export function useMessageOperations(topic: Topic) {
*/ */
const deleteGroupMessages = useCallback( const deleteGroupMessages = useCallback(
async (askId: string) => { async (askId: string) => {
const newMessages = messages.filter((m) => m.askId !== askId) await dispatch(deleteMessageAction(topic, askId, 'askId'))
await dispatch(updateMessages(topic, newMessages))
}, },
[dispatch, topic, messages] [dispatch, topic]
) )
/** /**
@ -58,23 +56,17 @@ export function useMessageOperations(topic: Topic) {
async (messageId: string, updates: Partial<Message>) => { async (messageId: string, updates: Partial<Message>) => {
// 如果更新包含内容变更,重新计算 token // 如果更新包含内容变更,重新计算 token
if ('content' in updates) { if ('content' in updates) {
const message = messages.find((m) => m.id === messageId) const messages = store.getState().messages.messagesByTopic[topic.id]
const message = messages?.find((m) => m.id === messageId)
if (message) { if (message) {
const updatedMessage = { ...message, ...updates } const updatedMessage = { ...message, ...updates }
const usage = await estimateMessageUsage(updatedMessage) const usage = await estimateMessageUsage(updatedMessage)
updates.usage = usage updates.usage = usage
} }
} }
await dispatch(updateMessageThunk(topic.id, messageId, updates))
await dispatch(
updateMessage({
topicId: topic.id,
messageId,
updates
})
)
}, },
[dispatch, topic.id, messages] [dispatch, topic.id]
) )
/** /**
@ -159,7 +151,6 @@ export function useMessageOperations(topic: Topic) {
EventEmitter.emit(EVENT_NAMES.NEW_CONTEXT) EventEmitter.emit(EVENT_NAMES.NEW_CONTEXT)
}, []) }, [])
const loading = useAppSelector((state) => selectTopicLoading(state, topic.id))
const displayCount = useAppSelector(selectDisplayCount) const displayCount = useAppSelector(selectDisplayCount)
// /** // /**
// * 获取当前消息列表 // * 获取当前消息列表
@ -211,8 +202,6 @@ export function useMessageOperations(topic: Topic) {
) )
return { return {
messages,
loading,
displayCount, displayCount,
updateMessages: updateMessagesAction, updateMessages: updateMessagesAction,
deleteMessage, deleteMessage,
@ -230,3 +219,13 @@ export function useMessageOperations(topic: Topic) {
resumeMessage resumeMessage
} }
} }
export const useTopicMessages = (topic: Topic) => {
const messages = useAppSelector((state) => selectTopicMessages(state, topic.id))
return messages
}
export const useTopicLoading = (topic: Topic) => {
const loading = useAppSelector((state) => selectTopicLoading(state, topic.id))
return loading
}

View File

@ -1,13 +1,16 @@
import './assets/styles/index.scss' import './assets/styles/index.scss'
import '@ant-design/v5-patch-for-react-19'
import ReactDOM from 'react-dom/client' import { createRoot } from 'react-dom/client'
import App from './App' import App from './App'
import MiniApp from './windows/mini/App' import MiniApp from './windows/mini/App'
if (location.hash === '#/mini') { if (location.hash === '#/mini') {
document.getElementById('spinner')?.remove() document.getElementById('spinner')?.remove()
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<MiniApp />) const root = createRoot(document.getElementById('root') as HTMLElement)
root.render(<MiniApp />)
} else { } else {
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<App />) const root = createRoot(document.getElementById('root') as HTMLElement)
root.render(<App />)
} }

View File

@ -13,7 +13,7 @@ import TranslateButton from '@renderer/components/TranslateButton'
import { isFunctionCallingModel, isGenerateImageModel, isVisionModel, isWebSearchModel } from '@renderer/config/models' import { isFunctionCallingModel, isGenerateImageModel, isVisionModel, isWebSearchModel } from '@renderer/config/models'
import db from '@renderer/databases' import db from '@renderer/databases'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { useMessageOperations } from '@renderer/hooks/useMessageOperations' import { useMessageOperations, useTopicLoading } from '@renderer/hooks/useMessageOperations'
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime' import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings' import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
import { useShortcut, useShortcutDisplay } from '@renderer/hooks/useShortcuts' import { useShortcut, useShortcutDisplay } from '@renderer/hooks/useShortcuts'
@ -83,10 +83,11 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
const containerRef = useRef(null) const containerRef = useRef(null)
const { searching } = useRuntime() const { searching } = useRuntime()
const { isBubbleStyle } = useMessageStyle() const { isBubbleStyle } = useMessageStyle()
const { loading, pauseMessages } = useMessageOperations(topic) const { pauseMessages } = useMessageOperations(topic)
const loading = useTopicLoading(topic)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const [spaceClickCount, setSpaceClickCount] = useState(0) const [spaceClickCount, setSpaceClickCount] = useState(0)
const spaceClickTimer = useRef<NodeJS.Timeout>() const spaceClickTimer = useRef<NodeJS.Timeout>(null)
const [isTranslating, setIsTranslating] = useState(false) const [isTranslating, setIsTranslating] = useState(false)
const [selectedKnowledgeBases, setSelectedKnowledgeBases] = useState<KnowledgeBase[]>([]) const [selectedKnowledgeBases, setSelectedKnowledgeBases] = useState<KnowledgeBase[]>([])
const [mentionModels, setMentionModels] = useState<Model[]>([]) const [mentionModels, setMentionModels] = useState<Model[]>([])
@ -96,7 +97,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
const [textareaHeight, setTextareaHeight] = useState<number>() const [textareaHeight, setTextareaHeight] = useState<number>()
const startDragY = useRef<number>(0) const startDragY = useRef<number>(0)
const startHeight = useRef<number>(0) const startHeight = useRef<number>(0)
const currentMessageId = useRef<string>() const currentMessageId = useRef<string>('')
const isVision = useMemo(() => isVisionModel(model), [model]) const isVision = useMemo(() => isVisionModel(model), [model])
const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision]) const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision])
const navigate = useNavigate() const navigate = useNavigate()

View File

@ -26,7 +26,7 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
const match = /language-(\w+)/.exec(className || '') || children?.includes('\n') const match = /language-(\w+)/.exec(className || '') || children?.includes('\n')
const { codeShowLineNumbers, fontSize, codeCollapsible, codeWrappable } = useSettings() const { codeShowLineNumbers, fontSize, codeCollapsible, codeWrappable } = useSettings()
const language = match?.[1] ?? 'text' const language = match?.[1] ?? 'text'
const [html, setHtml] = useState<string>('') // const [html, setHtml] = useState<string>('')
const { codeToHtml } = useSyntaxHighlighter() const { codeToHtml } = useSyntaxHighlighter()
const [isExpanded, setIsExpanded] = useState(!codeCollapsible) const [isExpanded, setIsExpanded] = useState(!codeCollapsible)
const [isUnwrapped, setIsUnwrapped] = useState(!codeWrappable) const [isUnwrapped, setIsUnwrapped] = useState(!codeWrappable)
@ -40,17 +40,14 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
useEffect(() => { useEffect(() => {
const loadHighlightedCode = async () => { const loadHighlightedCode = async () => {
const highlightedHtml = await codeToHtml(children, language) const highlightedHtml = await codeToHtml(children, language)
setHtml(highlightedHtml) if (codeContentRef.current) {
codeContentRef.current.innerHTML = highlightedHtml
setShouldShowExpandButton(codeContentRef.current.scrollHeight > 350)
}
} }
loadHighlightedCode() loadHighlightedCode()
}, [children, language, codeToHtml]) }, [children, language, codeToHtml])
useEffect(() => {
if (codeContentRef.current) {
setShouldShowExpandButton(codeContentRef.current.scrollHeight > 350)
}
}, [html])
useEffect(() => { useEffect(() => {
if (!codeCollapsible) { if (!codeCollapsible) {
setIsExpanded(true) setIsExpanded(true)
@ -112,7 +109,7 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
isShowLineNumbers={codeShowLineNumbers} isShowLineNumbers={codeShowLineNumbers}
isUnwrapped={isUnwrapped} isUnwrapped={isUnwrapped}
isCodeWrappable={codeWrappable} isCodeWrappable={codeWrappable}
dangerouslySetInnerHTML={{ __html: html }} // dangerouslySetInnerHTML={{ __html: html }}
style={{ style={{
border: '0.5px solid var(--color-code-background)', border: '0.5px solid var(--color-code-background)',
borderTopLeftRadius: 0, borderTopLeftRadius: 0,

View File

@ -50,7 +50,7 @@ const CustomNode: FC<{ data: any }> = ({ data }) => {
let title = '' let title = ''
let backgroundColor = 'var(--bg-color)' let backgroundColor = 'var(--bg-color)'
let gradientColor = 'rgba(0, 0, 0, 0.03)' let gradientColor = 'rgba(0, 0, 0, 0.03)'
let avatar: JSX.Element | null = null let avatar: React.ReactNode | null = null
// 根据消息类型设置不同的样式和图标 // 根据消息类型设置不同的样式和图标
if (nodeType === 'user') { if (nodeType === 'user') {

View File

@ -163,7 +163,7 @@ const MessageItem: FC<Props> = ({
isLastMessage={isLastMessage} isLastMessage={isLastMessage}
isAssistantMessage={isAssistantMessage} isAssistantMessage={isAssistantMessage}
isGrouped={isGrouped} isGrouped={isGrouped}
messageContainerRef={messageContainerRef} messageContainerRef={messageContainerRef as React.RefObject<HTMLDivElement>}
setModel={setModel} setModel={setModel}
/> />
</MessageFooter> </MessageFooter>

View File

@ -6,11 +6,11 @@ import { useSettings } from '@renderer/hooks/useSettings'
import { getMessageModelId } from '@renderer/services/MessagesService' import { getMessageModelId } from '@renderer/services/MessagesService'
import { getModelName } from '@renderer/services/ModelService' import { getModelName } from '@renderer/services/ModelService'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { updateMessage } from '@renderer/store/messages' import { updateMessageThunk } from '@renderer/store/messages'
import { Message } from '@renderer/types' import type { Message } from '@renderer/types'
import { isEmoji, removeLeadingEmoji } from '@renderer/utils' import { isEmoji, removeLeadingEmoji } from '@renderer/utils'
import { Avatar } from 'antd' import { Avatar } from 'antd'
import { FC, useCallback, useEffect, useRef, useState } from 'react' import { type FC, useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
interface MessageLineProps { interface MessageLineProps {
@ -100,15 +100,9 @@ const MessageAnchorLine: FC<MessageLineProps> = ({ messages }) => {
(message: Message) => { (message: Message) => {
const groupMessages = messages.filter((m) => m.askId === message.askId) const groupMessages = messages.filter((m) => m.askId === message.askId)
if (groupMessages.length > 1) { if (groupMessages.length > 1) {
groupMessages.forEach((m) => { for (const m of groupMessages) {
dispatch( dispatch(updateMessageThunk(m.topicId, m.id, { foldSelected: m.id === message.id }))
updateMessage({ }
topicId: m.topicId,
messageId: m.id,
updates: { foldSelected: m.id === message.id }
})
)
})
setTimeout(() => { setTimeout(() => {
const messageElement = document.getElementById(`message-${message.id}`) const messageElement = document.getElementById(`message-${message.id}`)

View File

@ -17,12 +17,12 @@ import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
import TextEditPopup from '@renderer/components/Popups/TextEditPopup' import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
import { isReasoningModel } from '@renderer/config/models' import { isReasoningModel } from '@renderer/config/models'
import { TranslateLanguageOptions } from '@renderer/config/translate' import { TranslateLanguageOptions } from '@renderer/config/translate'
import { useMessageOperations } from '@renderer/hooks/useMessageOperations' import { useMessageOperations, useTopicLoading } from '@renderer/hooks/useMessageOperations'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { getMessageTitle, resetAssistantMessage } from '@renderer/services/MessagesService' import { getMessageTitle, resetAssistantMessage } from '@renderer/services/MessagesService'
import { translateText } from '@renderer/services/TranslateService' import { translateText } from '@renderer/services/TranslateService'
import { Message, Model } from '@renderer/types' import type { Message, Model } from '@renderer/types'
import { Assistant, Topic } from '@renderer/types' import type { Assistant, Topic } from '@renderer/types'
import { captureScrollableDivAsBlob, captureScrollableDivAsDataURL, removeTrailingDoubleSpaces } from '@renderer/utils' import { captureScrollableDivAsBlob, captureScrollableDivAsDataURL, removeTrailingDoubleSpaces } from '@renderer/utils'
import { import {
exportMarkdownToJoplin, exportMarkdownToJoplin,
@ -62,15 +62,9 @@ const MessageMenubar: FC<Props> = (props) => {
const [showRegenerateTooltip, setShowRegenerateTooltip] = useState(false) const [showRegenerateTooltip, setShowRegenerateTooltip] = useState(false)
const [showDeleteTooltip, setShowDeleteTooltip] = useState(false) const [showDeleteTooltip, setShowDeleteTooltip] = useState(false)
const assistantModel = assistant?.model const assistantModel = assistant?.model
const { const { editMessage, setStreamMessage, deleteMessage, resendMessage, commitStreamMessage, clearStreamMessage } =
loading, useMessageOperations(topic)
editMessage, const loading = useTopicLoading(topic)
setStreamMessage,
deleteMessage,
resendMessage,
commitStreamMessage,
clearStreamMessage
} = useMessageOperations(topic)
const isUserMessage = message.role === 'user' const isUserMessage = message.role === 'user'
@ -382,7 +376,7 @@ const MessageMenubar: FC<Props> = (props) => {
okButtonProps={{ danger: true }} okButtonProps={{ danger: true }}
icon={<QuestionCircleOutlined style={{ color: 'red' }} />} icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
onOpenChange={(open) => open && setShowDeleteTooltip(false)} onOpenChange={(open) => open && setShowDeleteTooltip(false)}
onConfirm={() => deleteMessage(message)}> onConfirm={() => deleteMessage(message.id)}>
<ActionButton className="message-action-button" onClick={(e) => e.stopPropagation()}> <ActionButton className="message-action-button" onClick={(e) => e.stopPropagation()}>
<Tooltip <Tooltip
title={t('common.delete')} title={t('common.delete')}

View File

@ -1,6 +1,6 @@
import { CheckOutlined, ExpandOutlined, LoadingOutlined } from '@ant-design/icons' import { CheckOutlined, ExpandOutlined, LoadingOutlined } from '@ant-design/icons'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { MCPToolResponse, Message } from '@renderer/types' import { Message } from '@renderer/types'
import { Collapse, message as antdMessage, Modal, Tooltip } from 'antd' import { Collapse, message as antdMessage, Modal, Tooltip } from 'antd'
import { isEmpty } from 'lodash' import { isEmpty } from 'lodash'
import { FC, useMemo, useState } from 'react' import { FC, useMemo, useState } from 'react'
@ -42,9 +42,9 @@ const MessageTools: FC<Props> = ({ message }) => {
// Format tool responses for collapse items // Format tool responses for collapse items
const getCollapseItems = () => { const getCollapseItems = () => {
const items: { key: string; label: JSX.Element; children: React.ReactNode }[] = [] const items: { key: string; label: React.ReactNode; children: React.ReactNode }[] = []
// Add tool responses // Add tool responses
toolResponses.forEach((toolResponse: MCPToolResponse) => { for (const toolResponse of toolResponses) {
const { id, tool, status, response } = toolResponse const { id, tool, status, response } = toolResponse
const isInvoking = status === 'invoking' const isInvoking = status === 'invoking'
const isDone = status === 'done' const isDone = status === 'done'
@ -105,7 +105,7 @@ const MessageTools: FC<Props> = ({ message }) => {
</ToolResponseContainer> </ToolResponseContainer>
) )
}) })
}) }
return items return items
} }
@ -129,7 +129,9 @@ const MessageTools: FC<Props> = ({ message }) => {
onCancel={() => setExpandedResponse(null)} onCancel={() => setExpandedResponse(null)}
footer={null} footer={null}
width="80%" width="80%"
bodyStyle={{ maxHeight: '80vh', overflow: 'auto' }}> styles={{
body: { maxHeight: '80vh', overflow: 'auto' }
}}>
{expandedResponse && ( {expandedResponse && (
<ExpandedResponseContainer style={{ fontFamily, fontSize }}> <ExpandedResponseContainer style={{ fontFamily, fontSize }}>
<ActionButton <ActionButton

View File

@ -2,7 +2,7 @@ import Scrollbar from '@renderer/components/Scrollbar'
import { LOAD_MORE_COUNT } from '@renderer/config/constant' import { LOAD_MORE_COUNT } from '@renderer/config/constant'
import db from '@renderer/databases' import db from '@renderer/databases'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { useMessageOperations } from '@renderer/hooks/useMessageOperations' import { useMessageOperations, useTopicLoading, useTopicMessages } from '@renderer/hooks/useMessageOperations'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { useShortcut } from '@renderer/hooks/useShortcuts' import { useShortcut } from '@renderer/hooks/useShortcuts'
import { autoRenameTopic, getTopic } from '@renderer/hooks/useTopic' import { autoRenameTopic, getTopic } from '@renderer/hooks/useTopic'
@ -38,6 +38,42 @@ interface MessagesProps {
setActiveTopic: (topic: Topic) => void setActiveTopic: (topic: Topic) => void
} }
const computeDisplayMessages = (messages: Message[], startIndex: number, displayCount: number) => {
const reversedMessages = [...messages].reverse()
// 如果剩余消息数量小于 displayCount直接返回所有剩余消息
if (reversedMessages.length - startIndex <= displayCount) {
return reversedMessages.slice(startIndex)
}
const userIdSet = new Set() // 用户消息 id 集合
const assistantIdSet = new Set() // 助手消息 askId 集合
const displayMessages: Message[] = []
// 处理单条消息的函数
const processMessage = (message: Message) => {
if (!message) return
const idSet = message.role === 'user' ? userIdSet : assistantIdSet
const messageId = message.role === 'user' ? message.id : message.askId
if (!idSet.has(messageId)) {
idSet.add(messageId)
displayMessages.push(message)
return
}
// 如果是相同 askId 的助手消息,也要显示
displayMessages.push(message)
}
// 遍历消息直到满足显示数量要求
for (let i = startIndex; i < reversedMessages.length && userIdSet.size + assistantIdSet.size < displayCount; i++) {
processMessage(reversedMessages[i])
}
return displayMessages
}
const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic }) => { const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { showTopics, topicPosition, showAssistants, messageNavigation } = useSettings() const { showTopics, topicPosition, showAssistants, messageNavigation } = useSettings()
@ -48,9 +84,9 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
const [hasMore, setHasMore] = useState(false) const [hasMore, setHasMore] = useState(false)
const [isLoadingMore, setIsLoadingMore] = useState(false) const [isLoadingMore, setIsLoadingMore] = useState(false)
const [isProcessingContext, setIsProcessingContext] = useState(false) const [isProcessingContext, setIsProcessingContext] = useState(false)
const { messages, displayCount, loading, updateMessages, clearTopicMessages, deleteMessage } = const messages = useTopicMessages(topic)
useMessageOperations(topic) const { displayCount, updateMessages, clearTopicMessages, deleteMessage } = useMessageOperations(topic)
const loading = useTopicLoading(topic)
const messagesRef = useRef<Message[]>(messages) const messagesRef = useRef<Message[]>(messages)
useEffect(() => { useEffect(() => {
@ -58,9 +94,7 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
}, [messages]) }, [messages])
useEffect(() => { useEffect(() => {
const reversedMessages = [...messages].reverse() const newDisplayMessages = computeDisplayMessages(messages, 0, displayCount)
const newDisplayMessages = reversedMessages.slice(0, displayCount)
setDisplayMessages(newDisplayMessages) setDisplayMessages(newDisplayMessages)
setHasMore(messages.length > displayCount) setHasMore(messages.length > displayCount)
}, [messages, displayCount]) }, [messages, displayCount])
@ -73,7 +107,15 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
}, [showAssistants, showTopics, topicPosition]) }, [showAssistants, showTopics, topicPosition])
const scrollToBottom = useCallback(() => { const scrollToBottom = useCallback(() => {
setTimeout(() => containerRef.current?.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'auto' }), 50) if (containerRef.current) {
requestAnimationFrame(() => {
if (containerRef.current) {
containerRef.current.scrollTo({
top: containerRef.current.scrollHeight
})
}
})
}
}, []) }, [])
useEffect(() => { useEffect(() => {
@ -122,7 +164,7 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
const lastMessage = last(messages) const lastMessage = last(messages)
if (lastMessage?.type === 'clear') { if (lastMessage?.type === 'clear') {
await deleteMessage(lastMessage) await deleteMessage(lastMessage.id)
scrollToBottom() scrollToBottom()
return return
} }
@ -183,10 +225,9 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
setIsLoadingMore(true) setIsLoadingMore(true)
setTimeout(() => { setTimeout(() => {
const currentLength = displayMessages.length const currentLength = displayMessages.length
const reversedMessages = [...messages].reverse() const newMessages = computeDisplayMessages(messages, currentLength, LOAD_MORE_COUNT)
const moreMessages = reversedMessages.slice(currentLength, currentLength + LOAD_MORE_COUNT)
setDisplayMessages((prev) => [...prev, ...moreMessages]) setDisplayMessages((prev) => [...prev, ...newMessages])
setHasMore(currentLength + LOAD_MORE_COUNT < messages.length) setHasMore(currentLength + LOAD_MORE_COUNT < messages.length)
setIsLoadingMore(false) setIsLoadingMore(false)
}, 300) }, 300)
@ -199,7 +240,6 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
window.message.success(t('message.copy.success')) window.message.success(t('message.copy.success'))
} }
}) })
return ( return (
<Container <Container
id="messages" id="messages"
@ -214,8 +254,8 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
next={loadMoreMessages} next={loadMoreMessages}
hasMore={hasMore} hasMore={hasMore}
loader={null} loader={null}
inverse={true} scrollableTarget="messages"
scrollableTarget="messages"> inverse>
<ScrollContainer> <ScrollContainer>
<LoaderContainer $loading={isLoadingMore}> <LoaderContainer $loading={isLoadingMore}>
<BeatLoader size={8} color="var(--color-text-2)" /> <BeatLoader size={8} color="var(--color-text-2)" />

View File

@ -56,7 +56,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
const borderRadius = showTopicTime ? 12 : 'var(--list-item-border-radius)' const borderRadius = showTopicTime ? 12 : 'var(--list-item-border-radius)'
const [deletingTopicId, setDeletingTopicId] = useState<string | null>(null) const [deletingTopicId, setDeletingTopicId] = useState<string | null>(null)
const deleteTimerRef = useRef<NodeJS.Timeout>() const deleteTimerRef = useRef<NodeJS.Timeout>(null)
const pendingTopics = useMemo(() => { const pendingTopics = useMemo(() => {
return new Set<string>() return new Set<string>()

View File

@ -23,11 +23,12 @@ import { translateText } from '@renderer/services/TranslateService'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { DEFAULT_PAINTING } from '@renderer/store/paintings' import { DEFAULT_PAINTING } from '@renderer/store/paintings'
import { setGenerating } from '@renderer/store/runtime' import { setGenerating } from '@renderer/store/runtime'
import { FileType, Painting } from '@renderer/types' import type { FileType, Painting } from '@renderer/types'
import { getErrorMessage } from '@renderer/utils' import { getErrorMessage } from '@renderer/utils'
import { Button, Input, InputNumber, Radio, Select, Slider, Switch, Tooltip } from 'antd' import { Button, Input, InputNumber, Radio, Select, Slider, Switch, Tooltip } from 'antd'
import TextArea from 'antd/es/input/TextArea' import TextArea from 'antd/es/input/TextArea'
import { FC, useEffect, useRef, useState } from 'react' import type { FC } from 'react'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@ -243,7 +244,7 @@ const PaintingsPage: FC = () => {
const { autoTranslateWithSpace } = useSettings() const { autoTranslateWithSpace } = useSettings()
const [spaceClickCount, setSpaceClickCount] = useState(0) const [spaceClickCount, setSpaceClickCount] = useState(0)
const [isTranslating, setIsTranslating] = useState(false) const [isTranslating, setIsTranslating] = useState(false)
const spaceClickTimer = useRef<NodeJS.Timeout>() const spaceClickTimer = useRef<NodeJS.Timeout>(null)
const translate = async () => { const translate = async () => {
if (isTranslating) { if (isTranslating) {

View File

@ -152,7 +152,12 @@ const PopupContainer: React.FC<Props> = ({ server, create, resolve }) => {
width={600} width={600}
transitionName="ant-move-down" transitionName="ant-move-down"
centered centered
bodyStyle={{ maxHeight: '70vh', overflowY: 'auto' }}> styles={{
body: {
maxHeight: '70vh',
overflowY: 'auto'
}
}}>
<Form form={form} layout="vertical"> <Form form={form} layout="vertical">
<Form.Item <Form.Item
name="name" name="name"

View File

@ -280,7 +280,11 @@ const ShortcutSettings: FC = () => {
<HStack alignItems="center" style={{ position: 'relative' }}> <HStack alignItems="center" style={{ position: 'relative' }}>
{isEditing ? ( {isEditing ? (
<ShortcutInput <ShortcutInput
ref={(el) => el && (inputRefs.current[record.key] = el)} ref={(el) => {
if (el) {
inputRefs.current[record.key] = el
}
}}
value={formatShortcut(shortcut)} value={formatShortcut(shortcut)}
placeholder={t('settings.shortcuts.press_shortcut')} placeholder={t('settings.shortcuts.press_shortcut')}
onKeyDown={(e) => handleKeyDown(e, record)} onKeyDown={(e) => handleKeyDown(e, record)}

View File

@ -6,9 +6,9 @@ import { fetchChatCompletion } from '@renderer/services/ApiService'
import { getAssistantMessage, resetAssistantMessage } from '@renderer/services/MessagesService' import { getAssistantMessage, resetAssistantMessage } from '@renderer/services/MessagesService'
import type { AppDispatch, RootState } from '@renderer/store' import type { AppDispatch, RootState } from '@renderer/store'
import type { Assistant, Message, Topic } from '@renderer/types' import type { Assistant, Message, Topic } from '@renderer/types'
import { Model } from '@renderer/types' import type { Model } from '@renderer/types'
import { clearTopicQueue, getTopicQueue, waitForTopicQueue } from '@renderer/utils/queue' import { clearTopicQueue, getTopicQueue, waitForTopicQueue } from '@renderer/utils/queue'
import { cloneDeep, isEmpty, throttle } from 'lodash' import { isEmpty, throttle } from 'lodash'
export interface MessagesState { export interface MessagesState {
messagesByTopic: Record<string, Message[]> messagesByTopic: Record<string, Message[]>
@ -113,14 +113,10 @@ const messagesSlice = createSlice({
) => { ) => {
const { topicId, messageId, updates } = action.payload const { topicId, messageId, updates } = action.payload
const topicMessages = state.messagesByTopic[topicId] const topicMessages = state.messagesByTopic[topicId]
if (topicMessages) { if (topicMessages) {
const message = topicMessages.find((msg) => msg.id === messageId) const message = topicMessages.find((msg) => msg.id === messageId)
if (message) { if (message) {
Object.assign(message, updates) Object.assign(message, updates)
db.topics.update(topicId, {
messages: topicMessages.map((m) => (m.id === message.id ? cloneDeep(message) : cloneDeep(m)))
})
} }
} }
}, },
@ -255,7 +251,7 @@ export const sendMessage =
const isGroupedMessage = messageToReset.length > 1 const isGroupedMessage = messageToReset.length > 1
const resetMessage = resetAssistantMessage(m, isGroupedMessage ? m.model : assistant.model) const resetMessage = resetAssistantMessage(m, isGroupedMessage ? m.model : assistant.model)
// 更新状态 // 更新状态
dispatch(updateMessage({ topicId: topic.id, messageId: m.id, updates: resetMessage })) dispatch(updateMessageThunk(topic.id, m.id, resetMessage))
// 使用重置后的消息 // 使用重置后的消息
return resetMessage return resetMessage
}) })
@ -263,7 +259,7 @@ export const sendMessage =
const { model, id } = messageToReset const { model, id } = messageToReset
const resetMessage = resetAssistantMessage(messageToReset, model) const resetMessage = resetAssistantMessage(messageToReset, model)
// 更新状态 // 更新状态
dispatch(updateMessage({ topicId: topic.id, messageId: id, updates: resetMessage })) dispatch(updateMessageThunk(topic.id, id, resetMessage))
// 使用重置后的消息 // 使用重置后的消息
assistantMessages.push(resetMessage) assistantMessages.push(resetMessage)
} }
@ -396,10 +392,9 @@ export const sendMessage =
} catch (error: any) { } catch (error: any) {
console.error('Error in chat completion:', error) console.error('Error in chat completion:', error)
dispatch( dispatch(
updateMessage({ updateMessageThunk(topic.id, assistantMessage.id, {
topicId: topic.id, status: 'error',
messageId: assistantMessage.id, error: { message: error.message }
updates: { status: 'error', error: { message: error.message } }
}) })
) )
dispatch(clearStreamMessage({ topicId: topic.id, messageId: assistantMessage.id })) dispatch(clearStreamMessage({ topicId: topic.id, messageId: assistantMessage.id }))
@ -448,10 +443,9 @@ export const resendMessage =
const userMessage = topicMessages.find((m) => m.id === message.askId && m.role === 'user') const userMessage = topicMessages.find((m) => m.id === message.askId && m.role === 'user')
if (!userMessage) { if (!userMessage) {
dispatch( dispatch(
updateMessage({ updateMessageThunk(topic.id, message.id, {
topicId: topic.id, status: 'error',
messageId: message.id, error: { message: i18n.t('error.user_message_not_found') }
updates: { status: 'error', error: { message: i18n.t('error.user_message_not_found') } }
}) })
) )
console.error(i18n.t('error.user_message_not_found')) console.error(i18n.t('error.user_message_not_found'))
@ -521,6 +515,14 @@ export const clearTopicMessagesThunk = (topic: Topic) => async (dispatch: AppDis
} }
} }
export const deleteMessageAction =
(topic: Topic, id: string, idType: 'id' | 'askId' = 'id') =>
async (dispatch: AppDispatch, getState: () => RootState) => {
const messages = getState().messages.messagesByTopic[topic.id] || []
const newMessages = messages.filter((m) => m[idType] !== id)
await dispatch(updateMessages(topic, newMessages))
}
// 修改的 updateMessages thunk同时更新缓存 // 修改的 updateMessages thunk同时更新缓存
export const updateMessages = (topic: Topic, messages: Message[]) => async (dispatch: AppDispatch) => { export const updateMessages = (topic: Topic, messages: Message[]) => async (dispatch: AppDispatch) => {
try { try {
@ -534,6 +536,28 @@ export const updateMessages = (topic: Topic, messages: Message[]) => async (disp
} }
} }
// 新增一个 thunk 来处理消息更新
export const updateMessageThunk =
(topicId: string, messageId: string, updates: Partial<Message>) =>
async (dispatch: AppDispatch, getState: () => RootState) => {
try {
// 先更新 Redux 状态
dispatch(updateMessage({ topicId, messageId, updates }))
// 然后同步到数据库
const state = getState()
const topicMessages = state.messages.messagesByTopic[topicId]
if (topicMessages) {
await db.topics.update(topicId, {
messages: topicMessages
})
}
} catch (error) {
console.error('Failed to update message:', error)
dispatch(setError(error instanceof Error ? error.message : 'Failed to update message'))
}
}
// Selectors // Selectors
export const selectCurrentTopicId = (state: RootState): string | null => { export const selectCurrentTopicId = (state: RootState): string | null => {
const messagesState = state.messages const messagesState = state.messages
@ -546,11 +570,10 @@ export const selectTopicMessages = createSelector(
) )
// 获取特定话题的loading状态 // 获取特定话题的loading状态
export const selectTopicLoading = (state: RootState, topicId?: string): boolean => { export const selectTopicLoading = createSelector(
const messagesState = state.messages as MessagesState [(state: RootState) => state.messages.loadingByTopic, (_, topicId?: string) => topicId],
const currentTopicId = topicId || messagesState.currentTopic?.id || '' (loadingByTopic, topicId) => (topicId ? (loadingByTopic[topicId] ?? false) : false)
return currentTopicId ? (messagesState.loadingByTopic[currentTopicId] ?? false) : false )
}
export const selectDisplayCount = (state: RootState): number => { export const selectDisplayCount = (state: RootState): number => {
const messagesState = state.messages as MessagesState const messagesState = state.messages as MessagesState

View File

@ -306,7 +306,7 @@ export async function captureDiv(divRef: React.RefObject<HTMLDivElement>) {
return Promise.resolve(undefined) return Promise.resolve(undefined)
} }
export const captureScrollableDiv = async (divRef: React.RefObject<HTMLDivElement>) => { export const captureScrollableDiv = async (divRef: React.RefObject<HTMLDivElement | null>) => {
if (divRef.current) { if (divRef.current) {
try { try {
const div = divRef.current const div = divRef.current
@ -392,7 +392,7 @@ export const captureScrollableDiv = async (divRef: React.RefObject<HTMLDivElemen
return Promise.resolve(undefined) return Promise.resolve(undefined)
} }
export const captureScrollableDivAsDataURL = async (divRef: React.RefObject<HTMLDivElement>) => { export const captureScrollableDivAsDataURL = async (divRef: React.RefObject<HTMLDivElement | null>) => {
return captureScrollableDiv(divRef).then((canvas) => { return captureScrollableDiv(divRef).then((canvas) => {
if (canvas) { if (canvas) {
return canvas.toDataURL('image/png') return canvas.toDataURL('image/png')
@ -401,7 +401,10 @@ export const captureScrollableDivAsDataURL = async (divRef: React.RefObject<HTML
}) })
} }
export const captureScrollableDivAsBlob = async (divRef: React.RefObject<HTMLDivElement>, func: BlobCallback) => { export const captureScrollableDivAsBlob = async (
divRef: React.RefObject<HTMLDivElement | null>,
func: BlobCallback
) => {
await captureScrollableDiv(divRef).then((canvas) => { await captureScrollableDiv(divRef).then((canvas) => {
canvas?.toBlob(func, 'image/png') canvas?.toBlob(func, 'image/png')
}) })

View File

@ -9,7 +9,7 @@ import { SyntaxHighlighterProvider } from '../../context/SyntaxHighlighterProvid
import { ThemeProvider } from '../../context/ThemeProvider' import { ThemeProvider } from '../../context/ThemeProvider'
import HomeWindow from './home/HomeWindow' import HomeWindow from './home/HomeWindow'
function MiniWindow(): JSX.Element { function MiniWindow(): React.ReactElement {
return ( return (
<Provider store={store}> <Provider store={store}>
<ThemeProvider> <ThemeProvider>

101
yarn.lock
View File

@ -165,6 +165,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@ant-design/v5-patch-for-react-19@npm:^1.0.3":
version: 1.0.3
resolution: "@ant-design/v5-patch-for-react-19@npm:1.0.3"
peerDependencies:
antd: ">=5.22.6"
react: ">=19.0.0"
react-dom: ">=19.0.0"
checksum: 10c0/e3848c929b01a0a29a41e2886f489932e54d9665dd990c60c4b505e21740da2ef0db0d08f4dc7591651fa1f21b15227e7bb40f40280742225b4da74fd18cc899
languageName: node
linkType: hard
"@anthropic-ai/sdk@npm:^0.38.0": "@anthropic-ai/sdk@npm:^0.38.0":
version: 0.38.0 version: 0.38.0
resolution: "@anthropic-ai/sdk@npm:0.38.0" resolution: "@anthropic-ai/sdk@npm:0.38.0"
@ -3455,19 +3466,12 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/prop-types@npm:*": "@types/react-dom@npm:^19.0.4":
version: 15.7.14 version: 19.0.4
resolution: "@types/prop-types@npm:15.7.14" resolution: "@types/react-dom@npm:19.0.4"
checksum: 10c0/1ec775160bfab90b67a782d735952158c7e702ca4502968aa82565bd8e452c2de8601c8dfe349733073c31179116cf7340710160d3836aa8a1ef76d1532893b1
languageName: node
linkType: hard
"@types/react-dom@npm:^18.2.18":
version: 18.3.5
resolution: "@types/react-dom@npm:18.3.5"
peerDependencies: peerDependencies:
"@types/react": ^18.0.0 "@types/react": ^19.0.0
checksum: 10c0/b163d35a6b32a79f5782574a7aeb12a31a647e248792bf437e6d596e2676961c394c5e3c6e91d1ce44ae90441dbaf93158efb4f051c0d61e2612f1cb04ce4faa checksum: 10c0/4e71853919b94df9e746a4bd73f8180e9ae13016333ce9c543dcba9f4f4c8fe6e28b038ca6ee61c24e291af8e03ca3bc5ded17c46dee938fcb32d71186fda7a3
languageName: node languageName: node
linkType: hard linkType: hard
@ -3480,7 +3484,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/react@npm:*": "@types/react@npm:*, @types/react@npm:^19.0.12":
version: 19.0.12 version: 19.0.12
resolution: "@types/react@npm:19.0.12" resolution: "@types/react@npm:19.0.12"
dependencies: dependencies:
@ -3489,16 +3493,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/react@npm:^18.2.48":
version: 18.3.19
resolution: "@types/react@npm:18.3.19"
dependencies:
"@types/prop-types": "npm:*"
csstype: "npm:^3.0.2"
checksum: 10c0/236bfe0c4748ada1a640f13573eca3e0fc7c9d847b442947adb352b0718d6d285357fd84c33336c8ffb8cbfabc0d58a43a647c7fd79857fecd61fb58ab6f7918
languageName: node
linkType: hard
"@types/responselike@npm:^1.0.0": "@types/responselike@npm:^1.0.0":
version: 1.0.3 version: 1.0.3
resolution: "@types/responselike@npm:1.0.3" resolution: "@types/responselike@npm:1.0.3"
@ -3766,6 +3760,7 @@ __metadata:
"@agentic/exa": "npm:^7.3.3" "@agentic/exa": "npm:^7.3.3"
"@agentic/searxng": "npm:^7.3.3" "@agentic/searxng": "npm:^7.3.3"
"@agentic/tavily": "npm:^7.3.3" "@agentic/tavily": "npm:^7.3.3"
"@ant-design/v5-patch-for-react-19": "npm:^1.0.3"
"@anthropic-ai/sdk": "npm:^0.38.0" "@anthropic-ai/sdk": "npm:^0.38.0"
"@cherrystudio/embedjs": "npm:^0.1.28" "@cherrystudio/embedjs": "npm:^0.1.28"
"@cherrystudio/embedjs-libsql": "npm:^0.1.28" "@cherrystudio/embedjs-libsql": "npm:^0.1.28"
@ -3804,8 +3799,8 @@ __metadata:
"@types/md5": "npm:^2.3.5" "@types/md5": "npm:^2.3.5"
"@types/node": "npm:^18.19.9" "@types/node": "npm:^18.19.9"
"@types/pako": "npm:^1.0.2" "@types/pako": "npm:^1.0.2"
"@types/react": "npm:^18.2.48" "@types/react": "npm:^19.0.12"
"@types/react-dom": "npm:^18.2.18" "@types/react-dom": "npm:^19.0.4"
"@types/react-infinite-scroll-component": "npm:^5.0.0" "@types/react-infinite-scroll-component": "npm:^5.0.0"
"@types/tinycolor2": "npm:^1" "@types/tinycolor2": "npm:^1"
"@vitejs/plugin-react": "npm:^4.2.1" "@vitejs/plugin-react": "npm:^4.2.1"
@ -3853,8 +3848,9 @@ __metadata:
p-queue: "npm:^8.1.0" p-queue: "npm:^8.1.0"
prettier: "npm:^3.5.3" prettier: "npm:^3.5.3"
proxy-agent: "npm:^6.5.0" proxy-agent: "npm:^6.5.0"
react: "npm:^18.2.0" rc-virtual-list: "npm:^3.18.5"
react-dom: "npm:^18.2.0" react: "npm:^19.0.0"
react-dom: "npm:^19.0.0"
react-hotkeys-hook: "npm:^4.6.1" react-hotkeys-hook: "npm:^4.6.1"
react-i18next: "npm:^14.1.2" react-i18next: "npm:^14.1.2"
react-infinite-scroll-component: "npm:^6.1.0" react-infinite-scroll-component: "npm:^6.1.0"
@ -3884,9 +3880,6 @@ __metadata:
vite: "npm:^5.0.12" vite: "npm:^5.0.12"
webdav: "npm:^5.8.0" webdav: "npm:^5.8.0"
zipread: "npm:^1.3.3" zipread: "npm:^1.3.3"
peerDependencies:
react: ^17.0.0 || ^18.0.0
react-dom: ^17.0.0 || ^18.0.0
languageName: unknown languageName: unknown
linkType: soft linkType: soft
@ -9190,7 +9183,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": "js-tokens@npm:^4.0.0":
version: 4.0.0 version: 4.0.0
resolution: "js-tokens@npm:4.0.0" resolution: "js-tokens@npm:4.0.0"
checksum: 10c0/e248708d377aa058eacf2037b07ded847790e6de892bbad3dac0abba2e759cb9f121b00099a65195616badcb6eca8d14d975cb3e89eb1cfda644756402c8aeed checksum: 10c0/e248708d377aa058eacf2037b07ded847790e6de892bbad3dac0abba2e759cb9f121b00099a65195616badcb6eca8d14d975cb3e89eb1cfda644756402c8aeed
@ -9798,17 +9791,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"loose-envify@npm:^1.1.0":
version: 1.4.0
resolution: "loose-envify@npm:1.4.0"
dependencies:
js-tokens: "npm:^3.0.0 || ^4.0.0"
bin:
loose-envify: cli.js
checksum: 10c0/655d110220983c1a4b9c0c679a2e8016d4b67f6e9c7b5435ff5979ecdb20d0813f4dec0a08674fcbdd4846a3f07edbb50a36811fd37930b94aaa0d9daceb017e
languageName: node
linkType: hard
"lop@npm:^0.4.1": "lop@npm:^0.4.1":
version: 0.4.2 version: 0.4.2
resolution: "lop@npm:0.4.2" resolution: "lop@npm:0.4.2"
@ -13272,7 +13254,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"rc-virtual-list@npm:^3.14.2, rc-virtual-list@npm:^3.5.1, rc-virtual-list@npm:^3.5.2": "rc-virtual-list@npm:^3.14.2, rc-virtual-list@npm:^3.18.5, rc-virtual-list@npm:^3.5.1, rc-virtual-list@npm:^3.5.2":
version: 3.18.5 version: 3.18.5
resolution: "rc-virtual-list@npm:3.18.5" resolution: "rc-virtual-list@npm:3.18.5"
dependencies: dependencies:
@ -13301,15 +13283,14 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react-dom@npm:^18.2.0": "react-dom@npm:^19.0.0":
version: 18.3.1 version: 19.0.0
resolution: "react-dom@npm:18.3.1" resolution: "react-dom@npm:19.0.0"
dependencies: dependencies:
loose-envify: "npm:^1.1.0" scheduler: "npm:^0.25.0"
scheduler: "npm:^0.23.2"
peerDependencies: peerDependencies:
react: ^18.3.1 react: ^19.0.0
checksum: 10c0/a752496c1941f958f2e8ac56239172296fcddce1365ce45222d04a1947e0cc5547df3e8447f855a81d6d39f008d7c32eab43db3712077f09e3f67c4874973e85 checksum: 10c0/a36ce7ab507b237ae2759c984cdaad4af4096d8199fb65b3815c16825e5cfeb7293da790a3fc2184b52bfba7ba3ff31c058c01947aff6fd1a3701632aabaa6a9
languageName: node languageName: node
linkType: hard linkType: hard
@ -13480,12 +13461,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react@npm:^18.2.0": "react@npm:^19.0.0":
version: 18.3.1 version: 19.0.0
resolution: "react@npm:18.3.1" resolution: "react@npm:19.0.0"
dependencies: checksum: 10c0/9cad8f103e8e3a16d15cb18a0d8115d8bd9f9e1ce3420310aea381eb42aa0a4f812cf047bb5441349257a05fba8a291515691e3cb51267279b2d2c3253f38471
loose-envify: "npm:^1.1.0"
checksum: 10c0/283e8c5efcf37802c9d1ce767f302dd569dd97a70d9bb8c7be79a789b9902451e0d16334b05d73299b20f048cbc3c7d288bbbde10b701fa194e2089c237dbea3
languageName: node languageName: node
linkType: hard linkType: hard
@ -14183,12 +14162,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"scheduler@npm:^0.23.2": "scheduler@npm:^0.25.0":
version: 0.23.2 version: 0.25.0
resolution: "scheduler@npm:0.23.2" resolution: "scheduler@npm:0.25.0"
dependencies: checksum: 10c0/a4bb1da406b613ce72c1299db43759526058fdcc413999c3c3e0db8956df7633acf395cb20eb2303b6a65d658d66b6585d344460abaee8080b4aa931f10eaafe
loose-envify: "npm:^1.1.0"
checksum: 10c0/26383305e249651d4c58e6705d5f8425f153211aef95f15161c151f7b8de885f24751b377e4a0b3dd42cce09aad3f87a61dab7636859c0d89b7daf1a1e2a5c78
languageName: node languageName: node
linkType: hard linkType: hard