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:
parent
7fb85dc311
commit
fb9c23c500
14
package.json
14
package.json
@ -92,6 +92,7 @@
|
||||
"@agentic/exa": "^7.3.3",
|
||||
"@agentic/searxng": "^7.3.3",
|
||||
"@agentic/tavily": "^7.3.3",
|
||||
"@ant-design/v5-patch-for-react-19": "^1.0.3",
|
||||
"@anthropic-ai/sdk": "^0.38.0",
|
||||
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
||||
"@electron-toolkit/eslint-config-ts": "^3.0.0",
|
||||
@ -114,8 +115,8 @@
|
||||
"@types/md5": "^2.3.5",
|
||||
"@types/node": "^18.19.9",
|
||||
"@types/pako": "^1.0.2",
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/react": "^19.0.12",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@types/react-infinite-scroll-component": "^5.0.0",
|
||||
"@types/tinycolor2": "^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",
|
||||
"p-queue": "^8.1.0",
|
||||
"prettier": "^3.5.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"rc-virtual-list": "^3.18.5",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hotkeys-hook": "^4.6.1",
|
||||
"react-i18next": "^14.1.2",
|
||||
"react-infinite-scroll-component": "^6.1.0",
|
||||
@ -177,10 +179,6 @@
|
||||
"uuid": "^10.0.0",
|
||||
"vite": "^5.0.12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"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",
|
||||
|
||||
@ -21,7 +21,7 @@ import PaintingsPage from './pages/paintings/PaintingsPage'
|
||||
import SettingsPage from './pages/settings/SettingsPage'
|
||||
import TranslatePage from './pages/translate/TranslatePage'
|
||||
|
||||
function App(): JSX.Element {
|
||||
function App(): React.ReactElement {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<StyleSheetManager>
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
OnDragStartResponder,
|
||||
ResponderProvided
|
||||
} from '@hello-pangea/dnd'
|
||||
import VirtualList from 'rc-virtual-list'
|
||||
import { droppableReorder } from '@renderer/utils'
|
||||
import { FC } from 'react'
|
||||
|
||||
@ -47,26 +48,28 @@ const DragableList: FC<Props<any>> = ({
|
||||
<Droppable droppableId="droppable" {...droppableProps}>
|
||||
{(provided) => (
|
||||
<div {...provided.droppableProps} ref={provided.innerRef} style={style}>
|
||||
{list.map((item, index) => {
|
||||
const id = item.id || item
|
||||
return (
|
||||
<Draggable key={`draggable_${id}_${index}`} draggableId={id} index={index}>
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
style={{
|
||||
...listStyle,
|
||||
...provided.draggableProps.style,
|
||||
marginBottom: 8
|
||||
}}>
|
||||
{children(item, index)}
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
)
|
||||
})}
|
||||
<VirtualList data={list} itemKey="id">
|
||||
{(item, index) => {
|
||||
const id = item.id || item
|
||||
return (
|
||||
<Draggable key={`draggable_${id}_${index}`} draggableId={id} index={index}>
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
style={{
|
||||
...listStyle,
|
||||
...provided.draggableProps.style,
|
||||
marginBottom: 8
|
||||
}}>
|
||||
{children(item, index)}
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
)
|
||||
}}
|
||||
</VirtualList>
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
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'
|
||||
|
||||
type Props = PropsWithChildren & JSX.IntrinsicElements['div']
|
||||
type Props = PropsWithChildren & HTMLAttributes<HTMLDivElement>
|
||||
|
||||
export const Navbar: FC<Props> = ({ children, ...props }) => {
|
||||
const backgroundColor = useNavBackgroundColor()
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import isPropValid from '@emotion/is-prop-valid'
|
||||
import { ReactNode } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { StyleSheetManager as StyledComponentsStyleSheetManager } from 'styled-components'
|
||||
|
||||
interface StyleSheetManagerProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const StyleSheetManager = ({ children }: StyleSheetManagerProps): JSX.Element => {
|
||||
const StyleSheetManager = ({ children }: StyleSheetManagerProps): React.ReactElement => {
|
||||
return (
|
||||
<StyledComponentsStyleSheetManager
|
||||
shouldForwardProp={(prop, element) => {
|
||||
|
||||
@ -1,16 +1,11 @@
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useMermaid } from '@renderer/hooks/useMermaid'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { CodeStyleVarious, ThemeMode } from '@renderer/types'
|
||||
import React, { createContext, PropsWithChildren, useContext, useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
BundledLanguage,
|
||||
bundledLanguages,
|
||||
BundledTheme,
|
||||
bundledThemes,
|
||||
createHighlighter,
|
||||
HighlighterGeneric
|
||||
} from 'shiki'
|
||||
import { type CodeStyleVarious, ThemeMode } from '@renderer/types'
|
||||
import type React from 'react'
|
||||
import { createContext, type PropsWithChildren, use, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import type { BundledLanguage, BundledTheme, HighlighterGeneric } from 'shiki'
|
||||
import { bundledLanguages, bundledThemes, createHighlighter } from 'shiki'
|
||||
|
||||
interface SyntaxHighlighterContextType {
|
||||
codeToHtml: (code: string, language: string) => Promise<string>
|
||||
@ -51,42 +46,47 @@ export const SyntaxHighlighterProvider: React.FC<PropsWithChildren> = ({ childre
|
||||
initHighlighter()
|
||||
}, [highlighterTheme])
|
||||
|
||||
const codeToHtml = async (code: string, language: string) => {
|
||||
if (!highlighter) return ''
|
||||
const codeToHtml = useCallback(
|
||||
async (_code: string, language: string) => {
|
||||
{
|
||||
if (!highlighter) return ''
|
||||
|
||||
const languageMap: Record<string, string> = {
|
||||
vab: 'vb'
|
||||
}
|
||||
const languageMap: Record<string, string> = {
|
||||
vab: 'vb'
|
||||
}
|
||||
|
||||
const mappedLanguage = languageMap[language] || language
|
||||
const mappedLanguage = languageMap[language] || language
|
||||
|
||||
code = code?.trimEnd() ?? ''
|
||||
const escapedCode = code?.replace(/[<>]/g, (char) => ({ '<': '<', '>': '>' })[char]!)
|
||||
const code = _code?.trimEnd() ?? ''
|
||||
const escapedCode = code?.replace(/[<>]/g, (char) => ({ '<': '<', '>': '>' })[char]!)
|
||||
|
||||
try {
|
||||
if (!highlighter.getLoadedLanguages().includes(mappedLanguage as BundledLanguage)) {
|
||||
if (mappedLanguage in bundledLanguages || mappedLanguage === 'text') {
|
||||
await highlighter.loadLanguage(mappedLanguage as BundledLanguage)
|
||||
} else {
|
||||
try {
|
||||
if (!highlighter.getLoadedLanguages().includes(mappedLanguage as BundledLanguage)) {
|
||||
if (mappedLanguage in bundledLanguages || mappedLanguage === 'text') {
|
||||
await highlighter.loadLanguage(mappedLanguage as BundledLanguage)
|
||||
} else {
|
||||
return `<pre style="padding: 10px"><code>${escapedCode}</code></pre>`
|
||||
}
|
||||
}
|
||||
|
||||
return highlighter.codeToHtml(code, {
|
||||
lang: mappedLanguage,
|
||||
theme: highlighterTheme
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(`Error highlighting code for language '${mappedLanguage}':`, error)
|
||||
return `<pre style="padding: 10px"><code>${escapedCode}</code></pre>`
|
||||
}
|
||||
}
|
||||
|
||||
return highlighter.codeToHtml(code, {
|
||||
lang: mappedLanguage,
|
||||
theme: highlighterTheme
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(`Error highlighting code for language '${mappedLanguage}':`, error)
|
||||
return `<pre style="padding: 10px"><code>${escapedCode}</code></pre>`
|
||||
}
|
||||
}
|
||||
},
|
||||
[highlighter, highlighterTheme]
|
||||
)
|
||||
|
||||
return <SyntaxHighlighterContext.Provider value={{ codeToHtml }}>{children}</SyntaxHighlighterContext.Provider>
|
||||
}
|
||||
|
||||
export const useSyntaxHighlighter = () => {
|
||||
const context = useContext(SyntaxHighlighterContext)
|
||||
const context = use(SyntaxHighlighterContext)
|
||||
if (!context) {
|
||||
throw new Error('useSyntaxHighlighter must be used within a SyntaxHighlighterProvider')
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
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 {
|
||||
theme: ThemeMode
|
||||
@ -64,4 +64,4 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children, defaultT
|
||||
)
|
||||
}
|
||||
|
||||
export const useTheme = () => useContext(ThemeContext)
|
||||
export const useTheme = () => use(ThemeContext)
|
||||
|
||||
@ -5,14 +5,15 @@ import {
|
||||
clearStreamMessage,
|
||||
clearTopicMessages,
|
||||
commitStreamMessage,
|
||||
deleteMessageAction,
|
||||
resendMessage,
|
||||
selectDisplayCount,
|
||||
selectTopicLoading,
|
||||
selectTopicMessages,
|
||||
setStreamMessage,
|
||||
setTopicLoading,
|
||||
updateMessage,
|
||||
updateMessages
|
||||
updateMessages,
|
||||
updateMessageThunk
|
||||
} from '@renderer/store/messages'
|
||||
import type { Assistant, Message, Topic } from '@renderer/types'
|
||||
import { abortCompletion } from '@renderer/utils/abortController'
|
||||
@ -27,17 +28,15 @@ import { TopicManager } from './useTopic'
|
||||
*/
|
||||
export function useMessageOperations(topic: Topic) {
|
||||
const dispatch = useAppDispatch()
|
||||
const messages = useAppSelector((state) => selectTopicMessages(state, topic.id))
|
||||
|
||||
/**
|
||||
* 删除单个消息
|
||||
*/
|
||||
const deleteMessage = useCallback(
|
||||
async (message: Message) => {
|
||||
const newMessages = messages.filter((m) => m.id !== message.id)
|
||||
await dispatch(updateMessages(topic, newMessages))
|
||||
async (id: string) => {
|
||||
await dispatch(deleteMessageAction(topic, id))
|
||||
},
|
||||
[dispatch, topic, messages]
|
||||
[dispatch, topic]
|
||||
)
|
||||
|
||||
/**
|
||||
@ -45,10 +44,9 @@ export function useMessageOperations(topic: Topic) {
|
||||
*/
|
||||
const deleteGroupMessages = useCallback(
|
||||
async (askId: string) => {
|
||||
const newMessages = messages.filter((m) => m.askId !== askId)
|
||||
await dispatch(updateMessages(topic, newMessages))
|
||||
await dispatch(deleteMessageAction(topic, askId, 'askId'))
|
||||
},
|
||||
[dispatch, topic, messages]
|
||||
[dispatch, topic]
|
||||
)
|
||||
|
||||
/**
|
||||
@ -58,23 +56,17 @@ export function useMessageOperations(topic: Topic) {
|
||||
async (messageId: string, updates: Partial<Message>) => {
|
||||
// 如果更新包含内容变更,重新计算 token
|
||||
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) {
|
||||
const updatedMessage = { ...message, ...updates }
|
||||
const usage = await estimateMessageUsage(updatedMessage)
|
||||
updates.usage = usage
|
||||
}
|
||||
}
|
||||
|
||||
await dispatch(
|
||||
updateMessage({
|
||||
topicId: topic.id,
|
||||
messageId,
|
||||
updates
|
||||
})
|
||||
)
|
||||
await dispatch(updateMessageThunk(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)
|
||||
}, [])
|
||||
|
||||
const loading = useAppSelector((state) => selectTopicLoading(state, topic.id))
|
||||
const displayCount = useAppSelector(selectDisplayCount)
|
||||
// /**
|
||||
// * 获取当前消息列表
|
||||
@ -211,8 +202,6 @@ export function useMessageOperations(topic: Topic) {
|
||||
)
|
||||
|
||||
return {
|
||||
messages,
|
||||
loading,
|
||||
displayCount,
|
||||
updateMessages: updateMessagesAction,
|
||||
deleteMessage,
|
||||
@ -230,3 +219,13 @@ export function useMessageOperations(topic: Topic) {
|
||||
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
|
||||
}
|
||||
|
||||
@ -1,13 +1,16 @@
|
||||
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 MiniApp from './windows/mini/App'
|
||||
|
||||
if (location.hash === '#/mini') {
|
||||
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 {
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<App />)
|
||||
const root = createRoot(document.getElementById('root') as HTMLElement)
|
||||
root.render(<App />)
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ import TranslateButton from '@renderer/components/TranslateButton'
|
||||
import { isFunctionCallingModel, isGenerateImageModel, isVisionModel, isWebSearchModel } from '@renderer/config/models'
|
||||
import db from '@renderer/databases'
|
||||
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 { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShortcut, useShortcutDisplay } from '@renderer/hooks/useShortcuts'
|
||||
@ -83,10 +83,11 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
const containerRef = useRef(null)
|
||||
const { searching } = useRuntime()
|
||||
const { isBubbleStyle } = useMessageStyle()
|
||||
const { loading, pauseMessages } = useMessageOperations(topic)
|
||||
const { pauseMessages } = useMessageOperations(topic)
|
||||
const loading = useTopicLoading(topic)
|
||||
const dispatch = useAppDispatch()
|
||||
const [spaceClickCount, setSpaceClickCount] = useState(0)
|
||||
const spaceClickTimer = useRef<NodeJS.Timeout>()
|
||||
const spaceClickTimer = useRef<NodeJS.Timeout>(null)
|
||||
const [isTranslating, setIsTranslating] = useState(false)
|
||||
const [selectedKnowledgeBases, setSelectedKnowledgeBases] = useState<KnowledgeBase[]>([])
|
||||
const [mentionModels, setMentionModels] = useState<Model[]>([])
|
||||
@ -96,7 +97,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
const [textareaHeight, setTextareaHeight] = useState<number>()
|
||||
const startDragY = useRef<number>(0)
|
||||
const startHeight = useRef<number>(0)
|
||||
const currentMessageId = useRef<string>()
|
||||
const currentMessageId = useRef<string>('')
|
||||
const isVision = useMemo(() => isVisionModel(model), [model])
|
||||
const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision])
|
||||
const navigate = useNavigate()
|
||||
|
||||
@ -26,7 +26,7 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
|
||||
const match = /language-(\w+)/.exec(className || '') || children?.includes('\n')
|
||||
const { codeShowLineNumbers, fontSize, codeCollapsible, codeWrappable } = useSettings()
|
||||
const language = match?.[1] ?? 'text'
|
||||
const [html, setHtml] = useState<string>('')
|
||||
// const [html, setHtml] = useState<string>('')
|
||||
const { codeToHtml } = useSyntaxHighlighter()
|
||||
const [isExpanded, setIsExpanded] = useState(!codeCollapsible)
|
||||
const [isUnwrapped, setIsUnwrapped] = useState(!codeWrappable)
|
||||
@ -40,17 +40,14 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
|
||||
useEffect(() => {
|
||||
const loadHighlightedCode = async () => {
|
||||
const highlightedHtml = await codeToHtml(children, language)
|
||||
setHtml(highlightedHtml)
|
||||
if (codeContentRef.current) {
|
||||
codeContentRef.current.innerHTML = highlightedHtml
|
||||
setShouldShowExpandButton(codeContentRef.current.scrollHeight > 350)
|
||||
}
|
||||
}
|
||||
loadHighlightedCode()
|
||||
}, [children, language, codeToHtml])
|
||||
|
||||
useEffect(() => {
|
||||
if (codeContentRef.current) {
|
||||
setShouldShowExpandButton(codeContentRef.current.scrollHeight > 350)
|
||||
}
|
||||
}, [html])
|
||||
|
||||
useEffect(() => {
|
||||
if (!codeCollapsible) {
|
||||
setIsExpanded(true)
|
||||
@ -112,7 +109,7 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
|
||||
isShowLineNumbers={codeShowLineNumbers}
|
||||
isUnwrapped={isUnwrapped}
|
||||
isCodeWrappable={codeWrappable}
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
// dangerouslySetInnerHTML={{ __html: html }}
|
||||
style={{
|
||||
border: '0.5px solid var(--color-code-background)',
|
||||
borderTopLeftRadius: 0,
|
||||
|
||||
@ -50,7 +50,7 @@ const CustomNode: FC<{ data: any }> = ({ data }) => {
|
||||
let title = ''
|
||||
let backgroundColor = 'var(--bg-color)'
|
||||
let gradientColor = 'rgba(0, 0, 0, 0.03)'
|
||||
let avatar: JSX.Element | null = null
|
||||
let avatar: React.ReactNode | null = null
|
||||
|
||||
// 根据消息类型设置不同的样式和图标
|
||||
if (nodeType === 'user') {
|
||||
|
||||
@ -163,7 +163,7 @@ const MessageItem: FC<Props> = ({
|
||||
isLastMessage={isLastMessage}
|
||||
isAssistantMessage={isAssistantMessage}
|
||||
isGrouped={isGrouped}
|
||||
messageContainerRef={messageContainerRef}
|
||||
messageContainerRef={messageContainerRef as React.RefObject<HTMLDivElement>}
|
||||
setModel={setModel}
|
||||
/>
|
||||
</MessageFooter>
|
||||
|
||||
@ -6,11 +6,11 @@ import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { getMessageModelId } from '@renderer/services/MessagesService'
|
||||
import { getModelName } from '@renderer/services/ModelService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { updateMessage } from '@renderer/store/messages'
|
||||
import { Message } from '@renderer/types'
|
||||
import { updateMessageThunk } from '@renderer/store/messages'
|
||||
import type { Message } from '@renderer/types'
|
||||
import { isEmoji, removeLeadingEmoji } from '@renderer/utils'
|
||||
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 styled from 'styled-components'
|
||||
interface MessageLineProps {
|
||||
@ -100,15 +100,9 @@ const MessageAnchorLine: FC<MessageLineProps> = ({ messages }) => {
|
||||
(message: Message) => {
|
||||
const groupMessages = messages.filter((m) => m.askId === message.askId)
|
||||
if (groupMessages.length > 1) {
|
||||
groupMessages.forEach((m) => {
|
||||
dispatch(
|
||||
updateMessage({
|
||||
topicId: m.topicId,
|
||||
messageId: m.id,
|
||||
updates: { foldSelected: m.id === message.id }
|
||||
})
|
||||
)
|
||||
})
|
||||
for (const m of groupMessages) {
|
||||
dispatch(updateMessageThunk(m.topicId, m.id, { foldSelected: m.id === message.id }))
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const messageElement = document.getElementById(`message-${message.id}`)
|
||||
|
||||
@ -17,12 +17,12 @@ import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
|
||||
import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
|
||||
import { isReasoningModel } from '@renderer/config/models'
|
||||
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 { getMessageTitle, resetAssistantMessage } from '@renderer/services/MessagesService'
|
||||
import { translateText } from '@renderer/services/TranslateService'
|
||||
import { Message, Model } from '@renderer/types'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import type { Message, Model } from '@renderer/types'
|
||||
import type { Assistant, Topic } from '@renderer/types'
|
||||
import { captureScrollableDivAsBlob, captureScrollableDivAsDataURL, removeTrailingDoubleSpaces } from '@renderer/utils'
|
||||
import {
|
||||
exportMarkdownToJoplin,
|
||||
@ -62,15 +62,9 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
const [showRegenerateTooltip, setShowRegenerateTooltip] = useState(false)
|
||||
const [showDeleteTooltip, setShowDeleteTooltip] = useState(false)
|
||||
const assistantModel = assistant?.model
|
||||
const {
|
||||
loading,
|
||||
editMessage,
|
||||
setStreamMessage,
|
||||
deleteMessage,
|
||||
resendMessage,
|
||||
commitStreamMessage,
|
||||
clearStreamMessage
|
||||
} = useMessageOperations(topic)
|
||||
const { editMessage, setStreamMessage, deleteMessage, resendMessage, commitStreamMessage, clearStreamMessage } =
|
||||
useMessageOperations(topic)
|
||||
const loading = useTopicLoading(topic)
|
||||
|
||||
const isUserMessage = message.role === 'user'
|
||||
|
||||
@ -382,7 +376,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
okButtonProps={{ danger: true }}
|
||||
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
|
||||
onOpenChange={(open) => open && setShowDeleteTooltip(false)}
|
||||
onConfirm={() => deleteMessage(message)}>
|
||||
onConfirm={() => deleteMessage(message.id)}>
|
||||
<ActionButton className="message-action-button" onClick={(e) => e.stopPropagation()}>
|
||||
<Tooltip
|
||||
title={t('common.delete')}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { CheckOutlined, ExpandOutlined, LoadingOutlined } from '@ant-design/icons'
|
||||
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 { isEmpty } from 'lodash'
|
||||
import { FC, useMemo, useState } from 'react'
|
||||
@ -42,9 +42,9 @@ const MessageTools: FC<Props> = ({ message }) => {
|
||||
|
||||
// Format tool responses for collapse items
|
||||
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
|
||||
toolResponses.forEach((toolResponse: MCPToolResponse) => {
|
||||
for (const toolResponse of toolResponses) {
|
||||
const { id, tool, status, response } = toolResponse
|
||||
const isInvoking = status === 'invoking'
|
||||
const isDone = status === 'done'
|
||||
@ -105,7 +105,7 @@ const MessageTools: FC<Props> = ({ message }) => {
|
||||
</ToolResponseContainer>
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
@ -129,7 +129,9 @@ const MessageTools: FC<Props> = ({ message }) => {
|
||||
onCancel={() => setExpandedResponse(null)}
|
||||
footer={null}
|
||||
width="80%"
|
||||
bodyStyle={{ maxHeight: '80vh', overflow: 'auto' }}>
|
||||
styles={{
|
||||
body: { maxHeight: '80vh', overflow: 'auto' }
|
||||
}}>
|
||||
{expandedResponse && (
|
||||
<ExpandedResponseContainer style={{ fontFamily, fontSize }}>
|
||||
<ActionButton
|
||||
|
||||
@ -2,7 +2,7 @@ import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { LOAD_MORE_COUNT } from '@renderer/config/constant'
|
||||
import db from '@renderer/databases'
|
||||
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 { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||
import { autoRenameTopic, getTopic } from '@renderer/hooks/useTopic'
|
||||
@ -38,6 +38,42 @@ interface MessagesProps {
|
||||
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 { t } = useTranslation()
|
||||
const { showTopics, topicPosition, showAssistants, messageNavigation } = useSettings()
|
||||
@ -48,9 +84,9 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
|
||||
const [hasMore, setHasMore] = useState(false)
|
||||
const [isLoadingMore, setIsLoadingMore] = useState(false)
|
||||
const [isProcessingContext, setIsProcessingContext] = useState(false)
|
||||
const { messages, displayCount, loading, updateMessages, clearTopicMessages, deleteMessage } =
|
||||
useMessageOperations(topic)
|
||||
|
||||
const messages = useTopicMessages(topic)
|
||||
const { displayCount, updateMessages, clearTopicMessages, deleteMessage } = useMessageOperations(topic)
|
||||
const loading = useTopicLoading(topic)
|
||||
const messagesRef = useRef<Message[]>(messages)
|
||||
|
||||
useEffect(() => {
|
||||
@ -58,9 +94,7 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
|
||||
}, [messages])
|
||||
|
||||
useEffect(() => {
|
||||
const reversedMessages = [...messages].reverse()
|
||||
const newDisplayMessages = reversedMessages.slice(0, displayCount)
|
||||
|
||||
const newDisplayMessages = computeDisplayMessages(messages, 0, displayCount)
|
||||
setDisplayMessages(newDisplayMessages)
|
||||
setHasMore(messages.length > displayCount)
|
||||
}, [messages, displayCount])
|
||||
@ -73,7 +107,15 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
|
||||
}, [showAssistants, showTopics, topicPosition])
|
||||
|
||||
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(() => {
|
||||
@ -122,7 +164,7 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
|
||||
const lastMessage = last(messages)
|
||||
|
||||
if (lastMessage?.type === 'clear') {
|
||||
await deleteMessage(lastMessage)
|
||||
await deleteMessage(lastMessage.id)
|
||||
scrollToBottom()
|
||||
return
|
||||
}
|
||||
@ -183,10 +225,9 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
|
||||
setIsLoadingMore(true)
|
||||
setTimeout(() => {
|
||||
const currentLength = displayMessages.length
|
||||
const reversedMessages = [...messages].reverse()
|
||||
const moreMessages = reversedMessages.slice(currentLength, currentLength + LOAD_MORE_COUNT)
|
||||
const newMessages = computeDisplayMessages(messages, currentLength, LOAD_MORE_COUNT)
|
||||
|
||||
setDisplayMessages((prev) => [...prev, ...moreMessages])
|
||||
setDisplayMessages((prev) => [...prev, ...newMessages])
|
||||
setHasMore(currentLength + LOAD_MORE_COUNT < messages.length)
|
||||
setIsLoadingMore(false)
|
||||
}, 300)
|
||||
@ -199,7 +240,6 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
|
||||
window.message.success(t('message.copy.success'))
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Container
|
||||
id="messages"
|
||||
@ -214,8 +254,8 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
|
||||
next={loadMoreMessages}
|
||||
hasMore={hasMore}
|
||||
loader={null}
|
||||
inverse={true}
|
||||
scrollableTarget="messages">
|
||||
scrollableTarget="messages"
|
||||
inverse>
|
||||
<ScrollContainer>
|
||||
<LoaderContainer $loading={isLoadingMore}>
|
||||
<BeatLoader size={8} color="var(--color-text-2)" />
|
||||
|
||||
@ -56,7 +56,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
||||
const borderRadius = showTopicTime ? 12 : 'var(--list-item-border-radius)'
|
||||
|
||||
const [deletingTopicId, setDeletingTopicId] = useState<string | null>(null)
|
||||
const deleteTimerRef = useRef<NodeJS.Timeout>()
|
||||
const deleteTimerRef = useRef<NodeJS.Timeout>(null)
|
||||
|
||||
const pendingTopics = useMemo(() => {
|
||||
return new Set<string>()
|
||||
|
||||
@ -23,11 +23,12 @@ import { translateText } from '@renderer/services/TranslateService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { DEFAULT_PAINTING } from '@renderer/store/paintings'
|
||||
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 { Button, Input, InputNumber, Radio, Select, Slider, Switch, Tooltip } from 'antd'
|
||||
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 styled from 'styled-components'
|
||||
|
||||
@ -243,7 +244,7 @@ const PaintingsPage: FC = () => {
|
||||
const { autoTranslateWithSpace } = useSettings()
|
||||
const [spaceClickCount, setSpaceClickCount] = useState(0)
|
||||
const [isTranslating, setIsTranslating] = useState(false)
|
||||
const spaceClickTimer = useRef<NodeJS.Timeout>()
|
||||
const spaceClickTimer = useRef<NodeJS.Timeout>(null)
|
||||
|
||||
const translate = async () => {
|
||||
if (isTranslating) {
|
||||
|
||||
@ -152,7 +152,12 @@ const PopupContainer: React.FC<Props> = ({ server, create, resolve }) => {
|
||||
width={600}
|
||||
transitionName="ant-move-down"
|
||||
centered
|
||||
bodyStyle={{ maxHeight: '70vh', overflowY: 'auto' }}>
|
||||
styles={{
|
||||
body: {
|
||||
maxHeight: '70vh',
|
||||
overflowY: 'auto'
|
||||
}
|
||||
}}>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
name="name"
|
||||
|
||||
@ -280,7 +280,11 @@ const ShortcutSettings: FC = () => {
|
||||
<HStack alignItems="center" style={{ position: 'relative' }}>
|
||||
{isEditing ? (
|
||||
<ShortcutInput
|
||||
ref={(el) => el && (inputRefs.current[record.key] = el)}
|
||||
ref={(el) => {
|
||||
if (el) {
|
||||
inputRefs.current[record.key] = el
|
||||
}
|
||||
}}
|
||||
value={formatShortcut(shortcut)}
|
||||
placeholder={t('settings.shortcuts.press_shortcut')}
|
||||
onKeyDown={(e) => handleKeyDown(e, record)}
|
||||
|
||||
@ -6,9 +6,9 @@ import { fetchChatCompletion } from '@renderer/services/ApiService'
|
||||
import { getAssistantMessage, resetAssistantMessage } from '@renderer/services/MessagesService'
|
||||
import type { AppDispatch, RootState } from '@renderer/store'
|
||||
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 { cloneDeep, isEmpty, throttle } from 'lodash'
|
||||
import { isEmpty, throttle } from 'lodash'
|
||||
|
||||
export interface MessagesState {
|
||||
messagesByTopic: Record<string, Message[]>
|
||||
@ -113,14 +113,10 @@ const messagesSlice = createSlice({
|
||||
) => {
|
||||
const { topicId, messageId, updates } = action.payload
|
||||
const topicMessages = state.messagesByTopic[topicId]
|
||||
|
||||
if (topicMessages) {
|
||||
const message = topicMessages.find((msg) => msg.id === messageId)
|
||||
if (message) {
|
||||
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 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
|
||||
})
|
||||
@ -263,7 +259,7 @@ export const sendMessage =
|
||||
const { model, id } = messageToReset
|
||||
const resetMessage = resetAssistantMessage(messageToReset, model)
|
||||
// 更新状态
|
||||
dispatch(updateMessage({ topicId: topic.id, messageId: id, updates: resetMessage }))
|
||||
dispatch(updateMessageThunk(topic.id, id, resetMessage))
|
||||
// 使用重置后的消息
|
||||
assistantMessages.push(resetMessage)
|
||||
}
|
||||
@ -396,10 +392,9 @@ export const sendMessage =
|
||||
} catch (error: any) {
|
||||
console.error('Error in chat completion:', error)
|
||||
dispatch(
|
||||
updateMessage({
|
||||
topicId: topic.id,
|
||||
messageId: assistantMessage.id,
|
||||
updates: { status: 'error', error: { message: error.message } }
|
||||
updateMessageThunk(topic.id, assistantMessage.id, {
|
||||
status: 'error',
|
||||
error: { message: error.message }
|
||||
})
|
||||
)
|
||||
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')
|
||||
if (!userMessage) {
|
||||
dispatch(
|
||||
updateMessage({
|
||||
topicId: topic.id,
|
||||
messageId: message.id,
|
||||
updates: { status: 'error', error: { message: i18n.t('error.user_message_not_found') } }
|
||||
updateMessageThunk(topic.id, message.id, {
|
||||
status: 'error',
|
||||
error: { message: 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,同时更新缓存
|
||||
export const updateMessages = (topic: Topic, messages: Message[]) => async (dispatch: AppDispatch) => {
|
||||
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
|
||||
export const selectCurrentTopicId = (state: RootState): string | null => {
|
||||
const messagesState = state.messages
|
||||
@ -546,11 +570,10 @@ export const selectTopicMessages = createSelector(
|
||||
)
|
||||
|
||||
// 获取特定话题的loading状态
|
||||
export const selectTopicLoading = (state: RootState, topicId?: string): boolean => {
|
||||
const messagesState = state.messages as MessagesState
|
||||
const currentTopicId = topicId || messagesState.currentTopic?.id || ''
|
||||
return currentTopicId ? (messagesState.loadingByTopic[currentTopicId] ?? false) : false
|
||||
}
|
||||
export const selectTopicLoading = createSelector(
|
||||
[(state: RootState) => state.messages.loadingByTopic, (_, topicId?: string) => topicId],
|
||||
(loadingByTopic, topicId) => (topicId ? (loadingByTopic[topicId] ?? false) : false)
|
||||
)
|
||||
|
||||
export const selectDisplayCount = (state: RootState): number => {
|
||||
const messagesState = state.messages as MessagesState
|
||||
|
||||
@ -306,7 +306,7 @@ export async function captureDiv(divRef: React.RefObject<HTMLDivElement>) {
|
||||
return Promise.resolve(undefined)
|
||||
}
|
||||
|
||||
export const captureScrollableDiv = async (divRef: React.RefObject<HTMLDivElement>) => {
|
||||
export const captureScrollableDiv = async (divRef: React.RefObject<HTMLDivElement | null>) => {
|
||||
if (divRef.current) {
|
||||
try {
|
||||
const div = divRef.current
|
||||
@ -392,7 +392,7 @@ export const captureScrollableDiv = async (divRef: React.RefObject<HTMLDivElemen
|
||||
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) => {
|
||||
if (canvas) {
|
||||
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) => {
|
||||
canvas?.toBlob(func, 'image/png')
|
||||
})
|
||||
|
||||
@ -9,7 +9,7 @@ import { SyntaxHighlighterProvider } from '../../context/SyntaxHighlighterProvid
|
||||
import { ThemeProvider } from '../../context/ThemeProvider'
|
||||
import HomeWindow from './home/HomeWindow'
|
||||
|
||||
function MiniWindow(): JSX.Element {
|
||||
function MiniWindow(): React.ReactElement {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<ThemeProvider>
|
||||
|
||||
101
yarn.lock
101
yarn.lock
@ -165,6 +165,17 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 0.38.0
|
||||
resolution: "@anthropic-ai/sdk@npm:0.38.0"
|
||||
@ -3455,19 +3466,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/prop-types@npm:*":
|
||||
version: 15.7.14
|
||||
resolution: "@types/prop-types@npm:15.7.14"
|
||||
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"
|
||||
"@types/react-dom@npm:^19.0.4":
|
||||
version: 19.0.4
|
||||
resolution: "@types/react-dom@npm:19.0.4"
|
||||
peerDependencies:
|
||||
"@types/react": ^18.0.0
|
||||
checksum: 10c0/b163d35a6b32a79f5782574a7aeb12a31a647e248792bf437e6d596e2676961c394c5e3c6e91d1ce44ae90441dbaf93158efb4f051c0d61e2612f1cb04ce4faa
|
||||
"@types/react": ^19.0.0
|
||||
checksum: 10c0/4e71853919b94df9e746a4bd73f8180e9ae13016333ce9c543dcba9f4f4c8fe6e28b038ca6ee61c24e291af8e03ca3bc5ded17c46dee938fcb32d71186fda7a3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -3480,7 +3484,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/react@npm:*":
|
||||
"@types/react@npm:*, @types/react@npm:^19.0.12":
|
||||
version: 19.0.12
|
||||
resolution: "@types/react@npm:19.0.12"
|
||||
dependencies:
|
||||
@ -3489,16 +3493,6 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 1.0.3
|
||||
resolution: "@types/responselike@npm:1.0.3"
|
||||
@ -3766,6 +3760,7 @@ __metadata:
|
||||
"@agentic/exa": "npm:^7.3.3"
|
||||
"@agentic/searxng": "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"
|
||||
"@cherrystudio/embedjs": "npm:^0.1.28"
|
||||
"@cherrystudio/embedjs-libsql": "npm:^0.1.28"
|
||||
@ -3804,8 +3799,8 @@ __metadata:
|
||||
"@types/md5": "npm:^2.3.5"
|
||||
"@types/node": "npm:^18.19.9"
|
||||
"@types/pako": "npm:^1.0.2"
|
||||
"@types/react": "npm:^18.2.48"
|
||||
"@types/react-dom": "npm:^18.2.18"
|
||||
"@types/react": "npm:^19.0.12"
|
||||
"@types/react-dom": "npm:^19.0.4"
|
||||
"@types/react-infinite-scroll-component": "npm:^5.0.0"
|
||||
"@types/tinycolor2": "npm:^1"
|
||||
"@vitejs/plugin-react": "npm:^4.2.1"
|
||||
@ -3853,8 +3848,9 @@ __metadata:
|
||||
p-queue: "npm:^8.1.0"
|
||||
prettier: "npm:^3.5.3"
|
||||
proxy-agent: "npm:^6.5.0"
|
||||
react: "npm:^18.2.0"
|
||||
react-dom: "npm:^18.2.0"
|
||||
rc-virtual-list: "npm:^3.18.5"
|
||||
react: "npm:^19.0.0"
|
||||
react-dom: "npm:^19.0.0"
|
||||
react-hotkeys-hook: "npm:^4.6.1"
|
||||
react-i18next: "npm:^14.1.2"
|
||||
react-infinite-scroll-component: "npm:^6.1.0"
|
||||
@ -3884,9 +3880,6 @@ __metadata:
|
||||
vite: "npm:^5.0.12"
|
||||
webdav: "npm:^5.8.0"
|
||||
zipread: "npm:^1.3.3"
|
||||
peerDependencies:
|
||||
react: ^17.0.0 || ^18.0.0
|
||||
react-dom: ^17.0.0 || ^18.0.0
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
@ -9190,7 +9183,7 @@ __metadata:
|
||||
languageName: node
|
||||
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
|
||||
resolution: "js-tokens@npm:4.0.0"
|
||||
checksum: 10c0/e248708d377aa058eacf2037b07ded847790e6de892bbad3dac0abba2e759cb9f121b00099a65195616badcb6eca8d14d975cb3e89eb1cfda644756402c8aeed
|
||||
@ -9798,17 +9791,6 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 0.4.2
|
||||
resolution: "lop@npm:0.4.2"
|
||||
@ -13272,7 +13254,7 @@ __metadata:
|
||||
languageName: node
|
||||
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
|
||||
resolution: "rc-virtual-list@npm:3.18.5"
|
||||
dependencies:
|
||||
@ -13301,15 +13283,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-dom@npm:^18.2.0":
|
||||
version: 18.3.1
|
||||
resolution: "react-dom@npm:18.3.1"
|
||||
"react-dom@npm:^19.0.0":
|
||||
version: 19.0.0
|
||||
resolution: "react-dom@npm:19.0.0"
|
||||
dependencies:
|
||||
loose-envify: "npm:^1.1.0"
|
||||
scheduler: "npm:^0.23.2"
|
||||
scheduler: "npm:^0.25.0"
|
||||
peerDependencies:
|
||||
react: ^18.3.1
|
||||
checksum: 10c0/a752496c1941f958f2e8ac56239172296fcddce1365ce45222d04a1947e0cc5547df3e8447f855a81d6d39f008d7c32eab43db3712077f09e3f67c4874973e85
|
||||
react: ^19.0.0
|
||||
checksum: 10c0/a36ce7ab507b237ae2759c984cdaad4af4096d8199fb65b3815c16825e5cfeb7293da790a3fc2184b52bfba7ba3ff31c058c01947aff6fd1a3701632aabaa6a9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -13480,12 +13461,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react@npm:^18.2.0":
|
||||
version: 18.3.1
|
||||
resolution: "react@npm:18.3.1"
|
||||
dependencies:
|
||||
loose-envify: "npm:^1.1.0"
|
||||
checksum: 10c0/283e8c5efcf37802c9d1ce767f302dd569dd97a70d9bb8c7be79a789b9902451e0d16334b05d73299b20f048cbc3c7d288bbbde10b701fa194e2089c237dbea3
|
||||
"react@npm:^19.0.0":
|
||||
version: 19.0.0
|
||||
resolution: "react@npm:19.0.0"
|
||||
checksum: 10c0/9cad8f103e8e3a16d15cb18a0d8115d8bd9f9e1ce3420310aea381eb42aa0a4f812cf047bb5441349257a05fba8a291515691e3cb51267279b2d2c3253f38471
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -14183,12 +14162,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"scheduler@npm:^0.23.2":
|
||||
version: 0.23.2
|
||||
resolution: "scheduler@npm:0.23.2"
|
||||
dependencies:
|
||||
loose-envify: "npm:^1.1.0"
|
||||
checksum: 10c0/26383305e249651d4c58e6705d5f8425f153211aef95f15161c151f7b8de885f24751b377e4a0b3dd42cce09aad3f87a61dab7636859c0d89b7daf1a1e2a5c78
|
||||
"scheduler@npm:^0.25.0":
|
||||
version: 0.25.0
|
||||
resolution: "scheduler@npm:0.25.0"
|
||||
checksum: 10c0/a4bb1da406b613ce72c1299db43759526058fdcc413999c3c3e0db8956df7633acf395cb20eb2303b6a65d658d66b6585d344460abaee8080b4aa931f10eaafe
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user