mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-11 08:19:01 +08:00
feat: refactor cache management and update component dependencies
- Updated package.json to version 2.0.0-alpha, reflecting significant changes. - Refactored cache management by integrating useCache hooks across various components, enhancing state management and performance. - Replaced useRuntime references with useMinapps in multiple components to streamline minapp state handling. - Improved type safety in cache schemas and updated related components to utilize new types. - Removed deprecated runtime actions and streamlined the codebase for better maintainability.
This commit is contained in:
parent
6079961f44
commit
c242abd81a
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "CherryStudio",
|
||||
"version": "1.6.0-beta.7",
|
||||
"version": "2.0.0-alpha",
|
||||
"private": true,
|
||||
"description": "A powerful AI assistant for producer.",
|
||||
"main": "./out/main/index.js",
|
||||
|
||||
144
packages/shared/data/cache/cacheSchemas.ts
vendored
144
packages/shared/data/cache/cacheSchemas.ts
vendored
@ -1,22 +1,144 @@
|
||||
import type * as CacheValueTypes from './cacheValueTypes'
|
||||
|
||||
/**
|
||||
* Use cache schema for renderer hook
|
||||
*/
|
||||
|
||||
export type UseCacheSchema = {
|
||||
// App state
|
||||
'app.dist.update_state': CacheValueTypes.CacheAppUpdateState
|
||||
'app.user.avatar': string
|
||||
|
||||
// Chat context
|
||||
'chat.multi_select_mode': boolean
|
||||
'chat.selected_message_ids': string[]
|
||||
'chat.generating': boolean
|
||||
'chat.websearch.searching': boolean
|
||||
'chat.websearch.active_searches': CacheValueTypes.CacheActiveSearches
|
||||
|
||||
// Minapp management
|
||||
'minapp.opened_keep_alive': CacheValueTypes.CacheMinAppType[]
|
||||
'minapp.current_id': string
|
||||
'minapp.show': boolean
|
||||
'minapp.opened_oneoff': CacheValueTypes.CacheMinAppType | null
|
||||
|
||||
// Topic management
|
||||
'topic.active': CacheValueTypes.CacheTopic | null
|
||||
'topic.renaming': string[]
|
||||
'topic.newly_renamed': string[]
|
||||
|
||||
// Test keys (for dataRefactorTest window)
|
||||
// TODO: remove after testing
|
||||
'test-hook-memory-1': string
|
||||
'test-ttl-cache': string
|
||||
'test-protected-cache': string
|
||||
'test-deep-equal': { nested: { count: number }; tags: string[] }
|
||||
'test-performance': number
|
||||
'test-multi-hook': string
|
||||
'concurrent-test-1': number
|
||||
'concurrent-test-2': number
|
||||
'large-data-test': Record<string, any>
|
||||
'test-number-cache': number
|
||||
'test-object-cache': { name: string; count: number; active: boolean }
|
||||
}
|
||||
|
||||
export const DefaultUseCache: UseCacheSchema = {
|
||||
// App state
|
||||
'app.dist.update_state': {
|
||||
info: null,
|
||||
checking: false,
|
||||
downloading: false,
|
||||
downloaded: false,
|
||||
downloadProgress: 0,
|
||||
available: false
|
||||
},
|
||||
'app.user.avatar': '',
|
||||
|
||||
// Chat context
|
||||
'chat.multi_select_mode': false,
|
||||
'chat.selected_message_ids': [],
|
||||
'chat.generating': false,
|
||||
'chat.websearch.searching': false,
|
||||
'chat.websearch.active_searches': {},
|
||||
|
||||
// Minapp management
|
||||
'minapp.opened_keep_alive': [],
|
||||
'minapp.current_id': '',
|
||||
'minapp.show': false,
|
||||
'minapp.opened_oneoff': null,
|
||||
|
||||
// Topic management
|
||||
'topic.active': null,
|
||||
'topic.renaming': [],
|
||||
'topic.newly_renamed': [],
|
||||
|
||||
// Test keys (for dataRefactorTest window)
|
||||
// TODO: remove after testing
|
||||
'test-hook-memory-1': 'default-memory-value',
|
||||
'test-ttl-cache': 'test-ttl-cache',
|
||||
'test-protected-cache': 'protected-value',
|
||||
'test-deep-equal': { nested: { count: 0 }, tags: ['initial'] },
|
||||
'test-performance': 0,
|
||||
'test-multi-hook': 'hook-1-default',
|
||||
'concurrent-test-1': 0,
|
||||
'concurrent-test-2': 0,
|
||||
'large-data-test': {},
|
||||
'test-number-cache': 42,
|
||||
'test-object-cache': { name: 'test', count: 0, active: true }
|
||||
}
|
||||
|
||||
/**
|
||||
* Use shared cache schema for renderer hook
|
||||
*/
|
||||
export type UseSharedCacheSchema = {
|
||||
'example-key': string
|
||||
|
||||
// Test keys (for dataRefactorTest window)
|
||||
// TODO: remove after testing
|
||||
'test-hook-shared-1': string
|
||||
'test-multi-hook': string
|
||||
'concurrent-shared': number
|
||||
}
|
||||
|
||||
export const DefaultUseSharedCache: UseSharedCacheSchema = {
|
||||
'example-key': 'example default value',
|
||||
|
||||
// Test keys (for dataRefactorTest window)
|
||||
// TODO: remove after testing
|
||||
'concurrent-shared': 0,
|
||||
'test-hook-shared-1': 'default-shared-value',
|
||||
'test-multi-hook': 'hook-3-shared'
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist cache schema defining allowed keys and their value types
|
||||
* This ensures type safety and prevents key conflicts
|
||||
*/
|
||||
export interface PersistCacheSchema {
|
||||
export type RendererPersistCacheSchema = {
|
||||
'example-key': string
|
||||
|
||||
// Test keys (for dataRefactorTest window)
|
||||
// TODO: remove after testing
|
||||
'example-1': string
|
||||
'example-2': number
|
||||
'example-3': boolean
|
||||
'example-4': { a: string; b: number; c: boolean }
|
||||
'example-2': string
|
||||
'example-3': string
|
||||
'example-4': string
|
||||
}
|
||||
|
||||
export const DefaultPersistCache: PersistCacheSchema = {
|
||||
'example-1': 'example-1',
|
||||
'example-2': 1,
|
||||
'example-3': true,
|
||||
'example-4': { a: 'example-4', b: 4, c: false }
|
||||
export const DefaultRendererPersistCache: RendererPersistCacheSchema = {
|
||||
'example-key': 'example default value',
|
||||
|
||||
// Test keys (for dataRefactorTest window)
|
||||
// TODO: remove after testing
|
||||
'example-1': 'example default value',
|
||||
'example-2': 'example default value',
|
||||
'example-3': 'example default value',
|
||||
'example-4': 'example default value'
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-safe persist cache key
|
||||
* Type-safe cache key
|
||||
*/
|
||||
export type PersistCacheKey = keyof PersistCacheSchema
|
||||
export type RendererPersistCacheKey = keyof RendererPersistCacheSchema
|
||||
export type UseCacheKey = keyof UseCacheSchema
|
||||
export type UseSharedCacheKey = keyof UseSharedCacheSchema
|
||||
|
||||
18
packages/shared/data/cache/cacheValueTypes.ts
vendored
Normal file
18
packages/shared/data/cache/cacheValueTypes.ts
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
import type { MinAppType, Topic, WebSearchStatus } from '@types'
|
||||
import type { UpdateInfo } from 'builder-util-runtime'
|
||||
|
||||
export type CacheAppUpdateState = {
|
||||
info: UpdateInfo | null
|
||||
checking: boolean
|
||||
downloading: boolean
|
||||
downloaded: boolean
|
||||
downloadProgress: number
|
||||
available: boolean
|
||||
}
|
||||
|
||||
export type CacheActiveSearches = Record<string, WebSearchStatus>
|
||||
|
||||
// For cache schema, we use any for complex types to avoid circular dependencies
|
||||
// The actual type checking will be done at runtime by the cache system
|
||||
export type CacheMinAppType = MinAppType
|
||||
export type CacheTopic = Topic
|
||||
@ -5,14 +5,11 @@ import { loadCustomMiniApp, ORIGIN_DEFAULT_MIN_APPS, updateDefaultMinApps } from
|
||||
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||
import { useNavbarPosition } from '@renderer/hooks/useNavbar'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { setOpenedKeepAliveMinapps } from '@renderer/store/runtime'
|
||||
import { MinAppType } from '@renderer/types'
|
||||
import type { MenuProps } from 'antd'
|
||||
import { Dropdown } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@ -28,9 +25,18 @@ const logger = loggerService.withContext('App')
|
||||
const MinApp: FC<Props> = ({ app, onClick, size = 60, isLast }) => {
|
||||
const { openMinappKeepAlive } = useMinappPopup()
|
||||
const { t } = useTranslation()
|
||||
const { minapps, pinned, disabled, updateMinapps, updateDisabledMinapps, updatePinnedMinapps } = useMinapps()
|
||||
const { openedKeepAliveMinapps, currentMinappId, minappShow } = useRuntime()
|
||||
const dispatch = useDispatch()
|
||||
const {
|
||||
minapps,
|
||||
pinned,
|
||||
disabled,
|
||||
openedKeepAliveMinapps,
|
||||
currentMinappId,
|
||||
minappShow,
|
||||
setOpenedKeepAliveMinapps,
|
||||
updateMinapps,
|
||||
updateDisabledMinapps,
|
||||
updatePinnedMinapps
|
||||
} = useMinapps()
|
||||
const navigate = useNavigate()
|
||||
const isPinned = pinned.some((p) => p.id === app.id)
|
||||
const isVisible = minapps.some((m) => m.id === app.id)
|
||||
@ -76,7 +82,7 @@ const MinApp: FC<Props> = ({ app, onClick, size = 60, isLast }) => {
|
||||
updatePinnedMinapps(newPinned)
|
||||
// 更新 openedKeepAliveMinapps
|
||||
const newOpenedKeepAliveMinapps = openedKeepAliveMinapps.filter((item) => item.id !== app.id)
|
||||
dispatch(setOpenedKeepAliveMinapps(newOpenedKeepAliveMinapps))
|
||||
setOpenedKeepAliveMinapps(newOpenedKeepAliveMinapps)
|
||||
}
|
||||
},
|
||||
...(app.type === 'Custom'
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { loggerService } from '@logger'
|
||||
import WebviewContainer from '@renderer/components/MinApp/WebviewContainer'
|
||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||
import { useNavbarPosition } from '@renderer/hooks/useNavbar'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { getWebviewLoaded, setWebviewLoaded } from '@renderer/utils/webviewStateManager'
|
||||
import { WebviewTag } from 'electron'
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
@ -21,7 +21,7 @@ import styled from 'styled-components'
|
||||
const logger = loggerService.withContext('MinAppTabsPool')
|
||||
|
||||
const MinAppTabsPool: React.FC = () => {
|
||||
const { openedKeepAliveMinapps, currentMinappId } = useRuntime()
|
||||
const { openedKeepAliveMinapps, currentMinappId } = useMinapps()
|
||||
const { isTopNavbar } = useNavbarPosition()
|
||||
const location = useLocation()
|
||||
|
||||
|
||||
@ -20,7 +20,6 @@ import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||
import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor'
|
||||
import { useNavbarPosition } from '@renderer/hooks/useNavbar'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { MinAppType } from '@renderer/types'
|
||||
import { delay } from '@renderer/utils'
|
||||
@ -141,10 +140,10 @@ const GoogleLoginTip = ({
|
||||
|
||||
/** The main container for MinApp popup */
|
||||
const MinappPopupContainer: React.FC = () => {
|
||||
const { openedKeepAliveMinapps, openedOneOffMinapp, currentMinappId, minappShow } = useRuntime()
|
||||
const [minappsOpenLinkExternal, setMinappsOpenLinkExternal] = usePreference('feature.minapp.open_link_external')
|
||||
const { closeMinapp, hideMinappPopup } = useMinappPopup()
|
||||
const { pinned, updatePinnedMinapps } = useMinapps()
|
||||
const { pinned, updatePinnedMinapps, openedKeepAliveMinapps, openedOneOffMinapp, currentMinappId, minappShow } =
|
||||
useMinapps()
|
||||
const { t } = useTranslation()
|
||||
const backgroundColor = useNavBackgroundColor()
|
||||
const { isTopNavbar } = useNavbarPosition()
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import MinappPopupContainer from '@renderer/components/MinApp/MinappPopupContainer'
|
||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||
import { useNavbarPosition } from '@renderer/hooks/useNavbar'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
|
||||
const TopViewMinappContainer = () => {
|
||||
const { openedKeepAliveMinapps, openedOneOffMinapp } = useRuntime()
|
||||
const { openedKeepAliveMinapps, openedOneOffMinapp } = useMinapps()
|
||||
const { isLeftNavbar } = useNavbarPosition()
|
||||
const isCreate = openedKeepAliveMinapps.length > 0 || openedOneOffMinapp !== null
|
||||
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import { cacheService } from '@data/CacheService'
|
||||
import { usePreference } from '@data/hooks/usePreference'
|
||||
import DefaultAvatar from '@renderer/assets/images/avatar.png'
|
||||
import EmojiAvatar from '@renderer/components/Avatar/EmojiAvatar'
|
||||
import useAvatar from '@renderer/hooks/useAvatar'
|
||||
import ImageStorage from '@renderer/services/ImageStorage'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setAvatar } from '@renderer/store/runtime'
|
||||
import { compressImage, isEmoji } from '@renderer/utils'
|
||||
import { Avatar, Dropdown, Input, Modal, Popover, Upload } from 'antd'
|
||||
import React, { useState } from 'react'
|
||||
@ -26,7 +25,6 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
const [emojiPickerOpen, setEmojiPickerOpen] = useState(false)
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
const dispatch = useAppDispatch()
|
||||
const avatar = useAvatar()
|
||||
|
||||
const onOk = () => {
|
||||
@ -46,7 +44,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
// set emoji string
|
||||
await ImageStorage.set('avatar', emoji)
|
||||
// update avatar display
|
||||
dispatch(setAvatar(emoji))
|
||||
cacheService.set('avatar', emoji)
|
||||
setEmojiPickerOpen(false)
|
||||
} catch (error: any) {
|
||||
window.toast.error(error.message)
|
||||
@ -55,7 +53,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
const handleReset = async () => {
|
||||
try {
|
||||
await ImageStorage.set('avatar', DefaultAvatar)
|
||||
dispatch(setAvatar(DefaultAvatar))
|
||||
cacheService.set('avatar', DefaultAvatar)
|
||||
setDropdownOpen(false)
|
||||
} catch (error: any) {
|
||||
window.toast.error(error.message)
|
||||
@ -80,7 +78,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
const compressedFile = await compressImage(_file)
|
||||
await ImageStorage.set('avatar', compressedFile)
|
||||
}
|
||||
dispatch(setAvatar(await ImageStorage.get('avatar')))
|
||||
cacheService.set('avatar', await ImageStorage.get('avatar'))
|
||||
setDropdownOpen(false)
|
||||
} catch (error: any) {
|
||||
window.toast.error(error.message)
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { isLinux, isMac, isWin } from '@renderer/config/constant'
|
||||
import { useFullscreen } from '@renderer/hooks/useFullscreen'
|
||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||
import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor'
|
||||
import { useNavbarPosition } from '@renderer/hooks/useNavbar'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import type { FC, PropsWithChildren } from 'react'
|
||||
import type { HTMLAttributes } from 'react'
|
||||
import styled from 'styled-components'
|
||||
@ -15,7 +15,7 @@ export const Navbar: FC<Props> = ({ children, ...props }) => {
|
||||
const backgroundColor = useNavBackgroundColor()
|
||||
const isFullscreen = useFullscreen()
|
||||
const { isTopNavbar } = useNavbarPosition()
|
||||
const { minappShow } = useRuntime()
|
||||
const { minappShow } = useMinapps()
|
||||
|
||||
if (isTopNavbar) {
|
||||
return null
|
||||
|
||||
@ -3,7 +3,6 @@ import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||
import { useNavbarPosition } from '@renderer/hooks/useNavbar'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { MinAppType } from '@renderer/types'
|
||||
import type { MenuProps } from 'antd'
|
||||
import { Dropdown, Tooltip } from 'antd'
|
||||
@ -16,7 +15,7 @@ import MinAppIcon from '../Icons/MinAppIcon'
|
||||
|
||||
/** Tabs of opened minapps in sidebar */
|
||||
export const SidebarOpenedMinappTabs: FC = () => {
|
||||
const { minappShow, openedKeepAliveMinapps, currentMinappId } = useRuntime()
|
||||
const { minappShow, openedKeepAliveMinapps, currentMinappId } = useMinapps()
|
||||
const { openMinappKeepAlive, hideMinappPopup, closeMinapp, closeAllMinapps } = useMinappPopup()
|
||||
const [showOpenedMinappsInSidebar] = usePreference('feature.minapp.show_opened_in_sidebar')
|
||||
const { theme } = useTheme()
|
||||
@ -105,9 +104,8 @@ export const SidebarOpenedMinappTabs: FC = () => {
|
||||
}
|
||||
|
||||
export const SidebarPinnedApps: FC = () => {
|
||||
const { pinned, updatePinnedMinapps } = useMinapps()
|
||||
const { pinned, updatePinnedMinapps, minappShow, openedKeepAliveMinapps, currentMinappId } = useMinapps()
|
||||
const { t } = useTranslation()
|
||||
const { minappShow, openedKeepAliveMinapps, currentMinappId } = useRuntime()
|
||||
const { theme } = useTheme()
|
||||
const { openMinappKeepAlive } = useMinappPopup()
|
||||
const { isTopNavbar } = useNavbarPosition()
|
||||
|
||||
@ -7,8 +7,8 @@ import useAvatar from '@renderer/hooks/useAvatar'
|
||||
import { useFullscreen } from '@renderer/hooks/useFullscreen'
|
||||
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||
import { modelGenerating } from '@renderer/hooks/useModel'
|
||||
import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor'
|
||||
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { getSidebarIconLabel, getThemeModeLabel } from '@renderer/i18n/label'
|
||||
import { isEmoji } from '@renderer/utils'
|
||||
@ -39,8 +39,7 @@ import { SidebarOpenedMinappTabs, SidebarPinnedApps } from './PinnedMinapps'
|
||||
|
||||
const Sidebar: FC = () => {
|
||||
const { hideMinappPopup } = useMinappPopup()
|
||||
const { minappShow } = useRuntime()
|
||||
const { pinned } = useMinapps()
|
||||
const { pinned, minappShow } = useMinapps()
|
||||
const [visibleSidebarIcons] = usePreference('ui.sidebar.icons.visible')
|
||||
|
||||
const { pathname } = useLocation()
|
||||
@ -122,10 +121,11 @@ const Sidebar: FC = () => {
|
||||
|
||||
const MainMenus: FC = () => {
|
||||
const { hideMinappPopup } = useMinappPopup()
|
||||
const { minappShow } = useMinapps()
|
||||
|
||||
const { pathname } = useLocation()
|
||||
const [visibleSidebarIcons] = usePreference('ui.sidebar.icons.visible')
|
||||
const { defaultPaintingProvider } = useSettings()
|
||||
const { minappShow } = useRuntime()
|
||||
const navigate = useNavigate()
|
||||
const { theme } = useTheme()
|
||||
|
||||
|
||||
@ -1,6 +1,13 @@
|
||||
import { loggerService } from '@logger'
|
||||
import type { PersistCacheKey, PersistCacheSchema } from '@shared/data/cache/cacheSchemas'
|
||||
import { DefaultPersistCache } from '@shared/data/cache/cacheSchemas'
|
||||
import type {
|
||||
RendererPersistCacheKey,
|
||||
RendererPersistCacheSchema,
|
||||
UseCacheKey,
|
||||
UseCacheSchema,
|
||||
UseSharedCacheKey,
|
||||
UseSharedCacheSchema
|
||||
} from '@shared/data/cache/cacheSchemas'
|
||||
import { DefaultRendererPersistCache } from '@shared/data/cache/cacheSchemas'
|
||||
import type { CacheEntry, CacheSubscriber, CacheSyncMessage } from '@shared/data/cache/cacheTypes'
|
||||
|
||||
const STORAGE_PERSIST_KEY = 'cs_cache_persist'
|
||||
@ -28,7 +35,7 @@ export class CacheService {
|
||||
// Three-layer cache system
|
||||
private memoryCache = new Map<string, CacheEntry>() // Cross-component cache
|
||||
private sharedCache = new Map<string, CacheEntry>() // Cross-window cache (local copy)
|
||||
private persistCache = new Map<PersistCacheKey, any>() // Persistent cache
|
||||
private persistCache = new Map<RendererPersistCacheKey, any>() // Persistent cache
|
||||
|
||||
// Hook reference tracking
|
||||
private activeHooks = new Set<string>()
|
||||
@ -66,10 +73,13 @@ export class CacheService {
|
||||
/**
|
||||
* Get value from memory cache
|
||||
*/
|
||||
get<T>(key: string): T | undefined {
|
||||
get<K extends UseCacheKey>(key: K): UseCacheSchema[K]
|
||||
get<T>(key: Exclude<string, UseCacheKey>): T | undefined
|
||||
get(key: string): any {
|
||||
const entry = this.memoryCache.get(key)
|
||||
if (!entry) return undefined
|
||||
|
||||
if (entry === undefined) {
|
||||
return undefined
|
||||
}
|
||||
// Check TTL (lazy cleanup)
|
||||
if (entry.expireAt && Date.now() > entry.expireAt) {
|
||||
this.memoryCache.delete(key)
|
||||
@ -77,13 +87,15 @@ export class CacheService {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return entry.value as T
|
||||
return entry.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Set value in memory cache
|
||||
*/
|
||||
set<T>(key: string, value: T, ttl?: number): void {
|
||||
set<K extends UseCacheKey>(key: K, value: UseCacheSchema[K]): void
|
||||
set<T>(key: Exclude<string, UseCacheKey>, value: T, ttl?: number): void
|
||||
set(key: string, value: any, ttl?: number): void {
|
||||
const existingEntry = this.memoryCache.get(key)
|
||||
|
||||
// Value comparison optimization
|
||||
@ -99,7 +111,7 @@ export class CacheService {
|
||||
return // Skip notification
|
||||
}
|
||||
|
||||
const entry: CacheEntry<T> = {
|
||||
const entry: CacheEntry = {
|
||||
value,
|
||||
expireAt: ttl ? Date.now() + ttl : undefined
|
||||
}
|
||||
@ -112,9 +124,14 @@ export class CacheService {
|
||||
/**
|
||||
* Check if key exists in memory cache
|
||||
*/
|
||||
|
||||
has<K extends UseCacheKey>(key: K): boolean
|
||||
has(key: Exclude<string, UseCacheKey>): boolean
|
||||
has(key: string): boolean {
|
||||
const entry = this.memoryCache.get(key)
|
||||
if (!entry) return false
|
||||
if (entry === undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check TTL
|
||||
if (entry.expireAt && Date.now() > entry.expireAt) {
|
||||
@ -129,6 +146,8 @@ export class CacheService {
|
||||
/**
|
||||
* Delete from memory cache
|
||||
*/
|
||||
delete<K extends UseCacheKey>(key: K): boolean
|
||||
delete(key: Exclude<string, UseCacheKey>): boolean
|
||||
delete(key: string): boolean {
|
||||
// Check if key is being used by hooks
|
||||
if (this.activeHooks.has(key)) {
|
||||
@ -151,6 +170,8 @@ export class CacheService {
|
||||
/**
|
||||
* Check if a key has TTL set (for warning purposes)
|
||||
*/
|
||||
hasTTL<K extends UseCacheKey>(key: K): boolean
|
||||
hasTTL(key: Exclude<string, UseCacheKey>): boolean
|
||||
hasTTL(key: string): boolean {
|
||||
const entry = this.memoryCache.get(key)
|
||||
return entry?.expireAt !== undefined
|
||||
@ -159,6 +180,8 @@ export class CacheService {
|
||||
/**
|
||||
* Check if a shared cache key has TTL set (for warning purposes)
|
||||
*/
|
||||
hasSharedTTL<K extends UseSharedCacheKey>(key: K): boolean
|
||||
hasSharedTTL(key: Exclude<string, UseSharedCacheKey>): boolean
|
||||
hasSharedTTL(key: string): boolean {
|
||||
const entry = this.sharedCache.get(key)
|
||||
return entry?.expireAt !== undefined
|
||||
@ -169,7 +192,9 @@ export class CacheService {
|
||||
/**
|
||||
* Get value from shared cache
|
||||
*/
|
||||
getShared<T>(key: string): T | undefined {
|
||||
getShared<K extends UseSharedCacheKey>(key: K): UseSharedCacheSchema[K]
|
||||
getShared<T>(key: Exclude<string, UseSharedCacheKey>): T | undefined
|
||||
getShared(key: string): any {
|
||||
const entry = this.sharedCache.get(key)
|
||||
if (!entry) return undefined
|
||||
|
||||
@ -180,13 +205,15 @@ export class CacheService {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return entry.value as T
|
||||
return entry.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Set value in shared cache
|
||||
*/
|
||||
setShared<T>(key: string, value: T, ttl?: number): void {
|
||||
setShared<K extends UseSharedCacheKey>(key: K, value: UseSharedCacheSchema[K]): void
|
||||
setShared<T>(key: Exclude<string, UseSharedCacheKey>, value: T, ttl?: number): void
|
||||
setShared(key: string, value: any, ttl?: number): void {
|
||||
const existingEntry = this.sharedCache.get(key)
|
||||
|
||||
// Value comparison optimization
|
||||
@ -209,7 +236,7 @@ export class CacheService {
|
||||
return // Skip local update and notification
|
||||
}
|
||||
|
||||
const entry: CacheEntry<T> = {
|
||||
const entry: CacheEntry = {
|
||||
value,
|
||||
expireAt: ttl ? Date.now() + ttl : undefined
|
||||
}
|
||||
@ -231,6 +258,8 @@ export class CacheService {
|
||||
/**
|
||||
* Check if key exists in shared cache
|
||||
*/
|
||||
hasShared<K extends UseSharedCacheKey>(key: K): boolean
|
||||
hasShared(key: Exclude<string, UseSharedCacheKey>): boolean
|
||||
hasShared(key: string): boolean {
|
||||
const entry = this.sharedCache.get(key)
|
||||
if (!entry) return false
|
||||
@ -248,6 +277,8 @@ export class CacheService {
|
||||
/**
|
||||
* Delete from shared cache
|
||||
*/
|
||||
deleteShared<K extends UseSharedCacheKey>(key: K): boolean
|
||||
deleteShared(key: Exclude<string, UseSharedCacheKey>): boolean
|
||||
deleteShared(key: string): boolean {
|
||||
// Check if key is being used by hooks
|
||||
if (this.activeHooks.has(key)) {
|
||||
@ -279,14 +310,14 @@ export class CacheService {
|
||||
/**
|
||||
* Get value from persist cache
|
||||
*/
|
||||
getPersist<K extends PersistCacheKey>(key: K): PersistCacheSchema[K] {
|
||||
getPersist<K extends RendererPersistCacheKey>(key: K): RendererPersistCacheSchema[K] {
|
||||
const value = this.persistCache.get(key)
|
||||
if (value !== undefined) {
|
||||
return value
|
||||
}
|
||||
|
||||
// Fallback to default value if somehow missing
|
||||
const defaultValue = DefaultPersistCache[key]
|
||||
const defaultValue = DefaultRendererPersistCache[key]
|
||||
this.persistCache.set(key, defaultValue)
|
||||
this.schedulePersistSave()
|
||||
logger.warn(`Missing persist cache key "${key}", using default value`)
|
||||
@ -296,7 +327,7 @@ export class CacheService {
|
||||
/**
|
||||
* Set value in persist cache
|
||||
*/
|
||||
setPersist<K extends PersistCacheKey>(key: K, value: PersistCacheSchema[K]): void {
|
||||
setPersist<K extends RendererPersistCacheKey>(key: K, value: RendererPersistCacheSchema[K]): void {
|
||||
const existingValue = this.persistCache.get(key)
|
||||
|
||||
// Use deep comparison for persist cache (usually objects)
|
||||
@ -323,7 +354,7 @@ export class CacheService {
|
||||
/**
|
||||
* Check if key exists in persist cache
|
||||
*/
|
||||
hasPersist(key: PersistCacheKey): boolean {
|
||||
hasPersist(key: RendererPersistCacheKey): boolean {
|
||||
return this.persistCache.has(key)
|
||||
}
|
||||
|
||||
@ -424,8 +455,8 @@ export class CacheService {
|
||||
*/
|
||||
private loadPersistCache(): void {
|
||||
// First, initialize with default values
|
||||
for (const [key, defaultValue] of Object.entries(DefaultPersistCache)) {
|
||||
this.persistCache.set(key as PersistCacheKey, defaultValue)
|
||||
for (const [key, defaultValue] of Object.entries(DefaultRendererPersistCache)) {
|
||||
this.persistCache.set(key as RendererPersistCacheKey, defaultValue)
|
||||
}
|
||||
|
||||
try {
|
||||
@ -440,7 +471,7 @@ export class CacheService {
|
||||
const data = JSON.parse(stored)
|
||||
|
||||
// Only load keys that exist in schema, overriding defaults
|
||||
const schemaKeys = Object.keys(DefaultPersistCache) as PersistCacheKey[]
|
||||
const schemaKeys = Object.keys(DefaultRendererPersistCache) as RendererPersistCacheKey[]
|
||||
for (const key of schemaKeys) {
|
||||
if (key in data) {
|
||||
this.persistCache.set(key, data[key])
|
||||
@ -536,7 +567,7 @@ export class CacheService {
|
||||
this.notifySubscribers(message.key)
|
||||
} else if (message.type === 'persist') {
|
||||
// Update persist cache (other windows only update memory, not localStorage)
|
||||
this.persistCache.set(message.key as PersistCacheKey, message.value)
|
||||
this.persistCache.set(message.key as RendererPersistCacheKey, message.value)
|
||||
this.notifySubscribers(message.key)
|
||||
}
|
||||
})
|
||||
|
||||
@ -1,8 +1,15 @@
|
||||
import { cacheService } from '@data/CacheService'
|
||||
import { loggerService } from '@logger'
|
||||
import type { PersistCacheKey, PersistCacheSchema } from '@shared/data/cache/cacheSchemas'
|
||||
import type {
|
||||
RendererPersistCacheKey,
|
||||
RendererPersistCacheSchema,
|
||||
UseCacheKey,
|
||||
UseCacheSchema,
|
||||
UseSharedCacheKey,
|
||||
UseSharedCacheSchema
|
||||
} from '@shared/data/cache/cacheSchemas'
|
||||
import { DefaultUseCache, DefaultUseSharedCache } from '@shared/data/cache/cacheSchemas'
|
||||
import { useCallback, useEffect, useSyncExternalStore } from 'react'
|
||||
|
||||
const logger = loggerService.withContext('useCache')
|
||||
|
||||
/**
|
||||
@ -15,23 +22,32 @@ const logger = loggerService.withContext('useCache')
|
||||
* - TTL support with warning when used
|
||||
*
|
||||
* @param key - Cache key
|
||||
* @param defaultValue - Default value (set automatically if not exists)
|
||||
* @param initValue - Default value (set automatically if not exists)
|
||||
* @returns [value, setValue]
|
||||
*/
|
||||
export function useCache<T>(key: string, defaultValue?: T): [T | undefined, (value: T) => void] {
|
||||
export function useCache<K extends UseCacheKey>(
|
||||
key: K,
|
||||
initValue?: UseCacheSchema[K]
|
||||
): [UseCacheSchema[K], (value: UseCacheSchema[K]) => void] {
|
||||
// Subscribe to cache changes
|
||||
const value = useSyncExternalStore(
|
||||
useCallback((callback) => cacheService.subscribe(key, callback), [key]),
|
||||
useCallback(() => cacheService.get<T>(key), [key]),
|
||||
useCallback(() => cacheService.get<T>(key), [key]) // SSR snapshot
|
||||
useCallback(() => cacheService.get<UseCacheSchema[K]>(key), [key]),
|
||||
useCallback(() => cacheService.get<UseCacheSchema[K]>(key), [key]) // SSR snapshot
|
||||
)
|
||||
|
||||
// Set default value if not exists
|
||||
useEffect(() => {
|
||||
if (defaultValue !== undefined && !cacheService.has(key)) {
|
||||
cacheService.set(key, defaultValue)
|
||||
if (cacheService.has(key)) {
|
||||
return
|
||||
}
|
||||
}, [key, defaultValue])
|
||||
|
||||
if (initValue === undefined) {
|
||||
cacheService.set(key, DefaultUseCache[key])
|
||||
} else {
|
||||
cacheService.set(key, initValue)
|
||||
}
|
||||
}, [key, initValue])
|
||||
|
||||
// Register hook lifecycle
|
||||
useEffect(() => {
|
||||
@ -49,13 +65,13 @@ export function useCache<T>(key: string, defaultValue?: T): [T | undefined, (val
|
||||
}, [key])
|
||||
|
||||
const setValue = useCallback(
|
||||
(newValue: T) => {
|
||||
(newValue: UseCacheSchema[K]) => {
|
||||
cacheService.set(key, newValue)
|
||||
},
|
||||
[key]
|
||||
)
|
||||
|
||||
return [value ?? defaultValue, setValue]
|
||||
return [value ?? initValue ?? DefaultUseCache[key], setValue]
|
||||
}
|
||||
|
||||
/**
|
||||
@ -68,23 +84,32 @@ export function useCache<T>(key: string, defaultValue?: T): [T | undefined, (val
|
||||
* - Hook lifecycle management
|
||||
*
|
||||
* @param key - Cache key
|
||||
* @param defaultValue - Default value (set automatically if not exists)
|
||||
* @param initValue - Default value (set automatically if not exists)
|
||||
* @returns [value, setValue]
|
||||
*/
|
||||
export function useSharedCache<T>(key: string, defaultValue?: T): [T | undefined, (value: T) => void] {
|
||||
export function useSharedCache<K extends UseSharedCacheKey>(
|
||||
key: K,
|
||||
initValue?: UseSharedCacheSchema[K]
|
||||
): [UseSharedCacheSchema[K], (value: UseSharedCacheSchema[K]) => void] {
|
||||
// Subscribe to cache changes
|
||||
const value = useSyncExternalStore(
|
||||
useCallback((callback) => cacheService.subscribe(key, callback), [key]),
|
||||
useCallback(() => cacheService.getShared<T>(key), [key]),
|
||||
useCallback(() => cacheService.getShared<T>(key), [key]) // SSR snapshot
|
||||
useCallback(() => cacheService.getShared<UseSharedCacheSchema[K]>(key), [key]),
|
||||
useCallback(() => cacheService.getShared<UseSharedCacheSchema[K]>(key), [key]) // SSR snapshot
|
||||
)
|
||||
|
||||
// Set default value if not exists
|
||||
useEffect(() => {
|
||||
if (defaultValue !== undefined && !cacheService.hasShared(key)) {
|
||||
cacheService.setShared(key, defaultValue)
|
||||
if (cacheService.hasShared(key)) {
|
||||
return
|
||||
}
|
||||
}, [key, defaultValue])
|
||||
|
||||
if (initValue === undefined) {
|
||||
cacheService.setShared(key, DefaultUseSharedCache[key])
|
||||
} else {
|
||||
cacheService.setShared(key, initValue)
|
||||
}
|
||||
}, [key, initValue])
|
||||
|
||||
// Register hook lifecycle
|
||||
useEffect(() => {
|
||||
@ -102,13 +127,13 @@ export function useSharedCache<T>(key: string, defaultValue?: T): [T | undefined
|
||||
}, [key])
|
||||
|
||||
const setValue = useCallback(
|
||||
(newValue: T) => {
|
||||
(newValue: UseSharedCacheSchema[K]) => {
|
||||
cacheService.setShared(key, newValue)
|
||||
},
|
||||
[key]
|
||||
)
|
||||
|
||||
return [value ?? defaultValue, setValue]
|
||||
return [value ?? initValue ?? DefaultUseSharedCache[key], setValue]
|
||||
}
|
||||
|
||||
/**
|
||||
@ -123,9 +148,9 @@ export function useSharedCache<T>(key: string, defaultValue?: T): [T | undefined
|
||||
* @param key - Predefined persist cache key
|
||||
* @returns [value, setValue]
|
||||
*/
|
||||
export function usePersistCache<K extends PersistCacheKey>(
|
||||
export function usePersistCache<K extends RendererPersistCacheKey>(
|
||||
key: K
|
||||
): [PersistCacheSchema[K], (value: PersistCacheSchema[K]) => void] {
|
||||
): [RendererPersistCacheSchema[K], (value: RendererPersistCacheSchema[K]) => void] {
|
||||
// Subscribe to cache changes
|
||||
const value = useSyncExternalStore(
|
||||
useCallback((callback) => cacheService.subscribe(key, callback), [key]),
|
||||
@ -140,7 +165,7 @@ export function usePersistCache<K extends PersistCacheKey>(
|
||||
}, [key])
|
||||
|
||||
const setValue = useCallback(
|
||||
(newValue: PersistCacheSchema[K]) => {
|
||||
(newValue: RendererPersistCacheSchema[K]) => {
|
||||
cacheService.setPersist(key, newValue)
|
||||
},
|
||||
[key]
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import { cacheService } from '@data/CacheService'
|
||||
import { usePreference } from '@data/hooks/usePreference'
|
||||
import { loggerService } from '@logger'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { isLocalAi } from '@renderer/config/env'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import db from '@renderer/databases'
|
||||
import { useAppUpdateHandler, useAppUpdateState } from '@renderer/hooks/useAppUpdate'
|
||||
import i18n from '@renderer/i18n'
|
||||
import KnowledgeQueue from '@renderer/queue/KnowledgeQueue'
|
||||
import MemoryService from '@renderer/services/MemoryService'
|
||||
@ -11,7 +13,6 @@ import { useAppDispatch } from '@renderer/store'
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
import { handleSaveData } from '@renderer/store'
|
||||
import { selectMemoryConfig } from '@renderer/store/memory'
|
||||
import { setAvatar, setFilesPath, setResourcesPath, setUpdateState } from '@renderer/store/runtime'
|
||||
import { delay, runAsyncFunction } from '@renderer/utils'
|
||||
import { checkDataLimit } from '@renderer/utils'
|
||||
import { defaultLanguage } from '@shared/config/constant'
|
||||
@ -21,9 +22,8 @@ import { useEffect } from 'react'
|
||||
|
||||
import { useDefaultModel } from './useAssistant'
|
||||
import useFullScreenNotice from './useFullScreenNotice'
|
||||
import { useMinapps } from './useMinapps'
|
||||
import { useNavbarPosition } from './useNavbar'
|
||||
import { useRuntime } from './useRuntime'
|
||||
import useUpdateHandler from './useUpdateHandler'
|
||||
const logger = loggerService.withContext('useAppInit')
|
||||
|
||||
export function useAppInit() {
|
||||
@ -38,9 +38,10 @@ export function useAppInit() {
|
||||
const [enableDataCollection] = usePreference('app.privacy.data_collection.enabled')
|
||||
|
||||
const { isTopNavbar } = useNavbarPosition()
|
||||
const { minappShow } = useRuntime()
|
||||
const { minappShow } = useMinapps()
|
||||
const { updateAppUpdateState } = useAppUpdateState()
|
||||
const { setDefaultModel, setQuickModel, setTranslateModel } = useDefaultModel()
|
||||
const avatar = useLiveQuery(() => db.settings.get('image://avatar'))
|
||||
const savedAvatar = useLiveQuery(() => db.settings.get('image://avatar'))
|
||||
const { theme } = useTheme()
|
||||
const memoryConfig = useAppSelector(selectMemoryConfig)
|
||||
|
||||
@ -67,12 +68,12 @@ export function useAppInit() {
|
||||
})
|
||||
}, [])
|
||||
|
||||
useUpdateHandler()
|
||||
useAppUpdateHandler()
|
||||
useFullScreenNotice()
|
||||
|
||||
useEffect(() => {
|
||||
avatar?.value && dispatch(setAvatar(avatar.value))
|
||||
}, [avatar, dispatch])
|
||||
savedAvatar?.value && cacheService.set('avatar', savedAvatar.value)
|
||||
}, [savedAvatar, dispatch])
|
||||
|
||||
useEffect(() => {
|
||||
runAsyncFunction(async () => {
|
||||
@ -80,10 +81,10 @@ export function useAppInit() {
|
||||
if (isPackaged && autoCheckUpdate) {
|
||||
await delay(2)
|
||||
const { updateInfo } = await window.api.checkForUpdate()
|
||||
dispatch(setUpdateState({ info: updateInfo }))
|
||||
updateAppUpdateState({ info: updateInfo })
|
||||
}
|
||||
})
|
||||
}, [dispatch, autoCheckUpdate])
|
||||
}, [autoCheckUpdate, updateAppUpdateState])
|
||||
|
||||
useEffect(() => {
|
||||
if (proxyMode === 'system') {
|
||||
@ -125,8 +126,8 @@ export function useAppInit() {
|
||||
useEffect(() => {
|
||||
// set files path
|
||||
window.api.getAppInfo().then((info) => {
|
||||
dispatch(setFilesPath(info.filesPath))
|
||||
dispatch(setResourcesPath(info.resourcesPath))
|
||||
cacheService.set('filesPath', info.filesPath)
|
||||
cacheService.set('resourcesPath', info.resourcesPath)
|
||||
})
|
||||
}, [dispatch])
|
||||
|
||||
|
||||
@ -1,15 +1,28 @@
|
||||
import { useCache } from '@data/hooks/useCache'
|
||||
import { NotificationService } from '@renderer/services/NotificationService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setUpdateState } from '@renderer/store/runtime'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import type { CacheAppUpdateState } from '@shared/data/cache/cacheValueTypes'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import type { ProgressInfo, UpdateInfo } from 'builder-util-runtime'
|
||||
import { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
export const useAppUpdateState = () => {
|
||||
const [appUpdateState, setAppUpdateState] = useCache('app.dist.update_state')
|
||||
|
||||
export default function useUpdateHandler() {
|
||||
const dispatch = useAppDispatch()
|
||||
const updateAppUpdateState = (state: Partial<CacheAppUpdateState>) => {
|
||||
setAppUpdateState({ ...appUpdateState, ...state })
|
||||
}
|
||||
|
||||
return {
|
||||
appUpdateState,
|
||||
updateAppUpdateState
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: 这个函数是从useUpdateHandler中复制过来的,是v2数据重构时调整的,但这个函数本身需要重构和优化(并不需要用在use中)。by fullex
|
||||
export function useAppUpdateHandler() {
|
||||
const { t } = useTranslation()
|
||||
const { updateAppUpdateState } = useAppUpdateState()
|
||||
const notificationService = NotificationService.getInstance()
|
||||
|
||||
useEffect(() => {
|
||||
@ -19,7 +32,7 @@ export default function useUpdateHandler() {
|
||||
|
||||
const removers = [
|
||||
ipcRenderer.on(IpcChannel.UpdateNotAvailable, () => {
|
||||
dispatch(setUpdateState({ checking: false }))
|
||||
updateAppUpdateState({ checking: false })
|
||||
if (window.location.hash.includes('settings/about')) {
|
||||
window.toast.success(t('settings.about.updateNotAvailable'))
|
||||
}
|
||||
@ -34,48 +47,38 @@ export default function useUpdateHandler() {
|
||||
source: 'update',
|
||||
channel: 'system'
|
||||
})
|
||||
dispatch(
|
||||
setUpdateState({
|
||||
checking: false,
|
||||
downloading: true,
|
||||
info: releaseInfo,
|
||||
available: true
|
||||
})
|
||||
)
|
||||
updateAppUpdateState({
|
||||
checking: false,
|
||||
downloading: true,
|
||||
info: releaseInfo,
|
||||
available: true
|
||||
})
|
||||
}),
|
||||
ipcRenderer.on(IpcChannel.DownloadUpdate, () => {
|
||||
dispatch(
|
||||
setUpdateState({
|
||||
checking: false,
|
||||
downloading: true
|
||||
})
|
||||
)
|
||||
updateAppUpdateState({
|
||||
checking: false,
|
||||
downloading: true
|
||||
})
|
||||
}),
|
||||
ipcRenderer.on(IpcChannel.DownloadProgress, (_, progress: ProgressInfo) => {
|
||||
dispatch(
|
||||
setUpdateState({
|
||||
downloading: progress.percent < 100,
|
||||
downloadProgress: progress.percent
|
||||
})
|
||||
)
|
||||
updateAppUpdateState({
|
||||
downloading: progress.percent < 100,
|
||||
downloadProgress: progress.percent
|
||||
})
|
||||
}),
|
||||
ipcRenderer.on(IpcChannel.UpdateDownloaded, (_, releaseInfo: UpdateInfo) => {
|
||||
dispatch(
|
||||
setUpdateState({
|
||||
downloading: false,
|
||||
info: releaseInfo,
|
||||
downloaded: true
|
||||
})
|
||||
)
|
||||
updateAppUpdateState({
|
||||
downloading: false,
|
||||
info: releaseInfo,
|
||||
downloaded: true
|
||||
})
|
||||
}),
|
||||
ipcRenderer.on(IpcChannel.UpdateError, (_, error) => {
|
||||
dispatch(
|
||||
setUpdateState({
|
||||
checking: false,
|
||||
downloading: false,
|
||||
downloadProgress: 0
|
||||
})
|
||||
)
|
||||
updateAppUpdateState({
|
||||
checking: false,
|
||||
downloading: false,
|
||||
downloadProgress: 0
|
||||
})
|
||||
if (window.location.hash.includes('settings/about')) {
|
||||
window.modal.info({
|
||||
title: t('settings.about.updateError'),
|
||||
@ -86,5 +89,5 @@ export default function useUpdateHandler() {
|
||||
})
|
||||
]
|
||||
return () => removers.forEach((remover) => remover())
|
||||
}, [dispatch, notificationService, t])
|
||||
}, [notificationService, t, updateAppUpdateState])
|
||||
}
|
||||
@ -1,5 +1,7 @@
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
import { useCache } from '@data/hooks/useCache'
|
||||
import { UserAvatar } from '@renderer/config/env'
|
||||
|
||||
export default function useAvatar() {
|
||||
return useAppSelector((state) => state.runtime.avatar)
|
||||
const [avatar] = useCache('app.user.avatar', UserAvatar)
|
||||
return avatar
|
||||
}
|
||||
|
||||
@ -1,46 +1,49 @@
|
||||
import { useCache } from '@data/hooks/useCache'
|
||||
import { loggerService } from '@logger'
|
||||
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { RootState } from '@renderer/store'
|
||||
import { messageBlocksSelectors } from '@renderer/store/messageBlock'
|
||||
import { selectMessagesForTopic } from '@renderer/store/newMessage'
|
||||
import { setActiveTopic, setSelectedMessageIds, toggleMultiSelectMode } from '@renderer/store/runtime'
|
||||
// import { setActiveTopic, setSelectedMessageIds, toggleMultiSelectMode } from '@renderer/store/runtime'
|
||||
import { Topic } from '@renderer/types'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDispatch, useSelector, useStore } from 'react-redux'
|
||||
|
||||
import { useStore } from 'react-redux'
|
||||
const logger = loggerService.withContext('useChatContext')
|
||||
|
||||
export const useChatContext = (activeTopic: Topic) => {
|
||||
const { t } = useTranslation()
|
||||
const dispatch = useDispatch()
|
||||
const store = useStore<RootState>()
|
||||
const { deleteMessage } = useMessageOperations(activeTopic)
|
||||
|
||||
const [isMultiSelectMode, setIsMultiSelectMode] = useCache('chat.multi_select_mode')
|
||||
const [selectedMessageIds, setSelectedMessageIds] = useCache('chat.selected_message_ids')
|
||||
const [, setActiveTopic] = useCache('topic.active')
|
||||
|
||||
const [messageRefs, setMessageRefs] = useState<Map<string, HTMLElement>>(new Map())
|
||||
|
||||
const isMultiSelectMode = useSelector((state: RootState) => state.runtime.chat.isMultiSelectMode)
|
||||
const selectedMessageIds = useSelector((state: RootState) => state.runtime.chat.selectedMessageIds)
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = EventEmitter.on(EVENT_NAMES.CHANGE_TOPIC, () => {
|
||||
dispatch(toggleMultiSelectMode(false))
|
||||
})
|
||||
return () => unsubscribe()
|
||||
}, [dispatch])
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setActiveTopic(activeTopic))
|
||||
}, [dispatch, activeTopic])
|
||||
|
||||
const handleToggleMultiSelectMode = useCallback(
|
||||
(value: boolean) => {
|
||||
dispatch(toggleMultiSelectMode(value))
|
||||
setIsMultiSelectMode(value)
|
||||
if (!value) {
|
||||
setSelectedMessageIds([])
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
[setIsMultiSelectMode, setSelectedMessageIds]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = EventEmitter.on(EVENT_NAMES.CHANGE_TOPIC, () => {
|
||||
handleToggleMultiSelectMode(false)
|
||||
})
|
||||
return () => unsubscribe()
|
||||
}, [handleToggleMultiSelectMode])
|
||||
|
||||
useEffect(() => {
|
||||
setActiveTopic(activeTopic)
|
||||
}, [activeTopic, setActiveTopic])
|
||||
|
||||
const registerMessageElement = useCallback((id: string, element: HTMLElement | null) => {
|
||||
setMessageRefs((prev) => {
|
||||
const newRefs = new Map(prev)
|
||||
@ -81,17 +84,15 @@ export const useChatContext = (activeTopic: Topic) => {
|
||||
|
||||
const handleSelectMessage = useCallback(
|
||||
(messageId: string, selected: boolean) => {
|
||||
dispatch(
|
||||
setSelectedMessageIds(
|
||||
selected
|
||||
? selectedMessageIds.includes(messageId)
|
||||
? selectedMessageIds
|
||||
: [...selectedMessageIds, messageId]
|
||||
: selectedMessageIds.filter((id) => id !== messageId)
|
||||
)
|
||||
setSelectedMessageIds(
|
||||
selected
|
||||
? selectedMessageIds.includes(messageId)
|
||||
? selectedMessageIds
|
||||
: [...selectedMessageIds, messageId]
|
||||
: selectedMessageIds.filter((id) => id !== messageId)
|
||||
)
|
||||
},
|
||||
[dispatch, selectedMessageIds]
|
||||
[selectedMessageIds, setSelectedMessageIds]
|
||||
)
|
||||
|
||||
const handleMultiSelectAction = useCallback(
|
||||
|
||||
@ -1,14 +1,7 @@
|
||||
import { usePreference } from '@data/hooks/usePreference'
|
||||
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||
import TabsService from '@renderer/services/TabsService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import {
|
||||
setCurrentMinappId,
|
||||
setMinappShow,
|
||||
setOpenedKeepAliveMinapps,
|
||||
setOpenedOneOffMinapp
|
||||
} from '@renderer/store/runtime'
|
||||
import { MinAppType } from '@renderer/types'
|
||||
import { clearWebviewState } from '@renderer/utils/webviewStateManager'
|
||||
import { LRUCache } from 'lru-cache'
|
||||
@ -31,8 +24,15 @@ let minAppsCache: LRUCache<string, MinAppType>
|
||||
* const { openedKeepAliveMinapps, openedOneOffMinapp, minappShow } = useRuntime()
|
||||
*/
|
||||
export const useMinappPopup = () => {
|
||||
const dispatch = useAppDispatch()
|
||||
const { openedKeepAliveMinapps, openedOneOffMinapp, minappShow } = useRuntime()
|
||||
const {
|
||||
openedKeepAliveMinapps,
|
||||
openedOneOffMinapp,
|
||||
minappShow,
|
||||
setOpenedKeepAliveMinapps,
|
||||
setOpenedOneOffMinapp,
|
||||
setCurrentMinappId,
|
||||
setMinappShow
|
||||
} = useMinapps()
|
||||
const [maxKeepAliveMinapps] = usePreference('feature.minapp.max_keep_alive')
|
||||
|
||||
const createLRUCache = useCallback(() => {
|
||||
@ -50,15 +50,15 @@ export const useMinappPopup = () => {
|
||||
}
|
||||
|
||||
// Update Redux state
|
||||
dispatch(setOpenedKeepAliveMinapps(Array.from(minAppsCache.values())))
|
||||
setOpenedKeepAliveMinapps(Array.from(minAppsCache.values()))
|
||||
},
|
||||
onInsert: () => {
|
||||
dispatch(setOpenedKeepAliveMinapps(Array.from(minAppsCache.values())))
|
||||
setOpenedKeepAliveMinapps(Array.from(minAppsCache.values()))
|
||||
},
|
||||
updateAgeOnGet: true,
|
||||
updateAgeOnHas: true
|
||||
})
|
||||
}, [dispatch, maxKeepAliveMinapps])
|
||||
}, [maxKeepAliveMinapps, setOpenedKeepAliveMinapps])
|
||||
|
||||
// 缓存不存在
|
||||
if (!minAppsCache) {
|
||||
@ -89,23 +89,23 @@ export const useMinappPopup = () => {
|
||||
|
||||
// 如果小程序已经打开,只切换显示
|
||||
if (openedKeepAliveMinapps.some((item) => item.id === app.id)) {
|
||||
dispatch(setCurrentMinappId(app.id))
|
||||
dispatch(setMinappShow(true))
|
||||
setCurrentMinappId(app.id)
|
||||
setMinappShow(true)
|
||||
return
|
||||
}
|
||||
dispatch(setOpenedOneOffMinapp(null))
|
||||
dispatch(setCurrentMinappId(app.id))
|
||||
dispatch(setMinappShow(true))
|
||||
setOpenedOneOffMinapp(null)
|
||||
setCurrentMinappId(app.id)
|
||||
setMinappShow(true)
|
||||
return
|
||||
}
|
||||
|
||||
//if the minapp is not keep alive, open it as one-off minapp
|
||||
dispatch(setOpenedOneOffMinapp(app))
|
||||
dispatch(setCurrentMinappId(app.id))
|
||||
dispatch(setMinappShow(true))
|
||||
setOpenedOneOffMinapp(app)
|
||||
setCurrentMinappId(app.id)
|
||||
setMinappShow(true)
|
||||
return
|
||||
},
|
||||
[dispatch, openedKeepAliveMinapps]
|
||||
[openedKeepAliveMinapps, setOpenedOneOffMinapp, setCurrentMinappId, setMinappShow]
|
||||
)
|
||||
|
||||
/** a wrapper of openMinapp(app, true) */
|
||||
@ -133,14 +133,14 @@ export const useMinappPopup = () => {
|
||||
if (openedKeepAliveMinapps.some((item) => item.id === appid)) {
|
||||
minAppsCache.delete(appid)
|
||||
} else if (openedOneOffMinapp?.id === appid) {
|
||||
dispatch(setOpenedOneOffMinapp(null))
|
||||
setOpenedOneOffMinapp(null)
|
||||
}
|
||||
|
||||
dispatch(setCurrentMinappId(''))
|
||||
dispatch(setMinappShow(false))
|
||||
setCurrentMinappId('')
|
||||
setMinappShow(false)
|
||||
return
|
||||
},
|
||||
[dispatch, openedKeepAliveMinapps, openedOneOffMinapp]
|
||||
[openedKeepAliveMinapps, openedOneOffMinapp, setOpenedOneOffMinapp, setCurrentMinappId, setMinappShow]
|
||||
)
|
||||
|
||||
/** Close all minapps (popup hides and all minapps unloaded) */
|
||||
@ -148,22 +148,22 @@ export const useMinappPopup = () => {
|
||||
// minAppsCache.clear 会多次调用 dispose 方法
|
||||
// 重新创建一个 LRU Cache 替换
|
||||
minAppsCache = createLRUCache()
|
||||
dispatch(setOpenedKeepAliveMinapps([]))
|
||||
dispatch(setOpenedOneOffMinapp(null))
|
||||
dispatch(setCurrentMinappId(''))
|
||||
dispatch(setMinappShow(false))
|
||||
}, [dispatch, createLRUCache])
|
||||
setOpenedKeepAliveMinapps([])
|
||||
setOpenedOneOffMinapp(null)
|
||||
setCurrentMinappId('')
|
||||
setMinappShow(false)
|
||||
}, [createLRUCache, setOpenedKeepAliveMinapps, setOpenedOneOffMinapp, setCurrentMinappId, setMinappShow])
|
||||
|
||||
/** Hide the minapp popup (only one-off minapp unloaded) */
|
||||
const hideMinappPopup = useCallback(() => {
|
||||
if (!minappShow) return
|
||||
|
||||
if (openedOneOffMinapp) {
|
||||
dispatch(setOpenedOneOffMinapp(null))
|
||||
dispatch(setCurrentMinappId(''))
|
||||
setOpenedOneOffMinapp(null)
|
||||
setCurrentMinappId('')
|
||||
}
|
||||
dispatch(setMinappShow(false))
|
||||
}, [dispatch, minappShow, openedOneOffMinapp])
|
||||
setMinappShow(false)
|
||||
}, [minappShow, openedOneOffMinapp, setOpenedOneOffMinapp, setCurrentMinappId, setMinappShow])
|
||||
|
||||
return {
|
||||
openMinapp,
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { useCache } from '@data/hooks/useCache'
|
||||
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||
import { RootState, useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { setDisabledMinApps, setMinApps, setPinnedMinApps } from '@renderer/store/minapps'
|
||||
@ -7,10 +8,23 @@ export const useMinapps = () => {
|
||||
const { enabled, disabled, pinned } = useAppSelector((state: RootState) => state.minapps)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const [openedKeepAliveMinapps, setOpenedKeepAliveMinapps] = useCache('minapp.opened_keep_alive')
|
||||
const [currentMinappId, setCurrentMinappId] = useCache('minapp.current_id')
|
||||
const [minappShow, setMinappShow] = useCache('minapp.show')
|
||||
const [openedOneOffMinapp, setOpenedOneOffMinapp] = useCache('minapp.opened_oneoff')
|
||||
|
||||
return {
|
||||
minapps: enabled.map((app) => DEFAULT_MIN_APPS.find((item) => item.id === app.id) || app),
|
||||
disabled: disabled.map((app) => DEFAULT_MIN_APPS.find((item) => item.id === app.id) || app),
|
||||
pinned: pinned.map((app) => DEFAULT_MIN_APPS.find((item) => item.id === app.id) || app),
|
||||
openedKeepAliveMinapps,
|
||||
currentMinappId,
|
||||
minappShow,
|
||||
openedOneOffMinapp,
|
||||
setOpenedKeepAliveMinapps,
|
||||
setCurrentMinappId,
|
||||
setMinappShow,
|
||||
setOpenedOneOffMinapp,
|
||||
updateMinapps: (minapps: MinAppType[]) => {
|
||||
dispatch(setMinApps(minapps))
|
||||
},
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { cacheService } from '@data/CacheService'
|
||||
import i18n from '@renderer/i18n'
|
||||
import store from '@renderer/store'
|
||||
|
||||
import { useProviders } from './useProvider'
|
||||
@ -25,3 +27,14 @@ export function getModel(id?: string, providerId?: string) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function modelGenerating() {
|
||||
const generating = cacheService.get<boolean>('generating') ?? false
|
||||
|
||||
if (generating) {
|
||||
window.toast.warning(i18n.t('message.switch.disabled'))
|
||||
return Promise.reject()
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
@ -2,20 +2,8 @@
|
||||
* Data Refactor, notes by fullex
|
||||
* //TODO @deprecated this file will be removed
|
||||
*/
|
||||
import i18n from '@renderer/i18n'
|
||||
import store, { useAppSelector } from '@renderer/store'
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
|
||||
export function useRuntime() {
|
||||
return useAppSelector((state) => state.runtime)
|
||||
}
|
||||
|
||||
export function modelGenerating() {
|
||||
const generating = store.getState().runtime.generating
|
||||
|
||||
if (generating) {
|
||||
window.toast.warning(i18n.t('message.switch.disabled'))
|
||||
return Promise.reject()
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { cacheService } from '@data/CacheService'
|
||||
import { preferenceService } from '@data/PreferenceService'
|
||||
import db from '@renderer/databases'
|
||||
import i18n from '@renderer/i18n'
|
||||
@ -6,7 +7,6 @@ import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { deleteMessageFiles } from '@renderer/services/MessagesService'
|
||||
import store from '@renderer/store'
|
||||
import { updateTopic } from '@renderer/store/assistants'
|
||||
import { setNewlyRenamedTopics, setRenamingTopics } from '@renderer/store/runtime'
|
||||
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import { findMainTextBlocks } from '@renderer/utils/messageUtils/find'
|
||||
@ -71,9 +71,9 @@ export async function getTopicById(topicId: string) {
|
||||
* 开始重命名指定话题
|
||||
*/
|
||||
export const startTopicRenaming = (topicId: string) => {
|
||||
const currentIds = store.getState().runtime.chat.renamingTopics
|
||||
const currentIds = cacheService.get<string[]>('renamingTopics') ?? []
|
||||
if (!currentIds.includes(topicId)) {
|
||||
store.dispatch(setRenamingTopics([...currentIds, topicId]))
|
||||
cacheService.set('renamingTopics', [...currentIds, topicId])
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,20 +81,26 @@ export const startTopicRenaming = (topicId: string) => {
|
||||
* 完成重命名指定话题
|
||||
*/
|
||||
export const finishTopicRenaming = (topicId: string) => {
|
||||
const state = store.getState()
|
||||
|
||||
// 1. 立即从 renamingTopics 移除
|
||||
const currentRenaming = state.runtime.chat.renamingTopics
|
||||
store.dispatch(setRenamingTopics(currentRenaming.filter((id) => id !== topicId)))
|
||||
const renamingTopics = cacheService.get<string[]>('renamingTopics')
|
||||
if (renamingTopics && renamingTopics.includes(topicId)) {
|
||||
cacheService.set(
|
||||
'renamingTopics',
|
||||
renamingTopics.filter((id) => id !== topicId)
|
||||
)
|
||||
}
|
||||
|
||||
// 2. 立即添加到 newlyRenamedTopics
|
||||
const currentNewlyRenamed = state.runtime.chat.newlyRenamedTopics
|
||||
store.dispatch(setNewlyRenamedTopics([...currentNewlyRenamed, topicId]))
|
||||
const currentNewlyRenamed = cacheService.get<string[]>('newlyRenamedTopics') ?? []
|
||||
cacheService.set('newlyRenamedTopics', [...currentNewlyRenamed, topicId])
|
||||
|
||||
// 3. 延迟从 newlyRenamedTopics 移除
|
||||
setTimeout(() => {
|
||||
const current = store.getState().runtime.chat.newlyRenamedTopics
|
||||
store.dispatch(setNewlyRenamedTopics(current.filter((id) => id !== topicId)))
|
||||
const current = cacheService.get<string[]>('newlyRenamedTopics') ?? []
|
||||
cacheService.set(
|
||||
'newlyRenamedTopics',
|
||||
current.filter((id) => id !== topicId)
|
||||
)
|
||||
}, 700)
|
||||
}
|
||||
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
import { cacheService } from '@data/CacheService'
|
||||
import { loggerService } from '@logger'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import store from '@renderer/store'
|
||||
import { Agent } from '@renderer/types'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const logger = loggerService.withContext('useSystemAgents')
|
||||
|
||||
let _agents: Agent[] = []
|
||||
@ -24,7 +23,7 @@ export const getAgentsFromSystemAgents = (systemAgents: any) => {
|
||||
export function useSystemAgents() {
|
||||
const { defaultAgent } = useSettings()
|
||||
const [agents, setAgents] = useState<Agent[]>([])
|
||||
const { resourcesPath } = useRuntime()
|
||||
const resourcesPath = cacheService.get('resourcesPath') ?? ''
|
||||
const { agentssubscribeUrl } = store.getState().settings
|
||||
const { i18n } = useTranslation()
|
||||
const currentLanguage = i18n.language
|
||||
|
||||
@ -3,12 +3,13 @@ import { usePreference } from '@data/hooks/usePreference'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import SearchPopup from '@renderer/components/Popups/SearchPopup'
|
||||
import { MessageEditingProvider } from '@renderer/context/MessageEditingContext'
|
||||
import { modelGenerating } from '@renderer/hooks/useModel'
|
||||
import useScrollPosition from '@renderer/hooks/useScrollPosition'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { getTopicById } from '@renderer/hooks/useTopic'
|
||||
import { getAssistantById } from '@renderer/services/AssistantService'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { isGenerating, locateToMessage } from '@renderer/services/MessagesService'
|
||||
import { locateToMessage } from '@renderer/services/MessagesService'
|
||||
import NavigationService from '@renderer/services/NavigationService'
|
||||
import { Topic } from '@renderer/types'
|
||||
import { classNames, runAsyncFunction } from '@renderer/utils'
|
||||
@ -47,7 +48,7 @@ const TopicMessages: FC<Props> = ({ topic: _topic, ...props }) => {
|
||||
}
|
||||
|
||||
const onContinueChat = async (topic: Topic) => {
|
||||
await isGenerating()
|
||||
await modelGenerating()
|
||||
SearchPopup.hide()
|
||||
const assistant = getAssistantById(topic.assistantId)
|
||||
navigate('/', { state: { assistant, topic } })
|
||||
|
||||
@ -3,7 +3,7 @@ import { NavbarHeader } from '@renderer/components/app/Navbar'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import SearchPopup from '@renderer/components/Popups/SearchPopup'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { modelGenerating } from '@renderer/hooks/useRuntime'
|
||||
import { modelGenerating } from '@renderer/hooks/useModel'
|
||||
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { HolderOutlined } from '@ant-design/icons'
|
||||
import { useCache } from '@data/hooks/useCache'
|
||||
import { usePreference } from '@data/hooks/usePreference'
|
||||
import { loggerService } from '@logger'
|
||||
import { QuickPanelView, useQuickPanel } from '@renderer/components/QuickPanel'
|
||||
@ -18,7 +19,7 @@ import db from '@renderer/databases'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useKnowledgeBases } from '@renderer/hooks/useKnowledge'
|
||||
import { useMessageOperations, useTopicLoading } from '@renderer/hooks/useMessageOperations'
|
||||
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { modelGenerating } from '@renderer/hooks/useModel'
|
||||
import { useShortcut, useShortcutDisplay } from '@renderer/hooks/useShortcuts'
|
||||
import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
@ -33,8 +34,7 @@ import { spanManagerService } from '@renderer/services/SpanManagerService'
|
||||
import { estimateTextTokens as estimateTxtTokens, estimateUserPromptUsage } from '@renderer/services/TokenService'
|
||||
import { translateText } from '@renderer/services/TranslateService'
|
||||
import WebSearchService from '@renderer/services/WebSearchService'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { setSearching } from '@renderer/store/runtime'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { sendMessage as _sendMessage } from '@renderer/store/thunk/messageThunk'
|
||||
import { Assistant, FileType, FileTypes, KnowledgeBase, KnowledgeItem, Model, Topic } from '@renderer/types'
|
||||
import type { MessageInputBaseParams } from '@renderer/types/newMessage'
|
||||
@ -100,7 +100,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
const { t } = useTranslation()
|
||||
const { getLanguageByLangcode } = useTranslate()
|
||||
const containerRef = useRef(null)
|
||||
const { searching } = useRuntime()
|
||||
const [searching, setSearching] = useCache('chat.websearch.searching')
|
||||
const { pauseMessages } = useMessageOperations(topic)
|
||||
const loading = useTopicLoading(topic)
|
||||
const dispatch = useAppDispatch()
|
||||
@ -115,7 +115,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
const startDragY = useRef<number>(0)
|
||||
const startHeight = useRef<number>(0)
|
||||
const { bases: knowledgeBases } = useKnowledgeBases()
|
||||
const isMultiSelectMode = useAppSelector((state) => state.runtime.chat.isMultiSelectMode)
|
||||
const [isMultiSelectMode] = useCache('chat.multi_select_mode')
|
||||
const isVisionAssistant = useMemo(() => isVisionModel(model), [model])
|
||||
const isGenerateImageAssistant = useMemo(() => isGenerateImageModel(model), [model])
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
@ -911,7 +911,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
disabled={searching}
|
||||
onPaste={(e) => onPaste(e.nativeEvent)}
|
||||
onClick={() => {
|
||||
searching && dispatch(setSearching(false))
|
||||
searching && setSearching(false)
|
||||
quickPanel.close()
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { cacheService } from '@data/CacheService'
|
||||
import { GroundingMetadata } from '@google/genai'
|
||||
import Spinner from '@renderer/components/Spinner'
|
||||
import type { RootState } from '@renderer/store'
|
||||
@ -14,7 +15,7 @@ import CitationsList from '../CitationsList'
|
||||
function CitationBlock({ block }: { block: CitationMessageBlock }) {
|
||||
const { t } = useTranslation()
|
||||
const formattedCitations = useSelector((state: RootState) => selectFormattedCitationsByBlockId(state, block.id))
|
||||
const { websearch } = useSelector((state: RootState) => state.runtime)
|
||||
// const { websearch } = useSelector((state: RootState) => state.runtime)
|
||||
const message = useSelector((state: RootState) => state.messages.entities[block.messageId])
|
||||
const userMessageId = message?.askId || block.messageId // 如果没有 askId 则回退到 messageId
|
||||
|
||||
@ -29,7 +30,7 @@ function CitationBlock({ block }: { block: CitationMessageBlock }) {
|
||||
}, [formattedCitations, block.knowledge, block.memories, hasGeminiBlock])
|
||||
|
||||
const getWebSearchStatusText = (requestId: string) => {
|
||||
const status = websearch.activeSearches[requestId] ?? { phase: 'default' }
|
||||
const status = cacheService.get('activeSearches')?.[requestId] ?? { phase: 'default' }
|
||||
|
||||
switch (status.phase) {
|
||||
case 'fetch_complete':
|
||||
|
||||
@ -4,7 +4,7 @@ import { HStack } from '@renderer/components/Layout'
|
||||
import SearchPopup from '@renderer/components/Popups/SearchPopup'
|
||||
import { isLinux, isWin } from '@renderer/config/constant'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { modelGenerating } from '@renderer/hooks/useRuntime'
|
||||
import { modelGenerating } from '@renderer/hooks/useModel'
|
||||
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { useCache } from '@data/hooks/useCache'
|
||||
import { usePreference } from '@data/hooks/usePreference'
|
||||
import { useMultiplePreferences } from '@data/hooks/usePreference'
|
||||
import { DraggableVirtualList } from '@renderer/components/DraggableList'
|
||||
@ -8,15 +9,13 @@ import SaveToKnowledgePopup from '@renderer/components/Popups/SaveToKnowledgePop
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import { useInPlaceEdit } from '@renderer/hooks/useInPlaceEdit'
|
||||
import { modelGenerating } from '@renderer/hooks/useModel'
|
||||
import { useNotesSettings } from '@renderer/hooks/useNotesSettings'
|
||||
import { modelGenerating } from '@renderer/hooks/useRuntime'
|
||||
import { finishTopicRenaming, startTopicRenaming, TopicManager } from '@renderer/hooks/useTopic'
|
||||
import { fetchMessagesSummary } from '@renderer/services/ApiService'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import store from '@renderer/store'
|
||||
import { RootState } from '@renderer/store'
|
||||
import { newMessagesActions } from '@renderer/store/newMessage'
|
||||
import { setGenerating } from '@renderer/store/runtime'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import { classNames, removeSpecialCharactersForFileName } from '@renderer/utils'
|
||||
import { copyTopicAsMarkdown, copyTopicAsPlainText } from '@renderer/utils/copy'
|
||||
@ -52,7 +51,6 @@ import { FC, useCallback, useDeferredValue, useEffect, useMemo, useRef, useState
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import styled from 'styled-components'
|
||||
|
||||
// const logger = loggerService.withContext('TopicsTab')
|
||||
|
||||
interface Props {
|
||||
@ -67,15 +65,18 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic,
|
||||
const [showTopicTime] = usePreference('topic.tab.show_time')
|
||||
const [pinTopicsToTop] = usePreference('topic.tab.pin_to_top')
|
||||
|
||||
const [, setGenerating] = useCache('chat.generating')
|
||||
|
||||
const { t } = useTranslation()
|
||||
const { notesPath } = useNotesSettings()
|
||||
const { assistants } = useAssistants()
|
||||
const { assistant, removeTopic, moveTopic, updateTopic, updateTopics } = useAssistant(_assistant.id)
|
||||
|
||||
const renamingTopics = useSelector((state: RootState) => state.runtime.chat.renamingTopics)
|
||||
const [renamingTopics] = useCache('topic.renaming')
|
||||
const [newlyRenamedTopics] = useCache('topic.newly_renamed')
|
||||
|
||||
const topicLoadingQuery = useSelector((state: RootState) => state.messages.loadingByTopic)
|
||||
const topicFulfilledQuery = useSelector((state: RootState) => state.messages.fulfilledByTopic)
|
||||
const newlyRenamedTopics = useSelector((state: RootState) => state.runtime.chat.newlyRenamedTopics)
|
||||
|
||||
const borderRadius = showTopicTime ? 12 : 'var(--list-item-border-radius)'
|
||||
|
||||
@ -132,11 +133,14 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic,
|
||||
deleteTimerRef.current = setTimeout(() => setDeletingTopicId(null), 2000)
|
||||
}, [])
|
||||
|
||||
const onClearMessages = useCallback((topic: Topic) => {
|
||||
// window.keyv.set(EVENT_NAMES.CHAT_COMPLETION_PAUSED, true)
|
||||
store.dispatch(setGenerating(false))
|
||||
EventEmitter.emit(EVENT_NAMES.CLEAR_MESSAGES, topic)
|
||||
}, [])
|
||||
const onClearMessages = useCallback(
|
||||
(topic: Topic) => {
|
||||
// window.keyv.set(EVENT_NAMES.CHAT_COMPLETION_PAUSED, true)
|
||||
setGenerating(false)
|
||||
EventEmitter.emit(EVENT_NAMES.CLEAR_MESSAGES, topic)
|
||||
},
|
||||
[setGenerating]
|
||||
)
|
||||
|
||||
const handleConfirmDelete = useCallback(
|
||||
async (topic: Topic, e: React.MouseEvent) => {
|
||||
|
||||
@ -1,21 +1,21 @@
|
||||
import { SyncOutlined } from '@ant-design/icons'
|
||||
import { usePreference } from '@data/hooks/usePreference'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useAppUpdateState } from '@renderer/hooks/useAppUpdate'
|
||||
import { Button } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const UpdateAppButton: FC = () => {
|
||||
const { update } = useRuntime()
|
||||
const { appUpdateState } = useAppUpdateState()
|
||||
const [autoCheckUpdate] = usePreference('app.dist.auto_update.enabled')
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!update) {
|
||||
if (!appUpdateState) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!update.downloaded || !autoCheckUpdate) {
|
||||
if (!appUpdateState.downloaded || !autoCheckUpdate) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import App from '@renderer/components/MinApp/MinApp'
|
||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { Code, FileSearch, Folder, Languages, LayoutGrid, NotepadText, Palette, Sparkle } from 'lucide-react'
|
||||
import { FC, useMemo } from 'react'
|
||||
@ -12,8 +11,7 @@ const LaunchpadPage: FC = () => {
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation()
|
||||
const { defaultPaintingProvider } = useSettings()
|
||||
const { pinned } = useMinapps()
|
||||
const { openedKeepAliveMinapps } = useRuntime()
|
||||
const { pinned, openedKeepAliveMinapps } = useMinapps()
|
||||
|
||||
const appMenuItems = [
|
||||
{
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { PlusOutlined, RedoOutlined } from '@ant-design/icons'
|
||||
import { useCache } from '@data/hooks/useCache'
|
||||
import { usePreference } from '@data/hooks/usePreference'
|
||||
import { loggerService } from '@logger'
|
||||
import AiProvider from '@renderer/aiCore'
|
||||
@ -13,12 +14,9 @@ import { LanguagesEnum } from '@renderer/config/translate'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { usePaintings } from '@renderer/hooks/usePaintings'
|
||||
import { useAllProviders } from '@renderer/hooks/useProvider'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { getProviderLabel } from '@renderer/i18n/label'
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
import { translateText } from '@renderer/services/TranslateService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setGenerating } from '@renderer/store/runtime'
|
||||
import type { FileMetadata } from '@renderer/types'
|
||||
import type { PaintingAction, PaintingsState } from '@renderer/types'
|
||||
import { getErrorMessage, uuid } from '@renderer/utils'
|
||||
@ -90,8 +88,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
}
|
||||
}
|
||||
})
|
||||
const dispatch = useAppDispatch()
|
||||
const { generating } = useRuntime()
|
||||
const [generating, setGenerating] = useCache('chat.generating')
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const [autoTranslateWithSpace] = usePreference('chat.input.translate.auto_translate_with_space')
|
||||
@ -186,7 +183,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
const controller = new AbortController()
|
||||
setAbortController(controller)
|
||||
setIsLoading(true)
|
||||
dispatch(setGenerating(true))
|
||||
setGenerating(true)
|
||||
|
||||
let body: string | FormData = ''
|
||||
let headers: Record<string, string> = {
|
||||
@ -303,7 +300,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
handleError(error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
dispatch(setGenerating(false))
|
||||
setGenerating(false)
|
||||
setAbortController(null)
|
||||
}
|
||||
} else {
|
||||
@ -523,7 +520,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
handleError(error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
dispatch(setGenerating(false))
|
||||
setGenerating(false)
|
||||
setAbortController(null)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { PlusOutlined, RedoOutlined } from '@ant-design/icons'
|
||||
import { useCache } from '@data/hooks/useCache'
|
||||
import DMXAPIToImg from '@renderer/assets/images/providers/DMXAPI-to-img.webp'
|
||||
import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navbar'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
@ -7,11 +8,8 @@ import { isMac } from '@renderer/config/constant'
|
||||
import { getProviderLogo } from '@renderer/config/providers'
|
||||
import { usePaintings } from '@renderer/hooks/usePaintings'
|
||||
import { useAllProviders } from '@renderer/hooks/useProvider'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { getProviderLabel } from '@renderer/i18n/label'
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setGenerating } from '@renderer/store/runtime'
|
||||
import type { FileMetadata } from '@renderer/types'
|
||||
import { convertToBase64, uuid } from '@renderer/utils'
|
||||
import { DmxapiPainting } from '@types'
|
||||
@ -71,8 +69,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
const [currentImageIndex, setCurrentImageIndex] = useState(0)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [abortController, setAbortController] = useState<AbortController | null>(null)
|
||||
const dispatch = useAppDispatch()
|
||||
const { generating } = useRuntime()
|
||||
const [generating, setGenerating] = useCache('chat.generating')
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
|
||||
@ -558,7 +555,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
// 设置请求状态
|
||||
const controller = new AbortController()
|
||||
setAbortController(controller)
|
||||
dispatch(setGenerating(true))
|
||||
setGenerating(true)
|
||||
|
||||
// 准备请求配置
|
||||
const requestConfig = await prepareRequestConfig(prompt, painting)
|
||||
@ -602,7 +599,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
} finally {
|
||||
// 清理状态
|
||||
setIsLoading(false)
|
||||
dispatch(setGenerating(false))
|
||||
setGenerating(false)
|
||||
setAbortController(null)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { PlusOutlined } from '@ant-design/icons'
|
||||
import { useCache } from '@data/hooks/useCache'
|
||||
import { usePreference } from '@data/hooks/usePreference'
|
||||
import { loggerService } from '@logger'
|
||||
import AiProvider from '@renderer/aiCore'
|
||||
@ -12,7 +13,6 @@ import { LanguagesEnum } from '@renderer/config/translate'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { usePaintings } from '@renderer/hooks/usePaintings'
|
||||
import { useAllProviders } from '@renderer/hooks/useProvider'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import {
|
||||
getPaintingsBackgroundOptionsLabel,
|
||||
getPaintingsImageSizeOptionsLabel,
|
||||
@ -24,8 +24,6 @@ import PaintingsList from '@renderer/pages/paintings/components/PaintingsList'
|
||||
import { DEFAULT_PAINTING, MODELS, SUPPORTED_MODELS } from '@renderer/pages/paintings/config/NewApiConfig'
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
import { translateText } from '@renderer/services/TranslateService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setGenerating } from '@renderer/store/runtime'
|
||||
import type { PaintingAction, PaintingsState } from '@renderer/types'
|
||||
import { FileMetadata } from '@renderer/types'
|
||||
import { getErrorMessage, uuid } from '@renderer/utils'
|
||||
@ -81,8 +79,7 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
}
|
||||
}
|
||||
})
|
||||
const dispatch = useAppDispatch()
|
||||
const { generating } = useRuntime()
|
||||
const [generating, setGenerating] = useCache('chat.generating')
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const [autoTranslateWithSpace] = usePreference('chat.input.translate.auto_translate_with_space')
|
||||
@ -251,7 +248,7 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
const controller = new AbortController()
|
||||
setAbortController(controller)
|
||||
setIsLoading(true)
|
||||
dispatch(setGenerating(true))
|
||||
setGenerating(true)
|
||||
|
||||
let body: string | FormData = ''
|
||||
const headers: Record<string, string> = {
|
||||
@ -343,7 +340,7 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
handleError(error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
dispatch(setGenerating(false))
|
||||
setGenerating(false)
|
||||
setAbortController(null)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { PlusOutlined, RedoOutlined } from '@ant-design/icons'
|
||||
import { useCache } from '@data/hooks/useCache'
|
||||
import { usePreference } from '@data/hooks/usePreference'
|
||||
import { loggerService } from '@logger'
|
||||
import AiProvider from '@renderer/aiCore'
|
||||
@ -17,13 +18,10 @@ import { LanguagesEnum } from '@renderer/config/translate'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { usePaintings } from '@renderer/hooks/usePaintings'
|
||||
import { useAllProviders } from '@renderer/hooks/useProvider'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { getProviderLabel } from '@renderer/i18n/label'
|
||||
import { getProviderByModel } from '@renderer/services/AssistantService'
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
import { translateText } from '@renderer/services/TranslateService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setGenerating } from '@renderer/store/runtime'
|
||||
import type { FileMetadata, Painting } from '@renderer/types'
|
||||
import { getErrorMessage, uuid } from '@renderer/utils'
|
||||
import { Button, Input, InputNumber, Radio, Select, Slider, Switch, Tooltip } from 'antd'
|
||||
@ -128,8 +126,7 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [abortController, setAbortController] = useState<AbortController | null>(null)
|
||||
const dispatch = useAppDispatch()
|
||||
const { generating } = useRuntime()
|
||||
const [generating, setGenerating] = useCache('chat.generating')
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
|
||||
@ -196,7 +193,7 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
const controller = new AbortController()
|
||||
setAbortController(controller)
|
||||
setIsLoading(true)
|
||||
dispatch(setGenerating(true))
|
||||
setGenerating(true)
|
||||
const AI = new AiProvider(provider)
|
||||
|
||||
if (!painting.model) {
|
||||
@ -255,7 +252,7 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
dispatch(setGenerating(false))
|
||||
setGenerating(false)
|
||||
setAbortController(null)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { PlusOutlined } from '@ant-design/icons'
|
||||
import { useCache } from '@data/hooks/useCache'
|
||||
import { usePreference } from '@data/hooks/usePreference'
|
||||
import { loggerService } from '@logger'
|
||||
import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navbar'
|
||||
@ -9,12 +10,9 @@ import { getProviderLogo } from '@renderer/config/providers'
|
||||
import { LanguagesEnum } from '@renderer/config/translate'
|
||||
import { usePaintings } from '@renderer/hooks/usePaintings'
|
||||
import { useAllProviders } from '@renderer/hooks/useProvider'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { getProviderLabel } from '@renderer/i18n/label'
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
import { translateText } from '@renderer/services/TranslateService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setGenerating } from '@renderer/store/runtime'
|
||||
import type { TokenFluxPainting } from '@renderer/types'
|
||||
import { getErrorMessage, uuid } from '@renderer/utils'
|
||||
import { Avatar, Button, Select, Tooltip } from 'antd'
|
||||
@ -38,6 +36,7 @@ import TokenFluxService from './utils/TokenFluxService'
|
||||
const logger = loggerService.withContext('TokenFluxPage')
|
||||
|
||||
const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
const [generating, setGenerating] = useCache('chat.generating')
|
||||
const [models, setModels] = useState<TokenFluxModel[]>([])
|
||||
const [selectedModel, setSelectedModel] = useState<TokenFluxModel | null>(null)
|
||||
const [formData, setFormData] = useState<Record<string, any>>({})
|
||||
@ -70,8 +69,6 @@ const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
}
|
||||
})
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
const { generating } = useRuntime()
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const [autoTranslateWithSpace] = usePreference('chat.input.translate.auto_translate_with_space')
|
||||
@ -163,7 +160,7 @@ const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
const controller = new AbortController()
|
||||
setAbortController(controller)
|
||||
setIsLoading(true)
|
||||
dispatch(setGenerating(true))
|
||||
setGenerating(true)
|
||||
|
||||
try {
|
||||
const requestBody = {
|
||||
@ -197,12 +194,12 @@ const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
}
|
||||
|
||||
setIsLoading(false)
|
||||
dispatch(setGenerating(false))
|
||||
setGenerating(false)
|
||||
setAbortController(null)
|
||||
} catch (error: unknown) {
|
||||
handleError(error)
|
||||
setIsLoading(false)
|
||||
dispatch(setGenerating(false))
|
||||
setGenerating(false)
|
||||
setAbortController(null)
|
||||
}
|
||||
}
|
||||
@ -210,7 +207,7 @@ const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
const onCancel = () => {
|
||||
abortController?.abort()
|
||||
setIsLoading(false)
|
||||
dispatch(setGenerating(false))
|
||||
setGenerating(false)
|
||||
setAbortController(null)
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { PlusOutlined } from '@ant-design/icons'
|
||||
import { useCache } from '@data/hooks/useCache'
|
||||
import AiProvider from '@renderer/aiCore'
|
||||
import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navbar'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
@ -7,11 +8,8 @@ import { isMac } from '@renderer/config/constant'
|
||||
import { getProviderLogo } from '@renderer/config/providers'
|
||||
import { usePaintings } from '@renderer/hooks/usePaintings'
|
||||
import { useAllProviders } from '@renderer/hooks/useProvider'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { getProviderLabel } from '@renderer/i18n/label'
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setGenerating } from '@renderer/store/runtime'
|
||||
import { getErrorMessage, uuid } from '@renderer/utils'
|
||||
import { Avatar, Button, InputNumber, Radio, Select } from 'antd'
|
||||
import TextArea from 'antd/es/input/TextArea'
|
||||
@ -70,8 +68,7 @@ const ZhipuPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
const [currentImageIndex, setCurrentImageIndex] = useState(0)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [abortController, setAbortController] = useState<AbortController | null>(null)
|
||||
const dispatch = useAppDispatch()
|
||||
const { generating } = useRuntime()
|
||||
const [generating, setGenerating] = useCache('chat.generating')
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
|
||||
@ -118,7 +115,7 @@ const ZhipuPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
dispatch(setGenerating(true))
|
||||
setGenerating(true)
|
||||
const controller = new AbortController()
|
||||
setAbortController(controller)
|
||||
|
||||
@ -225,7 +222,7 @@ const ZhipuPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
dispatch(setGenerating(false))
|
||||
setGenerating(false)
|
||||
setAbortController(null)
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,11 +4,12 @@ import IndicatorLight from '@renderer/components/IndicatorLight'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { APP_NAME, AppLogo } from '@renderer/config/env'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useAppUpdateState } from '@renderer/hooks/useAppUpdate'
|
||||
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
// import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { handleSaveData, useAppDispatch } from '@renderer/store'
|
||||
import { setUpdateState } from '@renderer/store/runtime'
|
||||
import { handleSaveData } from '@renderer/store'
|
||||
// import { setUpdateState as setAppUpdateState } from '@renderer/store/runtime'
|
||||
import { runAsyncFunction } from '@renderer/utils'
|
||||
import { UpgradeChannel } from '@shared/data/preference/preferenceTypes'
|
||||
import { ThemeMode } from '@shared/data/preference/preferenceTypes'
|
||||
@ -33,23 +34,25 @@ const AboutSettings: FC = () => {
|
||||
const [isPortable, setIsPortable] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const dispatch = useAppDispatch()
|
||||
const { update } = useRuntime()
|
||||
// const dispatch = useAppDispatch()
|
||||
// const { update } = useRuntime()
|
||||
const { openMinapp } = useMinappPopup()
|
||||
|
||||
const { appUpdateState, updateAppUpdateState } = useAppUpdateState()
|
||||
|
||||
const onCheckUpdate = debounce(
|
||||
async () => {
|
||||
if (update.checking || update.downloading) {
|
||||
if (appUpdateState.checking || appUpdateState.downloading) {
|
||||
return
|
||||
}
|
||||
|
||||
if (update.downloaded) {
|
||||
if (appUpdateState.downloaded) {
|
||||
await handleSaveData()
|
||||
window.api.showUpdateDialog()
|
||||
return
|
||||
}
|
||||
|
||||
dispatch(setUpdateState({ checking: true }))
|
||||
updateAppUpdateState({ checking: true })
|
||||
|
||||
try {
|
||||
await window.api.checkForUpdate()
|
||||
@ -57,7 +60,7 @@ const AboutSettings: FC = () => {
|
||||
window.toast.error(t('settings.about.updateError'))
|
||||
}
|
||||
|
||||
dispatch(setUpdateState({ checking: false }))
|
||||
updateAppUpdateState({ checking: false })
|
||||
},
|
||||
2000,
|
||||
{ leading: true, trailing: false }
|
||||
@ -112,16 +115,14 @@ const AboutSettings: FC = () => {
|
||||
}
|
||||
setTestChannel(value)
|
||||
// Clear update info when switching upgrade channel
|
||||
dispatch(
|
||||
setUpdateState({
|
||||
available: false,
|
||||
info: null,
|
||||
downloaded: false,
|
||||
checking: false,
|
||||
downloading: false,
|
||||
downloadProgress: 0
|
||||
})
|
||||
)
|
||||
updateAppUpdateState({
|
||||
available: false,
|
||||
info: null,
|
||||
downloaded: false,
|
||||
checking: false,
|
||||
downloading: false,
|
||||
downloadProgress: 0
|
||||
})
|
||||
}
|
||||
|
||||
// Get available test version options based on current version
|
||||
@ -142,16 +143,14 @@ const AboutSettings: FC = () => {
|
||||
|
||||
const handleSetTestPlan = (value: boolean) => {
|
||||
setTestPlan(value)
|
||||
dispatch(
|
||||
setUpdateState({
|
||||
available: false,
|
||||
info: null,
|
||||
downloaded: false,
|
||||
checking: false,
|
||||
downloading: false,
|
||||
downloadProgress: 0
|
||||
})
|
||||
)
|
||||
updateAppUpdateState({
|
||||
available: false,
|
||||
info: null,
|
||||
downloaded: false,
|
||||
checking: false,
|
||||
downloading: false,
|
||||
downloadProgress: 0
|
||||
})
|
||||
|
||||
if (value === true) {
|
||||
setTestChannel(getTestChannel())
|
||||
@ -196,11 +195,11 @@ const AboutSettings: FC = () => {
|
||||
<AboutHeader>
|
||||
<Row align="middle">
|
||||
<AvatarWrapper onClick={() => onOpenWebsite('https://github.com/CherryHQ/cherry-studio')}>
|
||||
{update.downloadProgress > 0 && (
|
||||
{appUpdateState.downloadProgress > 0 && (
|
||||
<ProgressCircle
|
||||
type="circle"
|
||||
size={84}
|
||||
percent={update.downloadProgress}
|
||||
percent={appUpdateState.downloadProgress}
|
||||
showInfo={false}
|
||||
strokeLinecap="butt"
|
||||
strokeColor="#67ad5b"
|
||||
@ -222,11 +221,11 @@ const AboutSettings: FC = () => {
|
||||
{!isPortable && (
|
||||
<CheckUpdateButton
|
||||
onClick={onCheckUpdate}
|
||||
loading={update.checking}
|
||||
disabled={update.downloading || update.checking}>
|
||||
{update.downloading
|
||||
loading={appUpdateState.checking}
|
||||
disabled={appUpdateState.downloading || appUpdateState.checking}>
|
||||
{appUpdateState.downloading
|
||||
? t('settings.about.downloading')
|
||||
: update.available
|
||||
: appUpdateState.available
|
||||
? t('settings.about.checkUpdate.available')
|
||||
: t('settings.about.checkUpdate.label')}
|
||||
</CheckUpdateButton>
|
||||
@ -268,19 +267,19 @@ const AboutSettings: FC = () => {
|
||||
</>
|
||||
)}
|
||||
</SettingGroup>
|
||||
{update.info && update.available && (
|
||||
{appUpdateState.info && appUpdateState.available && (
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>
|
||||
{t('settings.about.updateAvailable', { version: update.info.version })}
|
||||
{t('settings.about.updateAvailable', { version: appUpdateState.info.version })}
|
||||
<IndicatorLight color="green" />
|
||||
</SettingRowTitle>
|
||||
</SettingRow>
|
||||
<UpdateNotesWrapper>
|
||||
<Markdown>
|
||||
{typeof update.info.releaseNotes === 'string'
|
||||
? update.info.releaseNotes.replace(/\n/g, '\n\n')
|
||||
: update.info.releaseNotes?.map((note) => note.note).join('\n')}
|
||||
{typeof appUpdateState.info.releaseNotes === 'string'
|
||||
? appUpdateState.info.releaseNotes.replace(/\n/g, '\n\n')
|
||||
: appUpdateState.info.releaseNotes?.map((note) => note.note).join('\n')}
|
||||
</Markdown>
|
||||
</UpdateNotesWrapper>
|
||||
</SettingGroup>
|
||||
|
||||
@ -18,7 +18,7 @@ import useTranslate from '@renderer/hooks/useTranslate'
|
||||
import { estimateTextTokens } from '@renderer/services/TokenService'
|
||||
import { saveTranslateHistory, translateText } from '@renderer/services/TranslateService'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { setTranslateAbortKey, setTranslating as setTranslatingAction } from '@renderer/store/runtime'
|
||||
// import { setTranslateAbortKey, setTranslating as setTranslatingAction } from '@renderer/store/runtime'
|
||||
import { setTranslatedContent as setTranslatedContentAction, setTranslateInput } from '@renderer/store/translate'
|
||||
import {
|
||||
type AutoDetectionMethod,
|
||||
@ -88,11 +88,13 @@ const TranslatePage: FC = () => {
|
||||
const [autoDetectionMethod, setAutoDetectionMethod] = useState<AutoDetectionMethod>('franc')
|
||||
const [isProcessing, setIsProcessing] = useState(false)
|
||||
|
||||
const [translating, setTranslating] = useState(false)
|
||||
const [abortKey, setTranslateAbortKey] = useState<string>('')
|
||||
// redux states
|
||||
const text = useAppSelector((state) => state.translate.translateInput)
|
||||
const translatedContent = useAppSelector((state) => state.translate.translatedContent)
|
||||
const translating = useAppSelector((state) => state.runtime.translating)
|
||||
const abortKey = useAppSelector((state) => state.runtime.translateAbortKey)
|
||||
// const translating = useAppSelector((state) => state.runtime.translating)
|
||||
// const abortKey = useAppSelector((state) => state.runtime.translateAbortKey)
|
||||
|
||||
// ref
|
||||
const contentContainerRef = useRef<HTMLDivElement>(null)
|
||||
@ -126,12 +128,12 @@ const TranslatePage: FC = () => {
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const setTranslating = useCallback(
|
||||
(translating: boolean) => {
|
||||
dispatch(setTranslatingAction(translating))
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
// const setTranslating = useCallback(
|
||||
// (translating: boolean) => {
|
||||
// dispatch(setTranslatingAction(translating))
|
||||
// },
|
||||
// [dispatch]
|
||||
// )
|
||||
|
||||
// 控制复制行为
|
||||
const onCopy = useCallback(async () => {
|
||||
@ -163,7 +165,7 @@ const TranslatePage: FC = () => {
|
||||
|
||||
let translated: string
|
||||
const abortKey = uuid()
|
||||
dispatch(setTranslateAbortKey(abortKey))
|
||||
setTranslateAbortKey(abortKey)
|
||||
|
||||
try {
|
||||
translated = await translateText(text, actualTargetLanguage, throttle(setTranslatedContent, 100), abortKey)
|
||||
@ -200,7 +202,7 @@ const TranslatePage: FC = () => {
|
||||
window.toast.error(t('translate.error.unknown') + ': ' + formatErrorMessage(e))
|
||||
}
|
||||
},
|
||||
[autoCopy, dispatch, onCopy, setTimeoutTimer, setTranslatedContent, setTranslating, t, translating]
|
||||
[autoCopy, onCopy, setTimeoutTimer, setTranslatedContent, setTranslating, t, translating]
|
||||
)
|
||||
|
||||
// 控制翻译按钮是否可用
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { cacheService } from '@data/CacheService'
|
||||
import { loggerService } from '@logger'
|
||||
import db from '@renderer/databases'
|
||||
import i18n from '@renderer/i18n'
|
||||
import store from '@renderer/store'
|
||||
import { FileMetadata } from '@renderer/types'
|
||||
import { getFileDirectory } from '@renderer/utils'
|
||||
import dayjs from 'dayjs'
|
||||
@ -81,7 +81,7 @@ class FileManager {
|
||||
const file = await db.files.get(id)
|
||||
|
||||
if (file) {
|
||||
const filesPath = store.getState().runtime.filesPath
|
||||
const filesPath = cacheService.get('filesPath') ?? ''
|
||||
file.path = filesPath + '/' + file.id + file.ext
|
||||
}
|
||||
|
||||
@ -89,7 +89,7 @@ class FileManager {
|
||||
}
|
||||
|
||||
static getFilePath(file: FileMetadata) {
|
||||
const filesPath = store.getState().runtime.filesPath
|
||||
const filesPath = cacheService.get('filesPath') ?? ''
|
||||
return filesPath + '/' + file.id + file.ext
|
||||
}
|
||||
|
||||
@ -137,7 +137,7 @@ class FileManager {
|
||||
}
|
||||
|
||||
static getFileUrl(file: FileMetadata) {
|
||||
const filesPath = store.getState().runtime.filesPath
|
||||
const filesPath = cacheService.get('filesPath') ?? ''
|
||||
return 'file://' + filesPath + '/' + file.name
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { loggerService } from '@logger'
|
||||
import SearchPopup from '@renderer/components/Popups/SearchPopup'
|
||||
import { DEFAULT_CONTEXTCOUNT, MAX_CONTEXT_COUNT, UNLIMITED_CONTEXT_COUNT } from '@renderer/config/constant'
|
||||
import { modelGenerating } from '@renderer/hooks/useModel'
|
||||
import { getTopicById } from '@renderer/hooks/useTopic'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { fetchMessagesSummary } from '@renderer/services/ApiService'
|
||||
import store from '@renderer/store'
|
||||
import { messageBlocksSelectors, removeManyBlocks } from '@renderer/store/messageBlock'
|
||||
@ -67,16 +67,8 @@ export function deleteMessageFiles(message: Message) {
|
||||
})
|
||||
}
|
||||
|
||||
export function isGenerating() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const generating = store.getState().runtime.generating
|
||||
generating && window.toast.warning(i18n.t('message.switch.disabled'))
|
||||
generating ? reject(false) : resolve(true)
|
||||
})
|
||||
}
|
||||
|
||||
export async function locateToMessage(navigate: NavigateFunction, message: Message) {
|
||||
await isGenerating()
|
||||
await modelGenerating()
|
||||
|
||||
SearchPopup.hide()
|
||||
const assistant = getAssistantById(message.assistantId)
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { cacheService } from '@data/CacheService'
|
||||
import { loggerService } from '@logger'
|
||||
import { DEFAULT_WEBSEARCH_RAG_DOCUMENT_COUNT } from '@renderer/config/constant'
|
||||
import i18n from '@renderer/i18n'
|
||||
import WebSearchEngineProvider from '@renderer/providers/WebSearchProvider'
|
||||
import { addSpan, endSpan } from '@renderer/services/SpanManagerService'
|
||||
import store from '@renderer/store'
|
||||
import { setWebSearchStatus } from '@renderer/store/runtime'
|
||||
import { CompressionConfig, WebSearchState } from '@renderer/store/websearch'
|
||||
import {
|
||||
KnowledgeBase,
|
||||
@ -202,12 +202,15 @@ class WebSearchService {
|
||||
* 设置网络搜索状态
|
||||
*/
|
||||
private async setWebSearchStatus(requestId: string, status: WebSearchStatus, delayMs?: number) {
|
||||
store.dispatch(setWebSearchStatus({ requestId, status }))
|
||||
const activeSearches = cacheService.get('chat.websearch.active_searches')
|
||||
activeSearches[requestId] = status
|
||||
|
||||
cacheService.set('chat.websearch.active_searches', activeSearches)
|
||||
|
||||
if (delayMs) {
|
||||
await new Promise((resolve) => setTimeout(resolve, delayMs))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保搜索压缩知识库存在并配置正确
|
||||
*/
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
/**
|
||||
* Data Refactor, notes by fullex
|
||||
* //TODO @deprecated this file will be removed
|
||||
*/
|
||||
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
import { AppLogo, UserAvatar } from '@renderer/config/env'
|
||||
import type { MinAppType, Topic, WebSearchStatus } from '@renderer/types'
|
||||
import type { Topic, WebSearchStatus } from '@renderer/types'
|
||||
import type { UpdateInfo } from 'builder-util-runtime'
|
||||
|
||||
export interface ChatState {
|
||||
@ -27,24 +31,24 @@ export interface UpdateState {
|
||||
}
|
||||
|
||||
export interface RuntimeState {
|
||||
avatar: string
|
||||
generating: boolean
|
||||
translating: boolean
|
||||
translateAbortKey?: string
|
||||
/** whether the minapp popup is shown */
|
||||
minappShow: boolean
|
||||
/** the minapps that are opened and should be keep alive */
|
||||
openedKeepAliveMinapps: MinAppType[]
|
||||
/** the minapp that is opened for one time */
|
||||
openedOneOffMinapp: MinAppType | null
|
||||
/** the current minapp id */
|
||||
currentMinappId: string
|
||||
searching: boolean
|
||||
filesPath: string
|
||||
resourcesPath: string
|
||||
update: UpdateState
|
||||
export: ExportState
|
||||
chat: ChatState
|
||||
// avatar: string
|
||||
// generating: boolean
|
||||
// translating: boolean
|
||||
// translateAbortKey?: string
|
||||
// /** whether the minapp popup is shown */
|
||||
// minappShow: boolean
|
||||
// /** the minapps that are opened and should be keep alive */
|
||||
// openedKeepAliveMinapps: MinAppType[]
|
||||
// /** the minapp that is opened for one time */
|
||||
// openedOneOffMinapp: MinAppType | null
|
||||
// /** the current minapp id */
|
||||
// currentMinappId: string
|
||||
// searching: boolean
|
||||
// filesPath: string
|
||||
// resourcesPath: string
|
||||
// update: UpdateState
|
||||
// export: ExportState
|
||||
// chat: ChatState
|
||||
websearch: WebSearchState
|
||||
}
|
||||
|
||||
@ -53,34 +57,34 @@ export interface ExportState {
|
||||
}
|
||||
|
||||
const initialState: RuntimeState = {
|
||||
avatar: UserAvatar,
|
||||
generating: false,
|
||||
translating: false,
|
||||
minappShow: false,
|
||||
openedKeepAliveMinapps: [],
|
||||
openedOneOffMinapp: null,
|
||||
currentMinappId: '',
|
||||
searching: false,
|
||||
filesPath: '',
|
||||
resourcesPath: '',
|
||||
update: {
|
||||
info: null,
|
||||
checking: false,
|
||||
downloading: false,
|
||||
downloaded: false,
|
||||
downloadProgress: 0,
|
||||
available: false
|
||||
},
|
||||
export: {
|
||||
isExporting: false
|
||||
},
|
||||
chat: {
|
||||
isMultiSelectMode: false,
|
||||
selectedMessageIds: [],
|
||||
activeTopic: null,
|
||||
renamingTopics: [],
|
||||
newlyRenamedTopics: []
|
||||
},
|
||||
// avatar: UserAvatar,
|
||||
// generating: false,
|
||||
// translating: false,
|
||||
// minappShow: false,
|
||||
// openedKeepAliveMinapps: [],
|
||||
// openedOneOffMinapp: null,
|
||||
// currentMinappId: '',
|
||||
// searching: false,
|
||||
// filesPath: '',
|
||||
// resourcesPath: '',
|
||||
// update: {
|
||||
// info: null,
|
||||
// checking: false,
|
||||
// downloading: false,
|
||||
// downloaded: false,
|
||||
// downloadProgress: 0,
|
||||
// available: false
|
||||
// },
|
||||
// export: {
|
||||
// isExporting: false
|
||||
// },
|
||||
// chat: {
|
||||
// isMultiSelectMode: false,
|
||||
// selectedMessageIds: [],
|
||||
// activeTopic: null,
|
||||
// renamingTopics: [],
|
||||
// newlyRenamedTopics: []
|
||||
// },
|
||||
websearch: {
|
||||
activeSearches: {}
|
||||
}
|
||||
@ -90,101 +94,105 @@ const runtimeSlice = createSlice({
|
||||
name: 'runtime',
|
||||
initialState,
|
||||
reducers: {
|
||||
setAvatar: (state, action: PayloadAction<string | null>) => {
|
||||
state.avatar = action.payload || AppLogo
|
||||
},
|
||||
setGenerating: (state, action: PayloadAction<boolean>) => {
|
||||
state.generating = action.payload
|
||||
},
|
||||
setTranslating: (state, action: PayloadAction<boolean>) => {
|
||||
state.translating = action.payload
|
||||
},
|
||||
setTranslateAbortKey: (state, action: PayloadAction<string>) => {
|
||||
state.translateAbortKey = action.payload
|
||||
},
|
||||
setMinappShow: (state, action: PayloadAction<boolean>) => {
|
||||
state.minappShow = action.payload
|
||||
},
|
||||
setOpenedKeepAliveMinapps: (state, action: PayloadAction<MinAppType[]>) => {
|
||||
state.openedKeepAliveMinapps = action.payload
|
||||
},
|
||||
setOpenedOneOffMinapp: (state, action: PayloadAction<MinAppType | null>) => {
|
||||
state.openedOneOffMinapp = action.payload
|
||||
},
|
||||
setCurrentMinappId: (state, action: PayloadAction<string>) => {
|
||||
state.currentMinappId = action.payload
|
||||
},
|
||||
setSearching: (state, action: PayloadAction<boolean>) => {
|
||||
state.searching = action.payload
|
||||
},
|
||||
setFilesPath: (state, action: PayloadAction<string>) => {
|
||||
state.filesPath = action.payload
|
||||
},
|
||||
setResourcesPath: (state, action: PayloadAction<string>) => {
|
||||
state.resourcesPath = action.payload
|
||||
},
|
||||
setUpdateState: (state, action: PayloadAction<Partial<UpdateState>>) => {
|
||||
state.update = { ...state.update, ...action.payload }
|
||||
},
|
||||
setExportState: (state, action: PayloadAction<Partial<ExportState>>) => {
|
||||
state.export = { ...state.export, ...action.payload }
|
||||
},
|
||||
// Chat related actions
|
||||
toggleMultiSelectMode: (state, action: PayloadAction<boolean>) => {
|
||||
state.chat.isMultiSelectMode = action.payload
|
||||
if (!action.payload) {
|
||||
state.chat.selectedMessageIds = []
|
||||
}
|
||||
},
|
||||
setSelectedMessageIds: (state, action: PayloadAction<string[]>) => {
|
||||
state.chat.selectedMessageIds = action.payload
|
||||
},
|
||||
setActiveTopic: (state, action: PayloadAction<Topic>) => {
|
||||
state.chat.activeTopic = action.payload
|
||||
},
|
||||
setRenamingTopics: (state, action: PayloadAction<string[]>) => {
|
||||
state.chat.renamingTopics = action.payload
|
||||
},
|
||||
setNewlyRenamedTopics: (state, action: PayloadAction<string[]>) => {
|
||||
state.chat.newlyRenamedTopics = action.payload
|
||||
},
|
||||
// WebSearch related actions
|
||||
setActiveSearches: (state, action: PayloadAction<Record<string, WebSearchStatus>>) => {
|
||||
state.websearch.activeSearches = action.payload
|
||||
},
|
||||
setWebSearchStatus: (state, action: PayloadAction<{ requestId: string; status: WebSearchStatus }>) => {
|
||||
const { requestId, status } = action.payload
|
||||
if (status.phase === 'default') {
|
||||
delete state.websearch.activeSearches[requestId]
|
||||
}
|
||||
state.websearch.activeSearches[requestId] = status
|
||||
// setAvatar: (state, action: PayloadAction<string | null>) => {
|
||||
// state.avatar = action.payload || AppLogo
|
||||
// },
|
||||
// setGenerating: (state, action: PayloadAction<boolean>) => {
|
||||
// state.generating = action.payload
|
||||
// },
|
||||
// setTranslating: (state, action: PayloadAction<boolean>) => {
|
||||
// state.translating = action.payload
|
||||
// },
|
||||
// setTranslateAbortKey: (state, action: PayloadAction<string>) => {
|
||||
// state.translateAbortKey = action.payload
|
||||
// },
|
||||
// setMinappShow: (state, action: PayloadAction<boolean>) => {
|
||||
// state.minappShow = action.payload
|
||||
// },
|
||||
// setOpenedKeepAliveMinapps: (state, action: PayloadAction<MinAppType[]>) => {
|
||||
// state.openedKeepAliveMinapps = action.payload
|
||||
// },
|
||||
// setOpenedOneOffMinapp: (state, action: PayloadAction<MinAppType | null>) => {
|
||||
// state.openedOneOffMinapp = action.payload
|
||||
// },
|
||||
// setCurrentMinappId: (state, action: PayloadAction<string>) => {
|
||||
// state.currentMinappId = action.payload
|
||||
// },
|
||||
// setSearching: (state, action: PayloadAction<boolean>) => {
|
||||
// state.searching = action.payload
|
||||
// },
|
||||
// setFilesPath: (state, action: PayloadAction<string>) => {
|
||||
// state.filesPath = action.payload
|
||||
// },
|
||||
// setResourcesPath: (state, action: PayloadAction<string>) => {
|
||||
// state.resourcesPath = action.payload
|
||||
// },
|
||||
// setUpdateState: (state, action: PayloadAction<Partial<UpdateState>>) => {
|
||||
// state.update = { ...state.update, ...action.payload }
|
||||
// },
|
||||
// setExportState: (state, action: PayloadAction<Partial<ExportState>>) => {
|
||||
// state.export = { ...state.export, ...action.payload }
|
||||
// },
|
||||
// // Chat related actions
|
||||
// toggleMultiSelectMode: (state, action: PayloadAction<boolean>) => {
|
||||
// state.chat.isMultiSelectMode = action.payload
|
||||
// if (!action.payload) {
|
||||
// state.chat.selectedMessageIds = []
|
||||
// }
|
||||
// },
|
||||
// setSelectedMessageIds: (state, action: PayloadAction<string[]>) => {
|
||||
// state.chat.selectedMessageIds = action.payload
|
||||
// },
|
||||
// setActiveTopic: (state, action: PayloadAction<Topic>) => {
|
||||
// state.chat.activeTopic = action.payload
|
||||
// },
|
||||
// setRenamingTopics: (state, action: PayloadAction<string[]>) => {
|
||||
// state.chat.renamingTopics = action.payload
|
||||
// },
|
||||
// setNewlyRenamedTopics: (state, action: PayloadAction<string[]>) => {
|
||||
// state.chat.newlyRenamedTopics = action.payload
|
||||
// },
|
||||
// // WebSearch related actions
|
||||
// setActiveSearches: (state, action: PayloadAction<Record<string, WebSearchStatus>>) => {
|
||||
// state.websearch.activeSearches = action.payload
|
||||
// },
|
||||
// setWebSearchStatus: (state, action: PayloadAction<{ requestId: string; status: WebSearchStatus }>) => {
|
||||
// const { requestId, status } = action.payload
|
||||
// if (status.phase === 'default') {
|
||||
// delete state.websearch.activeSearches[requestId]
|
||||
// }
|
||||
// state.websearch.activeSearches[requestId] = status
|
||||
// },
|
||||
setPlaceholder: (state, action: PayloadAction<Partial<RuntimeState>>) => {
|
||||
state = { ...state, ...action.payload }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const {
|
||||
setAvatar,
|
||||
setGenerating,
|
||||
setTranslating,
|
||||
setTranslateAbortKey,
|
||||
setMinappShow,
|
||||
setOpenedKeepAliveMinapps,
|
||||
setOpenedOneOffMinapp,
|
||||
setCurrentMinappId,
|
||||
setSearching,
|
||||
setFilesPath,
|
||||
setResourcesPath,
|
||||
setUpdateState,
|
||||
setExportState,
|
||||
// Chat related actions
|
||||
toggleMultiSelectMode,
|
||||
setSelectedMessageIds,
|
||||
setActiveTopic,
|
||||
setRenamingTopics,
|
||||
setNewlyRenamedTopics,
|
||||
// WebSearch related actions
|
||||
setActiveSearches,
|
||||
setWebSearchStatus
|
||||
// setAvatar,
|
||||
// setGenerating,
|
||||
// setTranslating,
|
||||
// setTranslateAbortKey,
|
||||
// setMinappShow,
|
||||
// setOpenedKeepAliveMinapps,
|
||||
// setOpenedOneOffMinapp,
|
||||
// setCurrentMinappId,
|
||||
// setSearching,
|
||||
// setFilesPath,
|
||||
// setResourcesPath,
|
||||
// setUpdateState,
|
||||
// setExportState,
|
||||
// // Chat related actions
|
||||
// toggleMultiSelectMode,
|
||||
// setSelectedMessageIds,
|
||||
// setActiveTopic,
|
||||
// setRenamingTopics,
|
||||
// setNewlyRenamedTopics,
|
||||
// // WebSearch related actions
|
||||
// setActiveSearches,
|
||||
// setWebSearchStatus,
|
||||
setPlaceholder
|
||||
} = runtimeSlice.actions
|
||||
|
||||
export default runtimeSlice.reducer
|
||||
|
||||
@ -5,8 +5,6 @@ import i18n from '@renderer/i18n'
|
||||
import { getProviderLabel } from '@renderer/i18n/label'
|
||||
import { getMessageTitle } from '@renderer/services/MessagesService'
|
||||
import { createNote } from '@renderer/services/NotesService'
|
||||
import store from '@renderer/store'
|
||||
import { setExportState } from '@renderer/store/runtime'
|
||||
import type { Topic } from '@renderer/types'
|
||||
import type { Message } from '@renderer/types/newMessage'
|
||||
import { NotesTreeNode } from '@renderer/types/note'
|
||||
@ -20,12 +18,11 @@ import { appendBlocks } from 'notion-helper'
|
||||
|
||||
const logger = loggerService.withContext('Utils:export')
|
||||
|
||||
// 全局的导出状态获取函数
|
||||
const getExportState = () => store.getState().runtime.export.isExporting
|
||||
let exportState = false
|
||||
|
||||
// 全局的导出状态设置函数,使用 dispatch 保障 Redux 状态更新正确
|
||||
const getExportState = () => exportState
|
||||
const setExportingState = (isExporting: boolean) => {
|
||||
store.dispatch(setExportState({ isExporting }))
|
||||
exportState = isExporting
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -2,7 +2,7 @@ import { AppLogo } from '@renderer/config/env'
|
||||
import { usePreference } from '@renderer/data/hooks/usePreference'
|
||||
import { loggerService } from '@renderer/services/LoggerService'
|
||||
import { ThemeMode } from '@shared/data/preference/preferenceTypes'
|
||||
import { Button, Card, Col, Divider, Layout, Row, Space, Typography, Tabs } from 'antd'
|
||||
import { Button, Card, Col, Divider, Layout, Row, Space, Tabs, Typography } from 'antd'
|
||||
import { Activity, AlertTriangle, Database, FlaskConical, Settings, TestTube, TrendingUp, Zap } from 'lucide-react'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
@ -104,11 +104,12 @@ const TestApp: React.FC = () => {
|
||||
</Title>
|
||||
</Space>
|
||||
<Text style={{ color: isDarkTheme ? '#d9d9d9' : 'rgba(0, 0, 0, 0.45)' }}>
|
||||
此测试窗口用于验证数据重构项目的各项功能,包括 PreferenceService、CacheService、DataApiService 和相关 React hooks
|
||||
的完整测试套件。
|
||||
此测试窗口用于验证数据重构项目的各项功能,包括 PreferenceService、CacheService、DataApiService
|
||||
和相关 React hooks 的完整测试套件。
|
||||
</Text>
|
||||
<Text style={{ color: isDarkTheme ? '#d9d9d9' : 'rgba(0, 0, 0, 0.45)' }}>
|
||||
PreferenceService 测试使用真实的偏好设置系统,CacheService 测试使用三层缓存架构,DataApiService 测试使用专用的测试路由和假数据。
|
||||
PreferenceService 测试使用真实的偏好设置系统,CacheService 测试使用三层缓存架构,DataApiService
|
||||
测试使用专用的测试路由和假数据。
|
||||
</Text>
|
||||
<Text style={{ color: 'var(--color-primary)', fontWeight: 'bold' }}>
|
||||
📋 跨窗口测试指南:在一个窗口中修改偏好设置,观察其他窗口是否实时同步更新。
|
||||
@ -403,39 +404,39 @@ const Container = styled.div`
|
||||
|
||||
const StyledTabs = styled(Tabs)<{ $isDark: boolean }>`
|
||||
.ant-tabs-nav {
|
||||
background: ${props => props.$isDark ? '#262626' : '#fafafa'};
|
||||
background: ${(props) => (props.$isDark ? '#262626' : '#fafafa')};
|
||||
border-radius: 6px 6px 0 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.ant-tabs-tab {
|
||||
color: ${props => props.$isDark ? '#d9d9d9' : '#666'} !important;
|
||||
color: ${(props) => (props.$isDark ? '#d9d9d9' : '#666')} !important;
|
||||
|
||||
&:hover {
|
||||
color: ${props => props.$isDark ? '#fff' : '#000'} !important;
|
||||
color: ${(props) => (props.$isDark ? '#fff' : '#000')} !important;
|
||||
}
|
||||
|
||||
&.ant-tabs-tab-active {
|
||||
color: ${props => props.$isDark ? '#1890ff' : '#1890ff'} !important;
|
||||
color: ${(props) => (props.$isDark ? '#1890ff' : '#1890ff')} !important;
|
||||
|
||||
.ant-tabs-tab-btn {
|
||||
color: ${props => props.$isDark ? '#1890ff' : '#1890ff'} !important;
|
||||
color: ${(props) => (props.$isDark ? '#1890ff' : '#1890ff')} !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-ink-bar {
|
||||
background: ${props => props.$isDark ? '#1890ff' : '#1890ff'};
|
||||
background: ${(props) => (props.$isDark ? '#1890ff' : '#1890ff')};
|
||||
}
|
||||
|
||||
.ant-tabs-content {
|
||||
background: ${props => props.$isDark ? '#1f1f1f' : '#fff'};
|
||||
background: ${(props) => (props.$isDark ? '#1f1f1f' : '#fff')};
|
||||
border-radius: 0 0 6px 6px;
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
.ant-tabs-tabpane {
|
||||
color: ${props => props.$isDark ? '#fff' : '#000'};
|
||||
color: ${(props) => (props.$isDark ? '#fff' : '#000')};
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@ -3,12 +3,12 @@ import { useCache, useSharedCache } from '@renderer/data/hooks/useCache'
|
||||
import { usePreference } from '@renderer/data/hooks/usePreference'
|
||||
import { loggerService } from '@renderer/services/LoggerService'
|
||||
import { ThemeMode } from '@shared/data/preference/preferenceTypes'
|
||||
import { Button, Input, message, Space, Typography, Card, Row, Col, Divider, Progress, Badge, Tag } from 'antd'
|
||||
import { Clock, Shield, Zap, Activity, AlertTriangle, CheckCircle, XCircle, Timer } from 'lucide-react'
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { Badge, Button, Card, Col, Divider, message, Progress, Row, Space, Tag, Typography } from 'antd'
|
||||
import { Activity, AlertTriangle, CheckCircle, Clock, Shield, Timer, XCircle, Zap } from 'lucide-react'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const { Text, Title } = Typography
|
||||
const { Text } = Typography
|
||||
|
||||
const logger = loggerService.withContext('CacheAdvancedTests')
|
||||
|
||||
@ -21,37 +21,37 @@ const CacheAdvancedTests: React.FC = () => {
|
||||
const isDarkTheme = currentTheme === ThemeMode.dark
|
||||
|
||||
// TTL Testing
|
||||
const [ttlKey] = useState('test-ttl-cache')
|
||||
const [ttlValue, setTtlValue] = useCache(ttlKey)
|
||||
const [ttlKey] = useState('test-ttl-cache' as const)
|
||||
const [ttlValue, setTtlValue] = useCache(ttlKey as any, 'test-ttl-cache')
|
||||
const [ttlExpireTime, setTtlExpireTime] = useState<number | null>(null)
|
||||
const [ttlProgress, setTtlProgress] = useState(0)
|
||||
|
||||
// Hook Reference Tracking
|
||||
const [protectedKey] = useState('test-protected-cache')
|
||||
const [protectedValue, setProtectedValue] = useCache(protectedKey, 'protected-value')
|
||||
const [protectedKey] = useState('test-protected-cache' as const)
|
||||
const [protectedValue] = useCache(protectedKey as any, 'protected-value')
|
||||
const [deleteAttemptResult, setDeleteAttemptResult] = useState<string>('')
|
||||
|
||||
// Deep Equality Testing
|
||||
const [deepEqualKey] = useState('test-deep-equal')
|
||||
const [objectValue, setObjectValue] = useCache(deepEqualKey, { nested: { count: 0 }, tags: ['initial'] })
|
||||
const [updateSkipCount, setUpdateSkipCount] = useState(0)
|
||||
const [deepEqualKey] = useState('test-deep-equal' as const)
|
||||
const [objectValue, setObjectValue] = useCache(deepEqualKey as any, { nested: { count: 0 }, tags: ['initial'] })
|
||||
const [updateSkipCount] = useState(0)
|
||||
|
||||
// Performance Testing
|
||||
const [perfKey] = useState('test-performance')
|
||||
const [perfValue, setPerfValue] = useCache(perfKey, 0)
|
||||
const [perfKey] = useState('test-performance' as const)
|
||||
const [perfValue, setPerfValue] = useCache(perfKey as any, 0)
|
||||
const [rapidUpdateCount, setRapidUpdateCount] = useState(0)
|
||||
const [subscriptionTriggers, setSubscriptionTriggers] = useState(0)
|
||||
const renderCountRef = useRef(0)
|
||||
const [displayRenderCount, setDisplayRenderCount] = useState(0)
|
||||
|
||||
// Multi-hook testing
|
||||
const [multiKey] = useState('test-multi-hook')
|
||||
const [value1] = useCache(multiKey, 'hook-1-default')
|
||||
const [value2] = useCache(multiKey, 'hook-2-default')
|
||||
const [value3] = useSharedCache(multiKey, 'hook-3-shared')
|
||||
const [multiKey] = useState('test-multi-hook' as const)
|
||||
const [value1] = useCache(multiKey as any, 'hook-1-default')
|
||||
const [value2] = useCache(multiKey as any, 'hook-2-default')
|
||||
const [value3] = useSharedCache(multiKey as any, 'hook-3-shared')
|
||||
|
||||
const intervalRef = useRef<NodeJS.Timeout>()
|
||||
const performanceTestRef = useRef<NodeJS.Timeout>()
|
||||
const intervalRef = useRef<NodeJS.Timeout>(null)
|
||||
const performanceTestRef = useRef<NodeJS.Timeout>(null)
|
||||
|
||||
// Update render count without causing re-renders
|
||||
renderCountRef.current += 1
|
||||
@ -59,53 +59,56 @@ const CacheAdvancedTests: React.FC = () => {
|
||||
// Track subscription changes
|
||||
useEffect(() => {
|
||||
const unsubscribe = cacheService.subscribe(perfKey, () => {
|
||||
setSubscriptionTriggers(prev => prev + 1)
|
||||
setSubscriptionTriggers((prev) => prev + 1)
|
||||
})
|
||||
return unsubscribe
|
||||
}, [perfKey])
|
||||
|
||||
// TTL Testing Functions
|
||||
const startTTLTest = useCallback((ttlMs: number) => {
|
||||
const testValue = { message: 'TTL Test', timestamp: Date.now() }
|
||||
cacheService.set(ttlKey, testValue, ttlMs)
|
||||
setTtlValue(testValue)
|
||||
const startTTLTest = useCallback(
|
||||
(ttlMs: number) => {
|
||||
const testValue = { message: 'TTL Test', timestamp: Date.now() }
|
||||
cacheService.set(ttlKey, testValue, ttlMs)
|
||||
setTtlValue(testValue.message)
|
||||
|
||||
const expireAt = Date.now() + ttlMs
|
||||
setTtlExpireTime(expireAt)
|
||||
const expireAt = Date.now() + ttlMs
|
||||
setTtlExpireTime(expireAt)
|
||||
|
||||
// Clear previous interval
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current)
|
||||
}
|
||||
|
||||
// Update progress every 100ms
|
||||
intervalRef.current = setInterval(() => {
|
||||
const now = Date.now()
|
||||
const remaining = Math.max(0, expireAt - now)
|
||||
const progress = Math.max(0, 100 - (remaining / ttlMs) * 100)
|
||||
|
||||
setTtlProgress(progress)
|
||||
|
||||
if (remaining <= 0) {
|
||||
clearInterval(intervalRef.current!)
|
||||
setTtlExpireTime(null)
|
||||
message.info('TTL expired, checking value...')
|
||||
|
||||
// Check if value is actually expired
|
||||
setTimeout(() => {
|
||||
const currentValue = cacheService.get(ttlKey)
|
||||
if (currentValue === undefined) {
|
||||
message.success('TTL expiration working correctly!')
|
||||
} else {
|
||||
message.warning('TTL expiration may have failed')
|
||||
}
|
||||
}, 100)
|
||||
// Clear previous interval
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current)
|
||||
}
|
||||
}, 100)
|
||||
|
||||
message.info(`TTL test started: ${ttlMs}ms`)
|
||||
logger.info('TTL test started', { key: ttlKey, ttl: ttlMs, expireAt })
|
||||
}, [ttlKey, setTtlValue])
|
||||
// Update progress every 100ms
|
||||
intervalRef.current = setInterval(() => {
|
||||
const now = Date.now()
|
||||
const remaining = Math.max(0, expireAt - now)
|
||||
const progress = Math.max(0, 100 - (remaining / ttlMs) * 100)
|
||||
|
||||
setTtlProgress(progress)
|
||||
|
||||
if (remaining <= 0) {
|
||||
clearInterval(intervalRef.current!)
|
||||
setTtlExpireTime(null)
|
||||
message.info('TTL expired, checking value...')
|
||||
|
||||
// Check if value is actually expired
|
||||
setTimeout(() => {
|
||||
const currentValue = cacheService.get(ttlKey)
|
||||
if (currentValue === undefined) {
|
||||
message.success('TTL expiration working correctly!')
|
||||
} else {
|
||||
message.warning('TTL expiration may have failed')
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
}, 100)
|
||||
|
||||
message.info(`TTL test started: ${ttlMs}ms`)
|
||||
logger.info('TTL test started', { key: ttlKey, ttl: ttlMs, expireAt })
|
||||
},
|
||||
[ttlKey, setTtlValue]
|
||||
)
|
||||
|
||||
// Hook Reference Tracking Test
|
||||
const testDeleteProtection = () => {
|
||||
@ -126,7 +129,7 @@ const CacheAdvancedTests: React.FC = () => {
|
||||
switch (operation) {
|
||||
case 'same-reference':
|
||||
// Set same reference - should skip
|
||||
setObjectValue(objectValue)
|
||||
setObjectValue(objectValue as { nested: { count: number }; tags: string[] })
|
||||
break
|
||||
|
||||
case 'same-content':
|
||||
@ -198,12 +201,19 @@ const CacheAdvancedTests: React.FC = () => {
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Space>
|
||||
<Text type="secondary">Advanced Features • Renders: {displayRenderCount || renderCountRef.current} • Subscriptions: {subscriptionTriggers}</Text>
|
||||
<Button size="small" onClick={() => {
|
||||
renderCountRef.current = 0
|
||||
setDisplayRenderCount(0)
|
||||
setSubscriptionTriggers(0)
|
||||
}}>Reset Stats</Button>
|
||||
<Text type="secondary">
|
||||
Advanced Features • Renders: {displayRenderCount || renderCountRef.current} • Subscriptions:{' '}
|
||||
{subscriptionTriggers}
|
||||
</Text>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
renderCountRef.current = 0
|
||||
setDisplayRenderCount(0)
|
||||
setSubscriptionTriggers(0)
|
||||
}}>
|
||||
Reset Stats
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
@ -221,10 +231,11 @@ const CacheAdvancedTests: React.FC = () => {
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff',
|
||||
borderColor: isDarkTheme ? '#303030' : '#d9d9d9'
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Text>Key: <code>{ttlKey}</code></Text>
|
||||
<Text>
|
||||
Key: <code>{ttlKey}</code>
|
||||
</Text>
|
||||
|
||||
<Space wrap>
|
||||
<Button size="small" onClick={() => startTTLTest(2000)} icon={<Clock size={12} />}>
|
||||
@ -270,27 +281,19 @@ const CacheAdvancedTests: React.FC = () => {
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff',
|
||||
borderColor: isDarkTheme ? '#303030' : '#d9d9d9'
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Text>Key: <code>{protectedKey}</code></Text>
|
||||
<Badge
|
||||
status="processing"
|
||||
text="This hook is actively using the cache key"
|
||||
/>
|
||||
<Text>
|
||||
Key: <code>{protectedKey}</code>
|
||||
</Text>
|
||||
<Badge status="processing" text="This hook is actively using the cache key" />
|
||||
|
||||
<Button
|
||||
danger
|
||||
onClick={testDeleteProtection}
|
||||
icon={<AlertTriangle size={12} />}
|
||||
>
|
||||
<Button danger onClick={testDeleteProtection} icon={<AlertTriangle size={12} />}>
|
||||
Attempt to Delete Key
|
||||
</Button>
|
||||
|
||||
{deleteAttemptResult && (
|
||||
<Tag color={deleteAttemptResult.includes('Protected') ? 'green' : 'red'}>
|
||||
{deleteAttemptResult}
|
||||
</Tag>
|
||||
<Tag color={deleteAttemptResult.includes('Protected') ? 'green' : 'red'}>{deleteAttemptResult}</Tag>
|
||||
)}
|
||||
|
||||
<ResultDisplay $isDark={isDarkTheme}>
|
||||
@ -316,17 +319,23 @@ const CacheAdvancedTests: React.FC = () => {
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff',
|
||||
borderColor: isDarkTheme ? '#303030' : '#d9d9d9'
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Text>Key: <code>{deepEqualKey}</code></Text>
|
||||
<Text>Skip Count: <Badge count={updateSkipCount} /></Text>
|
||||
<Text>
|
||||
Key: <code>{deepEqualKey}</code>
|
||||
</Text>
|
||||
<Text>
|
||||
Skip Count: <Badge count={updateSkipCount} />
|
||||
</Text>
|
||||
|
||||
<Space direction="vertical">
|
||||
<Button size="small" onClick={() => testDeepEquality('same-reference')} icon={<XCircle size={12} />}>
|
||||
Set Same Reference
|
||||
</Button>
|
||||
<Button size="small" onClick={() => testDeepEquality('same-content')} icon={<CheckCircle size={12} />}>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => testDeepEquality('same-content')}
|
||||
icon={<CheckCircle size={12} />}>
|
||||
Set Same Content
|
||||
</Button>
|
||||
<Button size="small" onClick={() => testDeepEquality('different-content')} icon={<Zap size={12} />}>
|
||||
@ -355,11 +364,14 @@ const CacheAdvancedTests: React.FC = () => {
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff',
|
||||
borderColor: isDarkTheme ? '#303030' : '#d9d9d9'
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Text>Key: <code>{perfKey}</code></Text>
|
||||
<Text>Updates: <Badge count={rapidUpdateCount} /></Text>
|
||||
<Text>
|
||||
Key: <code>{perfKey}</code>
|
||||
</Text>
|
||||
<Text>
|
||||
Updates: <Badge count={rapidUpdateCount} />
|
||||
</Text>
|
||||
|
||||
<Space>
|
||||
<Button type="primary" onClick={startRapidUpdates} icon={<Zap size={12} />}>
|
||||
@ -393,10 +405,11 @@ const CacheAdvancedTests: React.FC = () => {
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff',
|
||||
borderColor: isDarkTheme ? '#303030' : '#d9d9d9'
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Text>Testing multiple hooks using the same key: <code>{multiKey}</code></Text>
|
||||
<Text>
|
||||
Testing multiple hooks using the same key: <code>{multiKey}</code>
|
||||
</Text>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={8}>
|
||||
@ -423,14 +436,11 @@ const CacheAdvancedTests: React.FC = () => {
|
||||
</Row>
|
||||
|
||||
<Space>
|
||||
<Button
|
||||
onClick={() => cacheService.set(multiKey, `Updated at ${new Date().toLocaleTimeString()}`)}
|
||||
>
|
||||
<Button onClick={() => cacheService.set(multiKey, `Updated at ${new Date().toLocaleTimeString()}`)}>
|
||||
Update via CacheService
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => cacheService.setShared(multiKey, `Shared update at ${new Date().toLocaleTimeString()}`)}
|
||||
>
|
||||
onClick={() => cacheService.setShared(multiKey, `Shared update at ${new Date().toLocaleTimeString()}`)}>
|
||||
Update via Shared Cache
|
||||
</Button>
|
||||
</Space>
|
||||
@ -448,12 +458,12 @@ const CacheAdvancedTests: React.FC = () => {
|
||||
}
|
||||
|
||||
const TestContainer = styled.div<{ $isDark: boolean }>`
|
||||
color: ${props => props.$isDark ? '#fff' : '#000'};
|
||||
color: ${(props) => (props.$isDark ? '#fff' : '#000')};
|
||||
`
|
||||
|
||||
const ResultDisplay = styled.div<{ $isDark: boolean }>`
|
||||
background: ${props => props.$isDark ? '#0d1117' : '#f6f8fa'};
|
||||
border: 1px solid ${props => props.$isDark ? '#30363d' : '#d0d7de'};
|
||||
background: ${(props) => (props.$isDark ? '#0d1117' : '#f6f8fa')};
|
||||
border: 1px solid ${(props) => (props.$isDark ? '#30363d' : '#d0d7de')};
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
font-size: 11px;
|
||||
@ -464,9 +474,9 @@ const ResultDisplay = styled.div<{ $isDark: boolean }>`
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
color: ${props => props.$isDark ? '#e6edf3' : '#1f2328'};
|
||||
color: ${(props) => (props.$isDark ? '#e6edf3' : '#1f2328')};
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
}
|
||||
`
|
||||
|
||||
export default CacheAdvancedTests
|
||||
export default CacheAdvancedTests
|
||||
|
||||
@ -1,16 +1,15 @@
|
||||
import { useCache, useSharedCache, usePersistCache } from '@renderer/data/hooks/useCache'
|
||||
import { useCache, usePersistCache, useSharedCache } from '@renderer/data/hooks/useCache'
|
||||
import { usePreference } from '@renderer/data/hooks/usePreference'
|
||||
import { loggerService } from '@renderer/services/LoggerService'
|
||||
import type { PersistCacheKey } from '@shared/data/cache/cacheSchemas'
|
||||
import type { RendererPersistCacheKey } from '@shared/data/cache/cacheSchemas'
|
||||
import { ThemeMode } from '@shared/data/preference/preferenceTypes'
|
||||
import { Button, Input, message, Select, Space, Typography, Card, Row, Col, Divider, Slider } from 'antd'
|
||||
import { Zap, Database, Eye, Edit, RefreshCw, Users, HardDrive } from 'lucide-react'
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import { Button, Card, Col, Divider, Input, message, Row, Select, Slider, Space, Typography } from 'antd'
|
||||
import { Database, Edit, Eye, HardDrive, RefreshCw, Users, Zap } from 'lucide-react'
|
||||
import React, { useRef, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const { Text, Title } = Typography
|
||||
const { Text } = Typography
|
||||
const { Option } = Select
|
||||
const { TextArea } = Input
|
||||
|
||||
const logger = loggerService.withContext('CacheBasicTests')
|
||||
|
||||
@ -26,25 +25,25 @@ const CacheBasicTests: React.FC = () => {
|
||||
const [memoryCacheKey, setMemoryCacheKey] = useState('test-hook-memory-1')
|
||||
const [memoryCacheDefault, setMemoryCacheDefault] = useState('default-memory-value')
|
||||
const [newMemoryValue, setNewMemoryValue] = useState('')
|
||||
const [memoryValue, setMemoryValue] = useCache(memoryCacheKey, memoryCacheDefault)
|
||||
const [memoryValue, setMemoryValue] = useCache(memoryCacheKey as any, memoryCacheDefault)
|
||||
|
||||
// useSharedCache testing
|
||||
const [sharedCacheKey, setSharedCacheKey] = useState('test-hook-shared-1')
|
||||
const [sharedCacheDefault, setSharedCacheDefault] = useState('default-shared-value')
|
||||
const [newSharedValue, setNewSharedValue] = useState('')
|
||||
const [sharedValue, setSharedValue] = useSharedCache(sharedCacheKey, sharedCacheDefault)
|
||||
const [sharedValue, setSharedValue] = useSharedCache(sharedCacheKey as any, sharedCacheDefault)
|
||||
|
||||
// usePersistCache testing
|
||||
const [persistCacheKey, setPersistCacheKey] = useState<PersistCacheKey>('example-1')
|
||||
const [persistCacheKey, setPersistCacheKey] = useState<RendererPersistCacheKey>('example-1')
|
||||
const [newPersistValue, setNewPersistValue] = useState('')
|
||||
const [persistValue, setPersistValue] = usePersistCache(persistCacheKey)
|
||||
|
||||
// Testing different data types
|
||||
const [numberKey] = useState('test-number-cache')
|
||||
const [numberValue, setNumberValue] = useCache(numberKey, 42)
|
||||
const [numberKey] = useState('test-number-cache' as const)
|
||||
const [numberValue, setNumberValue] = useCache(numberKey as any, 42)
|
||||
|
||||
const [objectKey] = useState('test-object-cache')
|
||||
const [objectValue, setObjectValue] = useCache(objectKey, { name: 'test', count: 0, active: true })
|
||||
const [objectKey] = useState('test-object-cache' as const)
|
||||
const [objectValue, setObjectValue] = useCache(objectKey as any, { name: 'test', count: 0, active: true })
|
||||
|
||||
// Stats
|
||||
const renderCountRef = useRef(0)
|
||||
@ -52,7 +51,7 @@ const CacheBasicTests: React.FC = () => {
|
||||
const [updateCount, setUpdateCount] = useState(0)
|
||||
|
||||
// Available persist keys
|
||||
const persistKeys: PersistCacheKey[] = ['example-1', 'example-2', 'example-3', 'example-4']
|
||||
const persistKeys: RendererPersistCacheKey[] = ['example-1', 'example-2', 'example-3', 'example-4']
|
||||
|
||||
// Update render count without causing re-renders
|
||||
renderCountRef.current += 1
|
||||
@ -79,7 +78,7 @@ const CacheBasicTests: React.FC = () => {
|
||||
const parsed = parseValue(newMemoryValue)
|
||||
setMemoryValue(parsed)
|
||||
setNewMemoryValue('')
|
||||
setUpdateCount(prev => prev + 1)
|
||||
setUpdateCount((prev) => prev + 1)
|
||||
message.success(`Memory cache updated: ${memoryCacheKey}`)
|
||||
logger.info('Memory cache updated via hook', { key: memoryCacheKey, value: parsed })
|
||||
} catch (error) {
|
||||
@ -93,7 +92,7 @@ const CacheBasicTests: React.FC = () => {
|
||||
const parsed = parseValue(newSharedValue)
|
||||
setSharedValue(parsed)
|
||||
setNewSharedValue('')
|
||||
setUpdateCount(prev => prev + 1)
|
||||
setUpdateCount((prev) => prev + 1)
|
||||
message.success(`Shared cache updated: ${sharedCacheKey} (broadcasted to other windows)`)
|
||||
logger.info('Shared cache updated via hook', { key: sharedCacheKey, value: parsed })
|
||||
} catch (error) {
|
||||
@ -118,7 +117,7 @@ const CacheBasicTests: React.FC = () => {
|
||||
|
||||
setPersistValue(parsed as any)
|
||||
setNewPersistValue('')
|
||||
setUpdateCount(prev => prev + 1)
|
||||
setUpdateCount((prev) => prev + 1)
|
||||
message.success(`Persist cache updated: ${persistCacheKey} (saved + broadcasted)`)
|
||||
logger.info('Persist cache updated via hook', { key: persistCacheKey, value: parsed })
|
||||
} catch (error) {
|
||||
@ -129,13 +128,14 @@ const CacheBasicTests: React.FC = () => {
|
||||
// Test different data types
|
||||
const handleNumberUpdate = (newValue: number) => {
|
||||
setNumberValue(newValue)
|
||||
setUpdateCount(prev => prev + 1)
|
||||
setUpdateCount((prev) => prev + 1)
|
||||
logger.info('Number cache updated', { value: newValue })
|
||||
}
|
||||
|
||||
const handleObjectUpdate = (field: string, value: any) => {
|
||||
setObjectValue(prev => ({ ...prev, [field]: value }))
|
||||
setUpdateCount(prev => prev + 1)
|
||||
const currentValue = objectValue || { name: 'test', count: 0, active: true }
|
||||
setObjectValue({ ...currentValue, [field]: value })
|
||||
setUpdateCount((prev) => prev + 1)
|
||||
logger.info('Object cache updated', { field, value })
|
||||
}
|
||||
|
||||
@ -144,12 +144,18 @@ const CacheBasicTests: React.FC = () => {
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Space>
|
||||
<Text type="secondary">React Hook Tests • Renders: {displayRenderCount || renderCountRef.current} • Updates: {updateCount}</Text>
|
||||
<Button size="small" onClick={() => {
|
||||
renderCountRef.current = 0
|
||||
setDisplayRenderCount(0)
|
||||
setUpdateCount(0)
|
||||
}}>Reset Stats</Button>
|
||||
<Text type="secondary">
|
||||
React Hook Tests • Renders: {displayRenderCount || renderCountRef.current} • Updates: {updateCount}
|
||||
</Text>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
renderCountRef.current = 0
|
||||
setDisplayRenderCount(0)
|
||||
setUpdateCount(0)
|
||||
}}>
|
||||
Reset Stats
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
@ -167,8 +173,7 @@ const CacheBasicTests: React.FC = () => {
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff',
|
||||
borderColor: isDarkTheme ? '#303030' : '#d9d9d9'
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<Input
|
||||
placeholder="Cache Key"
|
||||
@ -192,12 +197,7 @@ const CacheBasicTests: React.FC = () => {
|
||||
prefix={<Edit size={14} />}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleMemoryUpdate}
|
||||
disabled={!newMemoryValue}
|
||||
block
|
||||
>
|
||||
<Button type="primary" onClick={handleMemoryUpdate} disabled={!newMemoryValue} block>
|
||||
Update Memory Cache
|
||||
</Button>
|
||||
|
||||
@ -222,8 +222,7 @@ const CacheBasicTests: React.FC = () => {
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff',
|
||||
borderColor: isDarkTheme ? '#303030' : '#d9d9d9'
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<Input
|
||||
placeholder="Cache Key"
|
||||
@ -247,12 +246,7 @@ const CacheBasicTests: React.FC = () => {
|
||||
prefix={<Edit size={14} />}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleSharedUpdate}
|
||||
disabled={!newSharedValue}
|
||||
block
|
||||
>
|
||||
<Button type="primary" onClick={handleSharedUpdate} disabled={!newSharedValue} block>
|
||||
Update Shared Cache
|
||||
</Button>
|
||||
|
||||
@ -277,17 +271,17 @@ const CacheBasicTests: React.FC = () => {
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff',
|
||||
borderColor: isDarkTheme ? '#303030' : '#d9d9d9'
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<Select
|
||||
value={persistCacheKey}
|
||||
onChange={setPersistCacheKey}
|
||||
style={{ width: '100%' }}
|
||||
placeholder="Select persist key"
|
||||
>
|
||||
{persistKeys.map(key => (
|
||||
<Option key={key} value={key}>{key}</Option>
|
||||
placeholder="Select persist key">
|
||||
{persistKeys.map((key) => (
|
||||
<Option key={key} value={key}>
|
||||
{key}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
@ -299,12 +293,7 @@ const CacheBasicTests: React.FC = () => {
|
||||
prefix={<Edit size={14} />}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handlePersistUpdate}
|
||||
disabled={!newPersistValue}
|
||||
block
|
||||
>
|
||||
<Button type="primary" onClick={handlePersistUpdate} disabled={!newPersistValue} block>
|
||||
Update Persist Cache
|
||||
</Button>
|
||||
|
||||
@ -333,11 +322,14 @@ const CacheBasicTests: React.FC = () => {
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff',
|
||||
borderColor: isDarkTheme ? '#303030' : '#d9d9d9'
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Text>Key: <code>{numberKey}</code></Text>
|
||||
<Text>Current Value: <strong>{numberValue}</strong></Text>
|
||||
<Text>
|
||||
Key: <code>{numberKey}</code>
|
||||
</Text>
|
||||
<Text>
|
||||
Current Value: <strong>{numberValue}</strong>
|
||||
</Text>
|
||||
|
||||
<Slider
|
||||
min={0}
|
||||
@ -347,7 +339,9 @@ const CacheBasicTests: React.FC = () => {
|
||||
/>
|
||||
|
||||
<Space>
|
||||
<Button size="small" onClick={() => handleNumberUpdate(0)}>Reset to 0</Button>
|
||||
<Button size="small" onClick={() => handleNumberUpdate(0)}>
|
||||
Reset to 0
|
||||
</Button>
|
||||
<Button size="small" onClick={() => handleNumberUpdate(Math.floor(Math.random() * 100))}>
|
||||
Random
|
||||
</Button>
|
||||
@ -368,10 +362,11 @@ const CacheBasicTests: React.FC = () => {
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff',
|
||||
borderColor: isDarkTheme ? '#303030' : '#d9d9d9'
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Text>Key: <code>{objectKey}</code></Text>
|
||||
<Text>
|
||||
Key: <code>{objectKey}</code>
|
||||
</Text>
|
||||
|
||||
<Space>
|
||||
<Input
|
||||
@ -389,8 +384,7 @@ const CacheBasicTests: React.FC = () => {
|
||||
/>
|
||||
<Button
|
||||
type={objectValue?.active ? 'primary' : 'default'}
|
||||
onClick={() => handleObjectUpdate('active', !objectValue?.active)}
|
||||
>
|
||||
onClick={() => handleObjectUpdate('active', !objectValue?.active)}>
|
||||
{objectValue?.active ? 'Active' : 'Inactive'}
|
||||
</Button>
|
||||
</Space>
|
||||
@ -414,12 +408,12 @@ const CacheBasicTests: React.FC = () => {
|
||||
}
|
||||
|
||||
const TestContainer = styled.div<{ $isDark: boolean }>`
|
||||
color: ${props => props.$isDark ? '#fff' : '#000'};
|
||||
color: ${(props) => (props.$isDark ? '#fff' : '#000')};
|
||||
`
|
||||
|
||||
const ResultDisplay = styled.div<{ $isDark: boolean }>`
|
||||
background: ${props => props.$isDark ? '#0d1117' : '#f6f8fa'};
|
||||
border: 1px solid ${props => props.$isDark ? '#30363d' : '#d0d7de'};
|
||||
background: ${(props) => (props.$isDark ? '#0d1117' : '#f6f8fa')};
|
||||
border: 1px solid ${(props) => (props.$isDark ? '#30363d' : '#d0d7de')};
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
font-size: 11px;
|
||||
@ -430,9 +424,9 @@ const ResultDisplay = styled.div<{ $isDark: boolean }>`
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
color: ${props => props.$isDark ? '#e6edf3' : '#1f2328'};
|
||||
color: ${(props) => (props.$isDark ? '#e6edf3' : '#1f2328')};
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
}
|
||||
`
|
||||
|
||||
export default CacheBasicTests
|
||||
export default CacheBasicTests
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import { cacheService } from '@renderer/data/CacheService'
|
||||
import { loggerService } from '@renderer/services/LoggerService'
|
||||
import type { PersistCacheKey, PersistCacheSchema } from '@shared/data/cache/cacheSchemas'
|
||||
import { usePreference } from '@renderer/data/hooks/usePreference'
|
||||
import { loggerService } from '@renderer/services/LoggerService'
|
||||
import type { RendererPersistCacheKey, RendererPersistCacheSchema } from '@shared/data/cache/cacheSchemas'
|
||||
import { ThemeMode } from '@shared/data/preference/preferenceTypes'
|
||||
import { Button, Input, message, Select, Space, Typography, Card, Row, Col, Divider } from 'antd'
|
||||
import { Database, Clock, Trash2, Eye, Edit, Zap } from 'lucide-react'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Button, Card, Col, Divider, Input, message, Row, Select, Space, Typography } from 'antd'
|
||||
import { Clock, Database, Edit, Eye, Trash2, Zap } from 'lucide-react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const { Text, Title } = Typography
|
||||
const { Text } = Typography
|
||||
const { Option } = Select
|
||||
const { TextArea } = Input
|
||||
|
||||
@ -31,7 +31,7 @@ const CacheServiceTests: React.FC = () => {
|
||||
const [sharedValue, setSharedValue] = useState('{"type": "shared", "data": "cross-window"}')
|
||||
const [sharedTTL, setSharedTTL] = useState<string>('10000')
|
||||
|
||||
const [persistKey, setPersistKey] = useState<PersistCacheKey>('example-1')
|
||||
const [persistKey, setPersistKey] = useState<RendererPersistCacheKey>('example-1')
|
||||
const [persistValue, setPersistValue] = useState('updated-example-value')
|
||||
|
||||
// Display states
|
||||
@ -42,7 +42,7 @@ const CacheServiceTests: React.FC = () => {
|
||||
const [updateCount, setUpdateCount] = useState(0)
|
||||
|
||||
// Available persist keys from schema
|
||||
const persistKeys: PersistCacheKey[] = ['example-1', 'example-2', 'example-3', 'example-4']
|
||||
const persistKeys: RendererPersistCacheKey[] = ['example-1', 'example-2', 'example-3', 'example-4']
|
||||
|
||||
const parseValue = (value: string): any => {
|
||||
if (!value) return undefined
|
||||
@ -67,7 +67,7 @@ const CacheServiceTests: React.FC = () => {
|
||||
const ttl = memoryTTL ? parseInt(memoryTTL) : undefined
|
||||
cacheService.set(memoryKey, parsed, ttl)
|
||||
message.success(`Memory cache set: ${memoryKey}`)
|
||||
setUpdateCount(prev => prev + 1)
|
||||
setUpdateCount((prev) => prev + 1)
|
||||
logger.info('Memory cache set', { key: memoryKey, value: parsed, ttl })
|
||||
} catch (error) {
|
||||
message.error(`Memory cache set failed: ${(error as Error).message}`)
|
||||
@ -115,7 +115,7 @@ const CacheServiceTests: React.FC = () => {
|
||||
const ttl = sharedTTL ? parseInt(sharedTTL) : undefined
|
||||
cacheService.setShared(sharedKey, parsed, ttl)
|
||||
message.success(`Shared cache set: ${sharedKey} (broadcasted to other windows)`)
|
||||
setUpdateCount(prev => prev + 1)
|
||||
setUpdateCount((prev) => prev + 1)
|
||||
logger.info('Shared cache set', { key: sharedKey, value: parsed, ttl })
|
||||
} catch (error) {
|
||||
message.error(`Shared cache set failed: ${(error as Error).message}`)
|
||||
@ -171,9 +171,9 @@ const CacheServiceTests: React.FC = () => {
|
||||
parsed = parseValue(persistValue) // object
|
||||
}
|
||||
|
||||
cacheService.setPersist(persistKey, parsed as PersistCacheSchema[typeof persistKey])
|
||||
cacheService.setPersist(persistKey, parsed as RendererPersistCacheSchema[typeof persistKey])
|
||||
message.success(`Persist cache set: ${persistKey} (saved to localStorage + broadcasted)`)
|
||||
setUpdateCount(prev => prev + 1)
|
||||
setUpdateCount((prev) => prev + 1)
|
||||
logger.info('Persist cache set', { key: persistKey, value: parsed })
|
||||
} catch (error) {
|
||||
message.error(`Persist cache set failed: ${(error as Error).message}`)
|
||||
@ -227,9 +227,7 @@ const CacheServiceTests: React.FC = () => {
|
||||
<TestContainer $isDark={isDarkTheme}>
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Text type="secondary">
|
||||
直接测试 CacheService API • Updates: {updateCount} • Auto-refresh: 1s
|
||||
</Text>
|
||||
<Text type="secondary">直接测试 CacheService API • Updates: {updateCount} • Auto-refresh: 1s</Text>
|
||||
</div>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
@ -246,8 +244,7 @@ const CacheServiceTests: React.FC = () => {
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff',
|
||||
borderColor: isDarkTheme ? '#303030' : '#d9d9d9'
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<Input
|
||||
placeholder="Cache Key"
|
||||
@ -257,7 +254,7 @@ const CacheServiceTests: React.FC = () => {
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
placeholder='Value (JSON or string)'
|
||||
placeholder="Value (JSON or string)"
|
||||
value={memoryValue}
|
||||
onChange={(e) => setMemoryValue(e.target.value)}
|
||||
rows={2}
|
||||
@ -306,8 +303,7 @@ const CacheServiceTests: React.FC = () => {
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff',
|
||||
borderColor: isDarkTheme ? '#303030' : '#d9d9d9'
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<Input
|
||||
placeholder="Cache Key"
|
||||
@ -317,7 +313,7 @@ const CacheServiceTests: React.FC = () => {
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
placeholder='Value (JSON or string)'
|
||||
placeholder="Value (JSON or string)"
|
||||
value={sharedValue}
|
||||
onChange={(e) => setSharedValue(e.target.value)}
|
||||
rows={2}
|
||||
@ -366,22 +362,22 @@ const CacheServiceTests: React.FC = () => {
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff',
|
||||
borderColor: isDarkTheme ? '#303030' : '#d9d9d9'
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<Select
|
||||
value={persistKey}
|
||||
onChange={setPersistKey}
|
||||
style={{ width: '100%' }}
|
||||
placeholder="Select persist key"
|
||||
>
|
||||
{persistKeys.map(key => (
|
||||
<Option key={key} value={key}>{key}</Option>
|
||||
placeholder="Select persist key">
|
||||
{persistKeys.map((key) => (
|
||||
<Option key={key} value={key}>
|
||||
{key}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<TextArea
|
||||
placeholder='Value (type depends on key)'
|
||||
placeholder="Value (type depends on key)"
|
||||
value={persistValue}
|
||||
onChange={(e) => setPersistValue(e.target.value)}
|
||||
rows={2}
|
||||
@ -421,12 +417,12 @@ const CacheServiceTests: React.FC = () => {
|
||||
}
|
||||
|
||||
const TestContainer = styled.div<{ $isDark: boolean }>`
|
||||
color: ${props => props.$isDark ? '#fff' : '#000'};
|
||||
color: ${(props) => (props.$isDark ? '#fff' : '#000')};
|
||||
`
|
||||
|
||||
const ResultDisplay = styled.div<{ $isDark: boolean }>`
|
||||
background: ${props => props.$isDark ? '#0d1117' : '#f6f8fa'};
|
||||
border: 1px solid ${props => props.$isDark ? '#30363d' : '#d0d7de'};
|
||||
background: ${(props) => (props.$isDark ? '#0d1117' : '#f6f8fa')};
|
||||
border: 1px solid ${(props) => (props.$isDark ? '#30363d' : '#d0d7de')};
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
font-size: 11px;
|
||||
@ -435,9 +431,9 @@ const ResultDisplay = styled.div<{ $isDark: boolean }>`
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
color: ${props => props.$isDark ? '#e6edf3' : '#1f2328'};
|
||||
color: ${(props) => (props.$isDark ? '#e6edf3' : '#1f2328')};
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
}
|
||||
`
|
||||
|
||||
export default CacheServiceTests
|
||||
export default CacheServiceTests
|
||||
|
||||
@ -3,12 +3,12 @@ import { useCache, useSharedCache } from '@renderer/data/hooks/useCache'
|
||||
import { usePreference } from '@renderer/data/hooks/usePreference'
|
||||
import { loggerService } from '@renderer/services/LoggerService'
|
||||
import { ThemeMode } from '@shared/data/preference/preferenceTypes'
|
||||
import { Button, Input, message, Space, Typography, Card, Row, Col, Progress, Statistic, Tag, Alert } from 'antd'
|
||||
import { Zap, AlertTriangle, TrendingUp, HardDrive, Users, Clock, Database } from 'lucide-react'
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { Alert, Button, Card, Col, message, Progress, Row, Space, Statistic, Tag, Typography } from 'antd'
|
||||
import { AlertTriangle, Database, HardDrive, TrendingUp, Users, Zap } from 'lucide-react'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const { Text, Title } = Typography
|
||||
const { Text } = Typography
|
||||
|
||||
const logger = loggerService.withContext('CacheStressTests')
|
||||
|
||||
@ -39,14 +39,14 @@ const CacheStressTests: React.FC = () => {
|
||||
const [concurrentShared, setConcurrentShared] = useSharedCache('concurrent-shared', 0)
|
||||
|
||||
// Large Data Testing
|
||||
const [largeDataKey] = useState('large-data-test')
|
||||
const [largeDataKey] = useState('large-data-test' as const)
|
||||
const [largeDataSize, setLargeDataSize] = useState(0)
|
||||
const [largeDataValue, setLargeDataValue] = useCache(largeDataKey)
|
||||
const [, setLargeDataValue] = useCache(largeDataKey as any, {})
|
||||
|
||||
// Timers and refs
|
||||
const testTimerRef = useRef<NodeJS.Timeout>()
|
||||
const metricsTimerRef = useRef<NodeJS.Timeout>()
|
||||
const concurrentTimerRef = useRef<NodeJS.Timeout>()
|
||||
const testTimerRef = useRef<NodeJS.Timeout>(null)
|
||||
const metricsTimerRef = useRef<NodeJS.Timeout>(null)
|
||||
const concurrentTimerRef = useRef<NodeJS.Timeout>(null)
|
||||
|
||||
// Update render count without causing re-renders
|
||||
renderCountRef.current += 1
|
||||
@ -56,7 +56,7 @@ const CacheStressTests: React.FC = () => {
|
||||
try {
|
||||
// Rough estimation based on localStorage size and objects
|
||||
const persistSize = localStorage.getItem('cs_cache_persist')?.length || 0
|
||||
const estimatedSize = persistSize + (totalOperations * 50) // Rough estimate
|
||||
const estimatedSize = persistSize + totalOperations * 50 // Rough estimate
|
||||
setMemoryUsage(estimatedSize)
|
||||
} catch (error) {
|
||||
logger.error('Memory usage estimation failed', error as Error)
|
||||
@ -206,7 +206,8 @@ const CacheStressTests: React.FC = () => {
|
||||
let testSize = 1
|
||||
let maxSize = 0
|
||||
|
||||
while (testSize <= 10240) { // Test up to 10MB
|
||||
while (testSize <= 10240) {
|
||||
// Test up to 10MB
|
||||
try {
|
||||
const testData = 'x'.repeat(testSize * 1024) // testSize KB
|
||||
localStorage.setItem('storage-limit-test', testData)
|
||||
@ -230,20 +231,21 @@ const CacheStressTests: React.FC = () => {
|
||||
setIsRunning(false)
|
||||
if (testTimerRef.current) {
|
||||
clearTimeout(testTimerRef.current)
|
||||
testTimerRef.current = undefined
|
||||
testTimerRef.current = null
|
||||
}
|
||||
if (concurrentTimerRef.current) {
|
||||
clearInterval(concurrentTimerRef.current)
|
||||
concurrentTimerRef.current = undefined
|
||||
concurrentTimerRef.current = null
|
||||
}
|
||||
message.info('All tests stopped')
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
useEffect(() => {
|
||||
const metricsCleanup = metricsTimerRef.current
|
||||
return () => {
|
||||
if (testTimerRef.current) clearTimeout(testTimerRef.current)
|
||||
if (metricsTimerRef.current) clearInterval(metricsTimerRef.current)
|
||||
if (metricsCleanup) clearInterval(metricsCleanup)
|
||||
if (concurrentTimerRef.current) clearInterval(concurrentTimerRef.current)
|
||||
}
|
||||
}, [])
|
||||
@ -253,37 +255,31 @@ const CacheStressTests: React.FC = () => {
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Space>
|
||||
<Text type="secondary">Stress Testing • Renders: {displayRenderCount || renderCountRef.current} • Errors: {errorCount}</Text>
|
||||
<Button size="small" onClick={() => {
|
||||
renderCountRef.current = 0
|
||||
setDisplayRenderCount(0)
|
||||
setErrorCount(0)
|
||||
}}>Reset Stats</Button>
|
||||
<Text type="secondary">
|
||||
Stress Testing • Renders: {displayRenderCount || renderCountRef.current} • Errors: {errorCount}
|
||||
</Text>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
renderCountRef.current = 0
|
||||
setDisplayRenderCount(0)
|
||||
setErrorCount(0)
|
||||
}}>
|
||||
Reset Stats
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{/* Performance Metrics */}
|
||||
<Row gutter={[16, 8]}>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="Operations/Second"
|
||||
value={operationsPerSecond}
|
||||
prefix={<TrendingUp size={16} />}
|
||||
/>
|
||||
<Statistic title="Operations/Second" value={operationsPerSecond} prefix={<TrendingUp size={16} />} />
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="Total Operations"
|
||||
value={totalOperations}
|
||||
prefix={<Database size={16} />}
|
||||
/>
|
||||
<Statistic title="Total Operations" value={totalOperations} prefix={<Database size={16} />} />
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="Memory Usage (bytes)"
|
||||
value={memoryUsage}
|
||||
prefix={<HardDrive size={16} />}
|
||||
/>
|
||||
<Statistic title="Memory Usage (bytes)" value={memoryUsage} prefix={<HardDrive size={16} />} />
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
@ -309,8 +305,7 @@ const CacheStressTests: React.FC = () => {
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff',
|
||||
borderColor: isDarkTheme ? '#303030' : '#d9d9d9'
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Text>High-frequency cache operations test (1000 ops in 10s)</Text>
|
||||
|
||||
@ -321,12 +316,7 @@ const CacheStressTests: React.FC = () => {
|
||||
/>
|
||||
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={runRapidFireTest}
|
||||
disabled={isRunning}
|
||||
icon={<Zap size={12} />}
|
||||
>
|
||||
<Button type="primary" onClick={runRapidFireTest} disabled={isRunning} icon={<Zap size={12} />}>
|
||||
Start Rapid Fire Test
|
||||
</Button>
|
||||
<Button onClick={stopAllTests} disabled={!isRunning} danger>
|
||||
@ -357,8 +347,7 @@ const CacheStressTests: React.FC = () => {
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff',
|
||||
borderColor: isDarkTheme ? '#303030' : '#d9d9d9'
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Text>Multiple hooks updating simultaneously</Text>
|
||||
|
||||
@ -381,16 +370,10 @@ const CacheStressTests: React.FC = () => {
|
||||
</Row>
|
||||
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={startConcurrentTest}
|
||||
icon={<Users size={12} />}
|
||||
>
|
||||
<Button type="primary" onClick={startConcurrentTest} icon={<Users size={12} />}>
|
||||
Start Concurrent Test
|
||||
</Button>
|
||||
<Button onClick={stopConcurrentTest}>
|
||||
Stop
|
||||
</Button>
|
||||
<Button onClick={stopConcurrentTest}>Stop</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
</Card>
|
||||
@ -411,8 +394,7 @@ const CacheStressTests: React.FC = () => {
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff',
|
||||
borderColor: isDarkTheme ? '#303030' : '#d9d9d9'
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Text>Test cache with large objects</Text>
|
||||
|
||||
@ -438,9 +420,11 @@ const CacheStressTests: React.FC = () => {
|
||||
)}
|
||||
|
||||
<ResultDisplay $isDark={isDarkTheme}>
|
||||
<Text strong>Large Data Key: </Text><code>{largeDataKey}</code>
|
||||
<Text strong>Large Data Key: </Text>
|
||||
<code>{largeDataKey}</code>
|
||||
<br />
|
||||
<Text strong>Current Size: </Text>{largeDataSize}KB
|
||||
<Text strong>Current Size: </Text>
|
||||
{largeDataSize}KB
|
||||
</ResultDisplay>
|
||||
</Space>
|
||||
</Card>
|
||||
@ -459,8 +443,7 @@ const CacheStressTests: React.FC = () => {
|
||||
style={{
|
||||
backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff',
|
||||
borderColor: isDarkTheme ? '#303030' : '#d9d9d9'
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Text>Test localStorage capacity and limits</Text>
|
||||
|
||||
@ -471,17 +454,15 @@ const CacheStressTests: React.FC = () => {
|
||||
showIcon
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={testLocalStorageLimit}
|
||||
icon={<Database size={12} />}
|
||||
>
|
||||
<Button onClick={testLocalStorageLimit} icon={<Database size={12} />}>
|
||||
Test Storage Limits
|
||||
</Button>
|
||||
|
||||
<Space direction="vertical">
|
||||
<Tag color="blue">Persist Cache Size Check</Tag>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
Current persist cache: ~{Math.round(JSON.stringify(localStorage.getItem('cs_cache_persist')).length / 1024)}KB
|
||||
Current persist cache: ~
|
||||
{Math.round(JSON.stringify(localStorage.getItem('cs_cache_persist')).length / 1024)}KB
|
||||
</Text>
|
||||
</Space>
|
||||
</Space>
|
||||
@ -500,12 +481,12 @@ const CacheStressTests: React.FC = () => {
|
||||
}
|
||||
|
||||
const TestContainer = styled.div<{ $isDark: boolean }>`
|
||||
color: ${props => props.$isDark ? '#fff' : '#000'};
|
||||
color: ${(props) => (props.$isDark ? '#fff' : '#000')};
|
||||
`
|
||||
|
||||
const ResultDisplay = styled.div<{ $isDark: boolean }>`
|
||||
background: ${props => props.$isDark ? '#0d1117' : '#f6f8fa'};
|
||||
border: 1px solid ${props => props.$isDark ? '#30363d' : '#d0d7de'};
|
||||
background: ${(props) => (props.$isDark ? '#0d1117' : '#f6f8fa')};
|
||||
border: 1px solid ${(props) => (props.$isDark ? '#30363d' : '#d0d7de')};
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
font-size: 11px;
|
||||
@ -516,9 +497,9 @@ const ResultDisplay = styled.div<{ $isDark: boolean }>`
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
color: ${props => props.$isDark ? '#e6edf3' : '#1f2328'};
|
||||
color: ${(props) => (props.$isDark ? '#e6edf3' : '#1f2328')};
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
}
|
||||
`
|
||||
|
||||
export default CacheStressTests
|
||||
export default CacheStressTests
|
||||
|
||||
Loading…
Reference in New Issue
Block a user