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:
fullex 2025-09-16 00:40:36 +08:00
parent 6079961f44
commit c242abd81a
49 changed files with 1000 additions and 807 deletions

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = [
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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]
)
// 控制翻译按钮是否可用

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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)' }}>
PreferenceServiceCacheServiceDataApiService React hooks
PreferenceServiceCacheServiceDataApiService
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')};
}
`

View File

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

View File

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

View File

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

View File

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