feat: CacheService & useCache Hooks

This commit is contained in:
fullex 2025-09-15 14:12:41 +08:00
parent 2e07b4ea58
commit 6d89f94335
65 changed files with 1079 additions and 90 deletions

View File

@ -300,7 +300,7 @@ export enum IpcChannel {
Memory_DeleteAllMemoriesForUser = 'memory:delete-all-memories-for-user',
Memory_GetUsersList = 'memory:get-users-list',
// Preference
// Data: Preference
Preference_Get = 'preference:get',
Preference_Set = 'preference:set',
Preference_GetMultiple = 'preference:get-multiple',
@ -309,9 +309,12 @@ export enum IpcChannel {
Preference_Subscribe = 'preference:subscribe',
Preference_Changed = 'preference:changed',
// Data API channels
// Data: Cache
Cache_Sync = 'cache:sync',
Cache_SyncBatch = 'cache:sync-batch',
// Data: API Channels
DataApi_Request = 'data-api:request',
DataApi_Response = 'data-api:response',
DataApi_Batch = 'data-api:batch',
DataApi_Transaction = 'data-api:transaction',
DataApi_Subscribe = 'data-api:subscribe',

View File

@ -12,7 +12,7 @@ This directory contains shared data structures and API type definitions for the
### API Types (`api/` subdirectory)
- **`api/index.ts`** - Barrel export file providing clean imports for all API types
- **`api/apiTypes.ts`** - Core request/response types and API infrastructure
- **`api/apiTypes.ts`** - Core request/response types and API infrastructure
- **`api/apiModels.ts`** - Business entity types and Data Transfer Objects (DTOs)
- **`api/apiSchemas.ts`** - Complete API endpoint definitions with type mappings
- **`api/errorCodes.ts`** - Error handling utilities and standardized error codes
@ -29,6 +29,7 @@ These files are part of the **Renderer-Main Virtual Data Acquisition Architectur
## 🔄 Classification Status
**Important**: These files are **NOT classified** in the data refactor system because they are:
- ✅ **Type definitions** - Not actual data storage
- ✅ **Compile-time artifacts** - Exist only during TypeScript compilation
- ✅ **Framework infrastructure** - Enable the data API architecture
@ -40,13 +41,7 @@ These files are part of the **Renderer-Main Virtual Data Acquisition Architectur
```typescript
// Import API types from the api subdirectory
import {
Topic,
CreateTopicDto,
DataRequest,
ApiSchemas,
ErrorCode
} from '@shared/data/api'
import { Topic, CreateTopicDto, DataRequest, ApiSchemas, ErrorCode } from '@shared/data/api'
// Import specific groups
import type { TopicTypes, MessageTypes } from '@shared/data/api'
@ -64,7 +59,7 @@ import type { ApiSchemas, ApiResponse } from '@shared/data/api'
type TopicsListResponse = ApiResponse<'/topics', 'GET'>
// Result: PaginatedResponse<Topic>
type CreateTopicResponse = ApiResponse<'/topics', 'POST'>
type CreateTopicResponse = ApiResponse<'/topics', 'POST'>
// Result: Topic
```
@ -169,18 +164,22 @@ export interface ApiSchemas {
## 🔗 Related Files
### Main Process Implementation
- `src/main/data/DataApiService.ts` - Main process data service
- `src/main/data/api/` - Controllers, services, and routing
### Renderer Process Implementation
### Renderer Process Implementation
- `src/renderer/src/data/DataApiService.ts` - Renderer API client
- `src/renderer/src/data/hooks/` - React hooks for data fetching
### Shared Data Types
- `packages/shared/data/api/` - API contract definitions
- `packages/shared/data/preferences.ts` - User preference schemas
### Architecture Documentation
- `.claude/data-request-arch.md` - Complete architecture documentation
- `CLAUDE.md` - Project development guidelines
@ -195,4 +194,4 @@ The type system is designed to support:
---
*This README is part of the Cherry Studio data refactor project. For more information, see the project documentation in `.claude/` directory.*
_This README is part of the Cherry Studio data refactor project. For more information, see the project documentation in `.claude/` directory._

View File

@ -0,0 +1,22 @@
/**
* Persist cache schema defining allowed keys and their value types
* This ensures type safety and prevents key conflicts
*/
export interface PersistCacheSchema {
'example-1': string
'example-2': number
'example-3': boolean
'example-4': { a: string; b: number; c: boolean }
}
export const DefaultPersistCache: PersistCacheSchema = {
'example-1': 'example-1',
'example-2': 1,
'example-3': true,
'example-4': { a: 'example-4', b: 4, c: false }
}
/**
* Type-safe persist cache key
*/
export type PersistCacheKey = keyof PersistCacheSchema

View File

@ -0,0 +1,43 @@
/**
* Cache types and interfaces for CacheService
*
* Supports three-layer caching architecture:
* 1. Memory cache (cross-component within renderer)
* 2. Shared cache (cross-window via IPC)
* 3. Persist cache (cross-window with localStorage persistence)
*/
/**
* Cache entry with optional TTL support
*/
export interface CacheEntry<T = any> {
value: T
expireAt?: number // Unix timestamp
}
/**
* Cache synchronization message for IPC communication
*/
export interface CacheSyncMessage {
type: 'shared' | 'persist'
key: string
value: any
ttl?: number
}
/**
* Batch cache synchronization message
*/
export interface CacheSyncBatchMessage {
type: 'shared' | 'persist'
entries: Array<{
key: string
value: any
ttl?: number
}>
}
/**
* Cache subscription callback
*/
export type CacheSubscriber = () => void

View File

@ -10,14 +10,14 @@
*/
import { TRANSLATE_PROMPT } from '@shared/config/prompts'
import * as PreferenceTypes from '@shared/data/preferenceTypes'
import * as PreferenceTypes from '@shared/data/preference/preferenceTypes'
/* eslint @typescript-eslint/member-ordering: ["error", {
"interfaces": { "order": "alphabetically" },
"typeLiterals": { "order": "alphabetically" }
}] */
export interface PreferencesType {
export interface PreferenceSchemas {
default: {
// redux/settings/enableDeveloperMode
'app.developer_mode.enabled': boolean
@ -413,7 +413,7 @@ export interface PreferencesType {
}
/* eslint sort-keys: ["error", "asc", {"caseSensitive": true, "natural": false}] */
export const DefaultPreferences: PreferencesType = {
export const DefaultPreferences: PreferenceSchemas = {
default: {
'app.developer_mode.enabled': false,
'app.disable_hardware_acceleration': false,

View File

@ -1,6 +1,6 @@
import { PreferencesType } from './preferences'
import { PreferenceSchemas } from './preferenceSchemas'
export type PreferenceDefaultScopeType = PreferencesType['default']
export type PreferenceDefaultScopeType = PreferenceSchemas['default']
export type PreferenceKeyType = keyof PreferenceDefaultScopeType
export type PreferenceUpdateOptions = {

View File

@ -0,0 +1,170 @@
import { loggerService } from '@logger'
import type { CacheEntry, CacheSyncMessage } from '@shared/data/cache/cacheTypes'
import { IpcChannel } from '@shared/IpcChannel'
import { BrowserWindow, ipcMain } from 'electron'
const logger = loggerService.withContext('CacheService')
/**
* Main process cache service
*
* Features:
* - Main process internal cache with TTL support
* - IPC handlers for cross-window cache synchronization
* - Broadcast mechanism for shared cache sync
* - Minimal storage (persist cache interface reserved for future)
*
* Responsibilities:
* 1. Provide cache for Main process services
* 2. Relay cache sync messages between renderer windows
* 3. Reserve persist cache interface (not implemented yet)
*/
export class CacheService {
private static instance: CacheService
// Main process cache
private cache = new Map<string, CacheEntry>()
private constructor() {
this.setupIpcHandlers()
logger.debug('CacheService initialized')
}
/**
* Get singleton instance
*/
public static getInstance(): CacheService {
if (!CacheService.instance) {
CacheService.instance = new CacheService()
}
return CacheService.instance
}
// ============ Main Process Cache (Internal) ============
/**
* Get value from main process cache
*/
get<T>(key: string): T | undefined {
const entry = this.cache.get(key)
if (!entry) return undefined
// Check TTL (lazy cleanup)
if (entry.expireAt && Date.now() > entry.expireAt) {
this.cache.delete(key)
return undefined
}
return entry.value as T
}
/**
* Set value in main process cache
*/
set<T>(key: string, value: T, ttl?: number): void {
const entry: CacheEntry<T> = {
value,
expireAt: ttl ? Date.now() + ttl : undefined
}
this.cache.set(key, entry)
}
/**
* Check if key exists in main process cache
*/
has(key: string): boolean {
const entry = this.cache.get(key)
if (!entry) return false
// Check TTL
if (entry.expireAt && Date.now() > entry.expireAt) {
this.cache.delete(key)
return false
}
return true
}
/**
* Delete from main process cache
*/
delete(key: string): boolean {
return this.cache.delete(key)
}
// ============ Persist Cache Interface (Reserved) ============
/**
* Get persist cache value (interface reserved for future)
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getPersist<T>(_key: string): T | undefined {
// TODO: Implement persist cache in future
logger.warn('getPersist not implemented yet')
return undefined
}
/**
* Set persist cache value (interface reserved for future)
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
setPersist<T>(_key: string, _value: T): void {
// TODO: Implement persist cache in future
logger.warn('setPersist not implemented yet')
}
/**
* Check persist cache key (interface reserved for future)
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
hasPersist(_key: string): boolean {
// TODO: Implement persist cache in future
return false
}
// ============ IPC Handlers for Cache Synchronization ============
/**
* Broadcast sync message to all renderer windows
*/
private broadcastSync(message: CacheSyncMessage, senderWindowId?: number): void {
const windows = BrowserWindow.getAllWindows()
for (const window of windows) {
if (!window.isDestroyed() && window.id !== senderWindowId) {
window.webContents.send(IpcChannel.Cache_Sync, message)
}
}
}
/**
* Setup IPC handlers for cache synchronization
*/
private setupIpcHandlers(): void {
// Handle cache sync broadcast from renderer
ipcMain.on(IpcChannel.Cache_Sync, (event, message: CacheSyncMessage) => {
const senderWindowId = BrowserWindow.fromWebContents(event.sender)?.id
this.broadcastSync(message, senderWindowId)
logger.verbose(`Broadcasted cache sync: ${message.type}:${message.key}`)
})
logger.debug('Cache sync IPC handlers registered')
}
/**
* Cleanup resources
*/
public cleanup(): void {
// Clear cache
this.cache.clear()
// Remove IPC handlers
ipcMain.removeAllListeners(IpcChannel.Cache_Sync)
logger.debug('CacheService cleanup completed')
}
}
// Export singleton instance for main process use
export const cacheService = CacheService.getInstance()
export default cacheService

View File

@ -1,7 +1,7 @@
import { dbService } from '@data/db/DbService'
import { loggerService } from '@logger'
import { DefaultPreferences } from '@shared/data/preferences'
import type { PreferenceDefaultScopeType, PreferenceKeyType } from '@shared/data/preferenceTypes'
import { DefaultPreferences } from '@shared/data/preference/preferenceSchemas'
import type { PreferenceDefaultScopeType, PreferenceKeyType } from '@shared/data/preference/preferenceTypes'
import { IpcChannel } from '@shared/IpcChannel'
import { and, eq } from 'drizzle-orm'
import { BrowserWindow, ipcMain } from 'electron'

View File

@ -1,5 +1,5 @@
import { preferenceTable } from '@data/db/schemas/preference'
import { DefaultPreferences } from '@shared/data/preferences'
import { DefaultPreferences } from '@shared/data/preference/preferenceSchemas'
import type { DbType, ISeed } from '../types'

View File

@ -1,7 +1,7 @@
import { dbService } from '@data/db/DbService'
import { preferenceTable } from '@data/db/schemas/preference'
import { loggerService } from '@logger'
import { DefaultPreferences } from '@shared/data/preferences'
import { DefaultPreferences } from '@shared/data/preference/preferenceSchemas'
import { and, eq } from 'drizzle-orm'
import { configManager } from '../../../../services/ConfigManager'

View File

@ -12,7 +12,7 @@ import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/pro
import { handleZoomFactor } from '@main/utils/zoom'
import { SpanEntity, TokenUsage } from '@mcp-trace/trace-core'
import { MIN_WINDOW_HEIGHT, MIN_WINDOW_WIDTH } from '@shared/config/constant'
import { UpgradeChannel } from '@shared/data/preferenceTypes'
import { UpgradeChannel } from '@shared/data/preference/preferenceTypes'
import { IpcChannel } from '@shared/IpcChannel'
import { FileMetadata, Provider, Shortcut } from '@types'
import checkDiskSpace from 'check-disk-space'

View File

@ -5,7 +5,7 @@ import { getIpCountry } from '@main/utils/ipService'
import { getI18n } from '@main/utils/language'
import { generateUserAgent } from '@main/utils/systemInfo'
import { FeedUrl } from '@shared/config/constant'
import { UpgradeChannel } from '@shared/data/preferenceTypes'
import { UpgradeChannel } from '@shared/data/preference/preferenceTypes'
import { IpcChannel } from '@shared/IpcChannel'
import { CancellationToken, UpdateInfo } from 'builder-util-runtime'
import { app, BrowserWindow, dialog, net } from 'electron'

View File

@ -2,8 +2,8 @@ import { preferenceService } from '@data/PreferenceService'
import { loggerService } from '@logger'
import { SELECTION_FINETUNED_LIST, SELECTION_PREDEFINED_BLACKLIST } from '@main/configs/SelectionConfig'
import { isDev, isMac, isWin } from '@main/constant'
import type { SelectionActionItem } from '@shared/data/preferenceTypes'
import { SelectionTriggerMode } from '@shared/data/preferenceTypes'
import type { SelectionActionItem } from '@shared/data/preference/preferenceTypes'
import { SelectionTriggerMode } from '@shared/data/preference/preferenceTypes'
import { IpcChannel } from '@shared/IpcChannel'
import { app, BrowserWindow, ipcMain, screen, systemPreferences } from 'electron'
import { join } from 'path'

View File

@ -1,5 +1,5 @@
import { preferenceService } from '@data/PreferenceService'
import { ThemeMode } from '@shared/data/preferenceTypes'
import { ThemeMode } from '@shared/data/preference/preferenceTypes'
import { IpcChannel } from '@shared/IpcChannel'
import { BrowserWindow, nativeTheme } from 'electron'

View File

@ -1,6 +1,6 @@
import { preferenceService } from '@data/PreferenceService'
import { defaultLanguage } from '@shared/config/constant'
import { LanguageVarious } from '@shared/data/preferenceTypes'
import { LanguageVarious } from '@shared/data/preference/preferenceTypes'
import { app } from 'electron'
import EnUs from '../../renderer/src/i18n/locales/en-us.json'

View File

@ -3,8 +3,13 @@ import { SpanEntity, TokenUsage } from '@mcp-trace/trace-core'
import { SpanContext } from '@opentelemetry/api'
import type { LogLevel, LogSourceWithContext } from '@shared/config/logger'
import type { FileChangeEvent } from '@shared/config/types'
import type { PreferenceDefaultScopeType, PreferenceKeyType, SelectionActionItem } from '@shared/data/preferenceTypes'
import { UpgradeChannel } from '@shared/data/preferenceTypes'
import type { CacheSyncMessage } from '@shared/data/cache/cacheTypes'
import type {
PreferenceDefaultScopeType,
PreferenceKeyType,
SelectionActionItem
} from '@shared/data/preference/preferenceTypes'
import { UpgradeChannel } from '@shared/data/preference/preferenceTypes'
import { IpcChannel } from '@shared/IpcChannel'
import {
AddMemoryOptions,
@ -455,6 +460,19 @@ const api = {
}
}
},
// CacheService related APIs
cache: {
// Broadcast sync message to other windows
broadcastSync: (message: CacheSyncMessage): void => ipcRenderer.send(IpcChannel.Cache_Sync, message),
// Listen for sync messages from other windows
onSync: (callback: (message: CacheSyncMessage) => void) => {
const listener = (_: any, message: CacheSyncMessage) => callback(message)
ipcRenderer.on(IpcChannel.Cache_Sync, listener)
return () => ipcRenderer.off(IpcChannel.Cache_Sync, listener)
}
},
// PreferenceService related APIs
// DO NOT MODIFY THIS SECTION
preference: {

View File

@ -45,8 +45,8 @@ import { isJSON, parseJSON } from '@renderer/utils'
import { addAbortController, removeAbortController } from '@renderer/utils/abortController'
import { findFileBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find'
import { defaultTimeout } from '@shared/config/constant'
import { defaultAppHeaders } from '@shared/utils'
import { REFERENCE_PROMPT } from '@shared/config/prompts'
import { defaultAppHeaders } from '@shared/utils'
import { isEmpty } from 'lodash'
import { CompletionsContext } from '../middleware/types'

View File

@ -2,7 +2,7 @@ import { CodeOutlined } from '@ant-design/icons'
import { loggerService } from '@logger'
import { useTheme } from '@renderer/context/ThemeProvider'
import { extractHtmlTitle, getFileNameFromHtmlTitle } from '@renderer/utils/formats'
import { ThemeMode } from '@shared/data/preferenceTypes'
import { ThemeMode } from '@shared/data/preference/preferenceTypes'
import { Button } from 'antd'
import { Code, DownloadIcon, Globe, LinkIcon, Sparkles } from 'lucide-react'
import { FC, useState } from 'react'

View File

@ -13,7 +13,7 @@ import { useAppDispatch, useAppSelector } from '@renderer/store'
import type { Tab } from '@renderer/store/tabs'
import { addTab, removeTab, setActiveTab, setTabs } from '@renderer/store/tabs'
import { classNames } from '@renderer/utils'
import { ThemeMode } from '@shared/data/preferenceTypes'
import { ThemeMode } from '@shared/data/preference/preferenceTypes'
import { Tooltip } from 'antd'
import {
FileSearch,

View File

@ -12,7 +12,7 @@ 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'
import { ThemeMode } from '@shared/data/preferenceTypes'
import { ThemeMode } from '@shared/data/preference/preferenceTypes'
import { Avatar, Tooltip } from 'antd'
import {
Code,

View File

@ -1,4 +1,4 @@
import { SidebarIcon } from '@shared/data/preferenceTypes'
import { SidebarIcon } from '@shared/data/preference/preferenceTypes'
//TODO 这个文件是否还有存在的价值? fullex @ data refactor

View File

@ -1,6 +1,6 @@
import { usePreference } from '@data/hooks/usePreference'
import { defaultLanguage } from '@shared/config/constant'
import { LanguageVarious } from '@shared/data/preferenceTypes'
import { LanguageVarious } from '@shared/data/preference/preferenceTypes'
import { ConfigProvider, theme } from 'antd'
import elGR from 'antd/locale/el_GR'
import enUS from 'antd/locale/en_US'

View File

@ -3,7 +3,7 @@ import { useTheme } from '@renderer/context/ThemeProvider'
import { useMermaid } from '@renderer/hooks/useMermaid'
import { HighlightChunkResult, ShikiPreProperties, shikiStreamService } from '@renderer/services/ShikiStreamService'
import { getHighlighter, getMarkdownIt, getShiki, loadLanguageIfNeeded, loadThemeIfNeeded } from '@renderer/utils/shiki'
import { ThemeMode } from '@shared/data/preferenceTypes'
import { ThemeMode } from '@shared/data/preference/preferenceTypes'
import * as cmThemes from '@uiw/codemirror-themes-all'
import type React from 'react'
import { createContext, type PropsWithChildren, use, useCallback, useEffect, useMemo, useState } from 'react'

View File

@ -2,7 +2,7 @@ import { usePreference } from '@data/hooks/usePreference'
import { isMac, isWin } from '@renderer/config/constant'
import { useNavbarPosition } from '@renderer/hooks/useNavbar'
import useUserTheme from '@renderer/hooks/useUserTheme'
import { ThemeMode } from '@shared/data/preferenceTypes'
import { ThemeMode } from '@shared/data/preference/preferenceTypes'
import { IpcChannel } from '@shared/IpcChannel'
import React, { createContext, PropsWithChildren, use, useEffect, useState } from 'react'
interface ThemeContextType {

View File

@ -0,0 +1,584 @@
import { loggerService } from '@logger'
import type { PersistCacheKey, PersistCacheSchema } from '@shared/data/cache/cacheSchemas'
import { DefaultPersistCache } from '@shared/data/cache/cacheSchemas'
import type { CacheEntry, CacheSubscriber, CacheSyncMessage } from '@shared/data/cache/cacheTypes'
const STORAGE_PERSIST_KEY = 'cs_cache_persist'
const logger = loggerService.withContext('CacheService')
/**
* Renderer process cache service
*
* Three-layer caching architecture:
* 1. Memory cache (cross-component within renderer)
* 2. Shared cache (cross-window via IPC)
* 3. Persist cache (cross-window with localStorage persistence)
*
* Features:
* - All APIs are synchronous (including shared cache via local copy)
* - TTL lazy cleanup (check on get, not timer-based)
* - Hook reference tracking (prevent deletion of active hooks)
* - Unified sync mechanism for shared and persist
* - Type-safe persist cache with predefined schema
*/
export class CacheService {
private static instance: 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
// Hook reference tracking
private activeHooks = new Set<string>()
// Subscription management
private subscribers = new Map<string, Set<CacheSubscriber>>()
// Persist cache debounce
private persistSaveTimer?: NodeJS.Timeout
private persistDirty = false
private constructor() {
this.initialize()
}
/**
* Get singleton instance
*/
public static getInstance(): CacheService {
if (!CacheService.instance) {
CacheService.instance = new CacheService()
}
return CacheService.instance
}
public initialize(): void {
this.loadPersistCache()
this.setupIpcListeners()
this.setupWindowUnloadHandler()
logger.debug('CacheService initialized')
}
// ============ Memory Cache (Cross-component) ============
/**
* Get value from memory cache
*/
get<T>(key: string): T | undefined {
const entry = this.memoryCache.get(key)
if (!entry) return undefined
// Check TTL (lazy cleanup)
if (entry.expireAt && Date.now() > entry.expireAt) {
this.memoryCache.delete(key)
this.notifySubscribers(key)
return undefined
}
return entry.value as T
}
/**
* Set value in memory cache
*/
set<T>(key: string, value: T, ttl?: number): void {
const existingEntry = this.memoryCache.get(key)
// Value comparison optimization
if (existingEntry && Object.is(existingEntry.value, value)) {
// Value is same, only update TTL if needed
const newExpireAt = ttl ? Date.now() + ttl : undefined
if (!Object.is(existingEntry.expireAt, newExpireAt)) {
existingEntry.expireAt = newExpireAt
logger.verbose(`Updated TTL for memory cache key "${key}"`)
} else {
logger.verbose(`Skipped memory cache update for key "${key}" - value and TTL unchanged`)
}
return // Skip notification
}
const entry: CacheEntry<T> = {
value,
expireAt: ttl ? Date.now() + ttl : undefined
}
this.memoryCache.set(key, entry)
this.notifySubscribers(key)
logger.verbose(`Updated memory cache for key "${key}"`)
}
/**
* Check if key exists in memory cache
*/
has(key: string): boolean {
const entry = this.memoryCache.get(key)
if (!entry) return false
// Check TTL
if (entry.expireAt && Date.now() > entry.expireAt) {
this.memoryCache.delete(key)
this.notifySubscribers(key)
return false
}
return true
}
/**
* Delete from memory cache
*/
delete(key: string): boolean {
// Check if key is being used by hooks
if (this.activeHooks.has(key)) {
logger.error(`Cannot delete key "${key}" as it's being used by useCache hook`)
return false
}
// Check if key exists before attempting deletion
if (!this.memoryCache.has(key)) {
logger.verbose(`Skipped memory cache delete for key "${key}" - not exists`)
return true
}
this.memoryCache.delete(key)
this.notifySubscribers(key)
logger.verbose(`Deleted memory cache key "${key}"`)
return true
}
/**
* Check if a key has TTL set (for warning purposes)
*/
hasTTL(key: string): boolean {
const entry = this.memoryCache.get(key)
return entry?.expireAt !== undefined
}
/**
* Check if a shared cache key has TTL set (for warning purposes)
*/
hasSharedTTL(key: string): boolean {
const entry = this.sharedCache.get(key)
return entry?.expireAt !== undefined
}
// ============ Shared Cache (Cross-window) ============
/**
* Get value from shared cache
*/
getShared<T>(key: string): T | undefined {
const entry = this.sharedCache.get(key)
if (!entry) return undefined
// Check TTL (lazy cleanup)
if (entry.expireAt && Date.now() > entry.expireAt) {
this.sharedCache.delete(key)
this.notifySubscribers(key)
return undefined
}
return entry.value as T
}
/**
* Set value in shared cache
*/
setShared<T>(key: string, value: T, ttl?: number): void {
const existingEntry = this.sharedCache.get(key)
// Value comparison optimization
if (existingEntry && Object.is(existingEntry.value, value)) {
// Value is same, only update TTL if needed
const newExpireAt = ttl ? Date.now() + ttl : undefined
if (!Object.is(existingEntry.expireAt, newExpireAt)) {
existingEntry.expireAt = newExpireAt
logger.verbose(`Updated TTL for shared cache key "${key}"`)
// TTL change still needs broadcast for consistency
this.broadcastSync({
type: 'shared',
key,
value,
ttl
})
} else {
logger.verbose(`Skipped shared cache update for key "${key}" - value and TTL unchanged`)
}
return // Skip local update and notification
}
const entry: CacheEntry<T> = {
value,
expireAt: ttl ? Date.now() + ttl : undefined
}
// Update local copy first
this.sharedCache.set(key, entry)
this.notifySubscribers(key)
// Broadcast to other windows via Main
this.broadcastSync({
type: 'shared',
key,
value,
ttl
})
logger.verbose(`Updated shared cache for key "${key}"`)
}
/**
* Check if key exists in shared cache
*/
hasShared(key: string): boolean {
const entry = this.sharedCache.get(key)
if (!entry) return false
// Check TTL
if (entry.expireAt && Date.now() > entry.expireAt) {
this.sharedCache.delete(key)
this.notifySubscribers(key)
return false
}
return true
}
/**
* Delete from shared cache
*/
deleteShared(key: string): boolean {
// Check if key is being used by hooks
if (this.activeHooks.has(key)) {
logger.error(`Cannot delete key "${key}" as it's being used by useSharedCache hook`)
return false
}
// Check if key exists before attempting deletion
if (!this.sharedCache.has(key)) {
logger.verbose(`Skipped shared cache delete for key "${key}" - not exists`)
return true
}
this.sharedCache.delete(key)
this.notifySubscribers(key)
// Broadcast deletion to other windows
this.broadcastSync({
type: 'shared',
key,
value: undefined // undefined means deletion
})
logger.verbose(`Deleted shared cache key "${key}"`)
return true
}
// ============ Persist Cache (Cross-window + localStorage) ============
/**
* Get value from persist cache
*/
getPersist<K extends PersistCacheKey>(key: K): PersistCacheSchema[K] {
const value = this.persistCache.get(key)
if (value !== undefined) {
return value
}
// Fallback to default value if somehow missing
const defaultValue = DefaultPersistCache[key]
this.persistCache.set(key, defaultValue)
this.schedulePersistSave()
logger.warn(`Missing persist cache key "${key}", using default value`)
return defaultValue
}
/**
* Set value in persist cache
*/
setPersist<K extends PersistCacheKey>(key: K, value: PersistCacheSchema[K]): void {
const existingValue = this.persistCache.get(key)
// Use deep comparison for persist cache (usually objects)
if (this.deepEqual(existingValue, value)) {
logger.verbose(`Skipped persist cache update for key "${key}" - value unchanged`)
return // Skip all updates
}
this.persistCache.set(key, value)
this.notifySubscribers(key)
// Broadcast to other windows
this.broadcastSync({
type: 'persist',
key,
value
})
// Schedule persist save
this.schedulePersistSave()
logger.verbose(`Updated persist cache for key "${key}"`)
}
/**
* Check if key exists in persist cache
*/
hasPersist(key: PersistCacheKey): boolean {
return this.persistCache.has(key)
}
// Note: No deletePersist method as discussed
// ============ Hook Reference Management ============
/**
* Register a hook as using a specific key
*/
registerHook(key: string): void {
this.activeHooks.add(key)
}
/**
* Unregister a hook from using a specific key
*/
unregisterHook(key: string): void {
this.activeHooks.delete(key)
}
// ============ Subscription Management ============
/**
* Subscribe to cache changes for specific key
*/
subscribe(key: string, callback: CacheSubscriber): () => void {
if (!this.subscribers.has(key)) {
this.subscribers.set(key, new Set())
}
const keySubscribers = this.subscribers.get(key)!
keySubscribers.add(callback)
return () => {
keySubscribers.delete(callback)
if (keySubscribers.size === 0) {
this.subscribers.delete(key)
}
}
}
/**
* Notify subscribers for specific key
*/
notifySubscribers(key: string): void {
const keySubscribers = this.subscribers.get(key)
if (keySubscribers) {
keySubscribers.forEach((callback) => {
try {
callback()
} catch (error) {
logger.error(`Subscriber callback error for key ${key}:`, error as Error)
}
})
}
}
// ============ Private Methods ============
/**
* Deep equality comparison for cache values
*/
private deepEqual(a: any, b: any): boolean {
// Use Object.is for primitive values and same reference
if (Object.is(a, b)) return true
// Different types or null/undefined cases
if (typeof a !== 'object' || typeof b !== 'object') return false
if (a === null || b === null) return false
// Array comparison
if (Array.isArray(a) !== Array.isArray(b)) return false
if (Array.isArray(a)) {
if (a.length !== b.length) return false
for (let i = 0; i < a.length; i++) {
if (!this.deepEqual(a[i], b[i])) return false
}
return true
}
// Object comparison
const keysA = Object.keys(a)
const keysB = Object.keys(b)
if (keysA.length !== keysB.length) return false
for (const key of keysA) {
if (!keysB.includes(key)) return false
if (!this.deepEqual(a[key], b[key])) return false
}
return true
}
/**
* Load persist cache from localStorage
*/
private loadPersistCache(): void {
// First, initialize with default values
for (const [key, defaultValue] of Object.entries(DefaultPersistCache)) {
this.persistCache.set(key as PersistCacheKey, defaultValue)
}
try {
const stored = localStorage.getItem(STORAGE_PERSIST_KEY)
if (!stored) {
// No stored data, save defaults to localStorage
this.savePersistCache()
logger.debug('Initialized persist cache with default values')
return
}
const data = JSON.parse(stored)
// Only load keys that exist in schema, overriding defaults
const schemaKeys = Object.keys(DefaultPersistCache) as PersistCacheKey[]
for (const key of schemaKeys) {
if (key in data) {
this.persistCache.set(key, data[key])
}
}
// Clean up localStorage (remove invalid keys and save merged data)
this.savePersistCache()
logger.debug('Loaded persist cache from localStorage with defaults')
} catch (error) {
logger.error('Failed to load persist cache:', error as Error)
localStorage.removeItem(STORAGE_PERSIST_KEY)
// Fallback to defaults only
logger.debug('Fallback to default persist cache values')
}
}
/**
* Save persist cache to localStorage
*/
private savePersistCache(): void {
try {
const data: Record<string, any> = {}
for (const [key, value] of this.persistCache.entries()) {
data[key] = value
}
const jsonData = JSON.stringify(data)
const size = jsonData.length
if (size > 1024 * 1024 * 2) {
logger.warn(
`Persist cache is too large (${(size / (1024 * 1024)).toFixed(
2
)} MB), this may cause performance issues, and may cause data loss, please check your persist cache and reduce the size`
)
}
localStorage.setItem(STORAGE_PERSIST_KEY, jsonData)
logger.verbose(`Saved persist cache to localStorage, size: ${(size / (1024 * 1024)).toFixed(2)} MB`)
} catch (error) {
logger.error('Failed to save persist cache:', error as Error)
}
}
/**
* Schedule persist cache save with debounce
*/
private schedulePersistSave(): void {
this.persistDirty = true
if (this.persistSaveTimer) {
clearTimeout(this.persistSaveTimer)
}
this.persistSaveTimer = setTimeout(() => {
this.savePersistCache()
this.persistDirty = false
}, 200) // 200ms debounce
}
/**
* Broadcast cache sync message to other windows
*/
private broadcastSync(message: CacheSyncMessage): void {
if (window.api?.cache?.broadcastSync) {
window.api.cache.broadcastSync(message)
}
}
/**
* Setup IPC listeners for cache synchronization
*/
private setupIpcListeners(): void {
if (!window.api?.cache?.onSync) {
logger.warn('Cache sync API not available')
return
}
// Listen for cache sync messages from other windows
window.api.cache.onSync((message: CacheSyncMessage) => {
if (message.type === 'shared') {
if (message.value === undefined) {
// Handle deletion
this.sharedCache.delete(message.key)
} else {
// Handle set
const entry: CacheEntry = {
value: message.value,
expireAt: message.ttl ? Date.now() + message.ttl : undefined
}
this.sharedCache.set(message.key, entry)
}
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.notifySubscribers(message.key)
}
})
}
/**
* Setup window unload handler to force save persist cache
*/
private setupWindowUnloadHandler(): void {
window.addEventListener('beforeunload', () => {
if (this.persistDirty) {
this.savePersistCache()
}
})
}
/**
* Cleanup service resources
*/
public cleanup(): void {
// Force save persist cache if dirty
if (this.persistDirty) {
this.savePersistCache()
}
// Clear timers
if (this.persistSaveTimer) {
clearTimeout(this.persistSaveTimer)
}
// Clear caches
this.memoryCache.clear()
this.sharedCache.clear()
this.persistCache.clear()
// Clear tracking
this.activeHooks.clear()
this.subscribers.clear()
logger.debug('CacheService cleanup completed')
}
}
// Export singleton instance
export const cacheService = CacheService.getInstance()

View File

@ -1,10 +1,10 @@
import { loggerService } from '@logger'
import { DefaultPreferences } from '@shared/data/preferences'
import { DefaultPreferences } from '@shared/data/preference/preferenceSchemas'
import type {
PreferenceDefaultScopeType,
PreferenceKeyType,
PreferenceUpdateOptions
} from '@shared/data/preferenceTypes'
} from '@shared/data/preference/preferenceTypes'
const logger = loggerService.withContext('PreferenceService')

View File

@ -0,0 +1,150 @@
import { cacheService } from '@data/CacheService'
import { loggerService } from '@logger'
import type { PersistCacheKey, PersistCacheSchema } from '@shared/data/cache/cacheSchemas'
import { useCallback, useEffect, useSyncExternalStore } from 'react'
const logger = loggerService.withContext('useCache')
/**
* React hook for cross-component memory cache
*
* Features:
* - Synchronous API with useSyncExternalStore
* - Automatic default value setting
* - Hook lifecycle management
* - TTL support with warning when used
*
* @param key - Cache key
* @param defaultValue - Default value (set automatically if not exists)
* @returns [value, setValue]
*/
export function useCache<T>(key: string, defaultValue?: T): [T | undefined, (value: T) => 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
)
// Set default value if not exists
useEffect(() => {
if (defaultValue !== undefined && !cacheService.has(key)) {
cacheService.set(key, defaultValue)
}
}, [key, defaultValue])
// Register hook lifecycle
useEffect(() => {
cacheService.registerHook(key)
return () => cacheService.unregisterHook(key)
}, [key])
// Check for TTL warning
useEffect(() => {
if (cacheService.hasTTL(key)) {
logger.warn(
`useCache hook for key "${key}" is using a cache with TTL. This may cause unstable behavior as the value can expire between renders.`
)
}
}, [key])
const setValue = useCallback(
(newValue: T) => {
cacheService.set(key, newValue)
},
[key]
)
return [value ?? defaultValue, setValue]
}
/**
* React hook for cross-window shared cache
*
* Features:
* - Synchronous API (uses local copy)
* - Cross-window synchronization via IPC
* - Automatic default value setting
* - Hook lifecycle management
*
* @param key - Cache key
* @param defaultValue - Default value (set automatically if not exists)
* @returns [value, setValue]
*/
export function useSharedCache<T>(key: string, defaultValue?: T): [T | undefined, (value: T) => 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
)
// Set default value if not exists
useEffect(() => {
if (defaultValue !== undefined && !cacheService.hasShared(key)) {
cacheService.setShared(key, defaultValue)
}
}, [key, defaultValue])
// Register hook lifecycle
useEffect(() => {
cacheService.registerHook(key)
return () => cacheService.unregisterHook(key)
}, [key])
// Check for TTL warning
useEffect(() => {
if (cacheService.hasSharedTTL(key)) {
logger.warn(
`useSharedCache hook for key "${key}" is using a cache with TTL. This may cause unstable behavior as the value can expire between renders.`
)
}
}, [key])
const setValue = useCallback(
(newValue: T) => {
cacheService.setShared(key, newValue)
},
[key]
)
return [value ?? defaultValue, setValue]
}
/**
* React hook for persistent cache with localStorage
*
* Features:
* - Type-safe with predefined schema
* - Cross-window synchronization
* - Automatic default value setting
* - No TTL support (as discussed)
*
* @param key - Predefined persist cache key
* @returns [value, setValue]
*/
export function usePersistCache<K extends PersistCacheKey>(
key: K
): [PersistCacheSchema[K], (value: PersistCacheSchema[K]) => void] {
// Subscribe to cache changes
const value = useSyncExternalStore(
useCallback((callback) => cacheService.subscribe(key, callback), [key]),
useCallback(() => cacheService.getPersist(key), [key]),
useCallback(() => cacheService.getPersist(key), [key]) // SSR snapshot
)
// Register hook lifecycle (using string key for tracking)
useEffect(() => {
cacheService.registerHook(key)
return () => cacheService.unregisterHook(key)
}, [key])
const setValue = useCallback(
(newValue: PersistCacheSchema[K]) => {
cacheService.setPersist(key, newValue)
},
[key]
)
return [value, setValue]
}

View File

@ -1,11 +1,11 @@
import { preferenceService } from '@data/PreferenceService'
import { loggerService } from '@logger'
import { DefaultPreferences } from '@shared/data/preferences'
import { DefaultPreferences } from '@shared/data/preference/preferenceSchemas'
import type {
PreferenceDefaultScopeType,
PreferenceKeyType,
PreferenceUpdateOptions
} from '@shared/data/preferenceTypes'
} from '@shared/data/preference/preferenceTypes'
import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react'
const logger = loggerService.withContext('usePreference')

View File

@ -21,8 +21,8 @@ import { useEffect } from 'react'
import { useDefaultModel } from './useAssistant'
import useFullScreenNotice from './useFullScreenNotice'
import { useRuntime } from './useRuntime'
import { useNavbarPosition } from './useNavbar'
import { useRuntime } from './useRuntime'
import useUpdateHandler from './useUpdateHandler'
const logger = loggerService.withContext('useAppInit')

View File

@ -1,5 +1,5 @@
import { useTheme } from '@renderer/context/ThemeProvider'
import { ThemeMode } from '@shared/data/preferenceTypes'
import { ThemeMode } from '@shared/data/preference/preferenceTypes'
import { useEffect, useState } from 'react'
// 跟踪 mermaid 模块状态,单例模式

View File

@ -1,5 +1,5 @@
import { usePreference } from '@data/hooks/usePreference'
import { SidebarIcon } from '@shared/data/preferenceTypes'
import { SidebarIcon } from '@shared/data/preference/preferenceTypes'
export function useSidebarIconShow(icon: SidebarIcon) {
const [visibleSidebarIcons] = usePreference('ui.sidebar.icons.visible')

View File

@ -9,7 +9,7 @@ import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import type { Topic } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage'
import { classNames } from '@renderer/utils'
import type { MultiModelMessageStyle } from '@shared/data/preferenceTypes'
import type { MultiModelMessageStyle } from '@shared/data/preference/preferenceTypes'
import { Popover } from 'antd'
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import styled from 'styled-components'

View File

@ -13,7 +13,7 @@ import type { Topic } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage'
import { AssistantMessageStatus } from '@renderer/types/newMessage'
import { getMainTextContent } from '@renderer/utils/messageUtils/find'
import { MultiModelMessageStyle } from '@shared/data/preferenceTypes'
import { MultiModelMessageStyle } from '@shared/data/preference/preferenceTypes'
import { Button, Tooltip } from 'antd'
import { FC, memo } from 'react'
import { useTranslation } from 'react-i18next'

View File

@ -6,7 +6,7 @@ import Scrollbar from '@renderer/components/Scrollbar'
import type { Model } from '@renderer/types'
import { AssistantMessageStatus, type Message } from '@renderer/types/newMessage'
import { lightbulbSoftVariants } from '@renderer/utils/motionVariants'
import type { MultiModelFoldDisplayMode } from '@shared/data/preferenceTypes'
import type { MultiModelFoldDisplayMode } from '@shared/data/preference/preferenceTypes'
import { Avatar, Segmented as AntdSegmented, Tooltip } from 'antd'
import { motion } from 'motion/react'
import { FC, memo, useCallback } from 'react'

View File

@ -2,7 +2,7 @@ import { FormOutlined } from '@ant-design/icons'
import { useTheme } from '@renderer/context/ThemeProvider'
import { EventEmitter } from '@renderer/services/EventService'
import { EVENT_NAMES } from '@renderer/services/EventService'
import { ThemeMode } from '@shared/data/preferenceTypes'
import { ThemeMode } from '@shared/data/preference/preferenceTypes'
import { Button as AntdButton } from 'antd'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'

View File

@ -6,7 +6,7 @@ import { useAssistants } from '@renderer/hooks/useAssistant'
import { useAssistantsTabSortType } from '@renderer/hooks/useStore'
import { useTags } from '@renderer/hooks/useTags'
import { Assistant } from '@renderer/types'
import type { AssistantTabSortType } from '@shared/data/preferenceTypes'
import type { AssistantTabSortType } from '@shared/data/preference/preferenceTypes'
import { Tooltip, Typography } from 'antd'
import { Plus } from 'lucide-react'
import { FC, useCallback, useMemo, useRef, useState } from 'react'

View File

@ -22,8 +22,8 @@ import { setCodeFancyBlock } from '@renderer/store/settings'
import { Assistant, AssistantSettings, CodeStyleVarious, MathEngine } from '@renderer/types'
import { modalConfirm } from '@renderer/utils'
import { getSendMessageShortcutLabel } from '@renderer/utils/input'
import type { SendMessageShortcut } from '@shared/data/preferenceTypes'
import { ThemeMode } from '@shared/data/preferenceTypes'
import type { SendMessageShortcut } from '@shared/data/preference/preferenceTypes'
import { ThemeMode } from '@shared/data/preference/preferenceTypes'
import { Button, Col, InputNumber, Row, Slider, Switch } from 'antd'
import { Settings2 } from 'lucide-react'
import { FC, useCallback, useEffect, useMemo, useState } from 'react'

View File

@ -11,7 +11,7 @@ import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { Assistant } from '@renderer/types'
import { getLeadingEmoji, uuid } from '@renderer/utils'
import { hasTopicPendingRequests } from '@renderer/utils/queue'
import type { AssistantTabSortType } from '@shared/data/preferenceTypes'
import type { AssistantTabSortType } from '@shared/data/preference/preferenceTypes'
import { Dropdown, MenuProps } from 'antd'
import { omit } from 'lodash'
import {

View File

@ -10,8 +10,8 @@ import i18n from '@renderer/i18n'
import { handleSaveData, useAppDispatch } from '@renderer/store'
import { setUpdateState } from '@renderer/store/runtime'
import { runAsyncFunction } from '@renderer/utils'
import { UpgradeChannel } from '@shared/data/preferenceTypes'
import { ThemeMode } from '@shared/data/preferenceTypes'
import { UpgradeChannel } from '@shared/data/preference/preferenceTypes'
import { ThemeMode } from '@shared/data/preference/preferenceTypes'
import { Avatar, Button, Progress, Radio, Row, Switch, Tag, Tooltip } from 'antd'
import { debounce } from 'lodash'
import { Bug, FileCheck, Github, Globe, Mail, Rss } from 'lucide-react'

View File

@ -7,9 +7,9 @@ import { isMac, THEME_COLOR_PRESETS } from '@renderer/config/constant'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useNavbarPosition } from '@renderer/hooks/useNavbar'
import useUserTheme from '@renderer/hooks/useUserTheme'
import { DefaultPreferences } from '@shared/data/preferences'
import { AssistantIconType } from '@shared/data/preferenceTypes'
import { ThemeMode } from '@shared/data/preferenceTypes'
import { DefaultPreferences } from '@shared/data/preference/preferenceSchemas'
import { AssistantIconType } from '@shared/data/preference/preferenceTypes'
import { ThemeMode } from '@shared/data/preference/preferenceTypes'
import { Button, ColorPicker, Segmented, Select, Switch } from 'antd'
import { Minus, Monitor, Moon, Plus, Sun } from 'lucide-react'
import { FC, useCallback, useEffect, useMemo, useState } from 'react'

View File

@ -8,7 +8,7 @@ import {
DropResult
} from '@hello-pangea/dnd'
import { getSidebarIconLabel } from '@renderer/i18n/label'
import { SidebarIcon } from '@shared/data/preferenceTypes'
import { SidebarIcon } from '@shared/data/preference/preferenceTypes'
import { message } from 'antd'
import {
Code,

View File

@ -10,7 +10,7 @@ import { NotificationSource } from '@renderer/types/notification'
import { isValidProxyUrl } from '@renderer/utils'
import { formatErrorMessage } from '@renderer/utils/error'
import { defaultByPassRules, defaultLanguage } from '@shared/config/constant'
import { LanguageVarious } from '@shared/data/preferenceTypes'
import { LanguageVarious } from '@shared/data/preference/preferenceTypes'
import { Flex, Input, Switch, Tooltip } from 'antd'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'

View File

@ -3,7 +3,7 @@ import { isMac, isWin } from '@renderer/config/constant'
import { useTheme } from '@renderer/context/ThemeProvider'
import { getSelectionDescriptionLabel } from '@renderer/i18n/label'
import SelectionToolbar from '@renderer/windows/selection/toolbar/SelectionToolbar'
import type { SelectionFilterMode, SelectionTriggerMode } from '@shared/data/preferenceTypes'
import type { SelectionFilterMode, SelectionTriggerMode } from '@shared/data/preference/preferenceTypes'
import { Button, Radio, Row, Slider, Switch, Tooltip } from 'antd'
import { CircleHelp, Edit2 } from 'lucide-react'
import { FC, useEffect, useState } from 'react'

View File

@ -1,6 +1,6 @@
import type { DroppableProvided } from '@hello-pangea/dnd'
import { Draggable, Droppable } from '@hello-pangea/dnd'
import type { SelectionActionItem } from '@shared/data/preferenceTypes'
import type { SelectionActionItem } from '@shared/data/preference/preferenceTypes'
import { memo } from 'react'
import styled from 'styled-components'

View File

@ -1,5 +1,5 @@
import type { DraggableProvided } from '@hello-pangea/dnd'
import type { SelectionActionItem } from '@shared/data/preferenceTypes'
import type { SelectionActionItem } from '@shared/data/preference/preferenceTypes'
import { Button } from 'antd'
import { Pencil, Settings2, Trash } from 'lucide-react'
import { DynamicIcon } from 'lucide-react/dynamic'

View File

@ -1,5 +1,5 @@
import { loggerService } from '@logger'
import type { SelectionActionItem } from '@shared/data/preferenceTypes'
import type { SelectionActionItem } from '@shared/data/preference/preferenceTypes'
import { Button, Form, Input, Modal, Select } from 'antd'
import { Globe } from 'lucide-react'
import { FC, useEffect } from 'react'

View File

@ -2,7 +2,7 @@ import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
import CopyButton from '@renderer/components/CopyButton'
import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant'
import { getDefaultModel } from '@renderer/services/AssistantService'
import type { SelectionActionItem } from '@shared/data/preferenceTypes'
import type { SelectionActionItem } from '@shared/data/preference/preferenceTypes'
import { Col, Input, Modal, Radio, Row, Select, Space, Tooltip } from 'antd'
import { CircleHelp, Dices, OctagonX } from 'lucide-react'
import { DynamicIcon, iconNames } from 'lucide-react/dynamic'

View File

@ -1,8 +1,8 @@
import { DragDropContext } from '@hello-pangea/dnd'
import { useTheme } from '@renderer/context/ThemeProvider'
import SelectionToolbar from '@renderer/windows/selection/toolbar/SelectionToolbar'
import { DefaultPreferences } from '@shared/data/preferences'
import type { SelectionActionItem } from '@shared/data/preferenceTypes'
import { DefaultPreferences } from '@shared/data/preference/preferenceSchemas'
import type { SelectionActionItem } from '@shared/data/preference/preferenceTypes'
import { Row } from 'antd'
import { FC } from 'react'
import styled from 'styled-components'

View File

@ -1,7 +1,7 @@
import { DropResult } from '@hello-pangea/dnd'
import { loggerService } from '@logger'
import { DefaultPreferences } from '@shared/data/preferences'
import type { SelectionActionItem } from '@shared/data/preferenceTypes'
import { DefaultPreferences } from '@shared/data/preference/preferenceSchemas'
import type { SelectionActionItem } from '@shared/data/preference/preferenceTypes'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'

View File

@ -1,4 +1,4 @@
import { ThemeMode } from '@shared/data/preferenceTypes'
import { ThemeMode } from '@shared/data/preference/preferenceTypes'
import { ChevronDown, ChevronRight } from 'lucide-react'
import { AnimatePresence, motion } from 'motion/react'
import { useState } from 'react'

View File

@ -1,4 +1,4 @@
import { ThemeMode } from '@shared/data/preferenceTypes'
import { ThemeMode } from '@shared/data/preference/preferenceTypes'
import { Divider } from 'antd'
import Link from 'antd/es/typography/Link'
import styled, { CSSProp } from 'styled-components'

View File

@ -33,8 +33,8 @@ import {
import { getDefaultGroupName, getLeadingEmoji, runAsyncFunction, uuid } from '@renderer/utils'
import { defaultByPassRules } from '@shared/config/constant'
import { TRANSLATE_PROMPT } from '@shared/config/prompts'
import { DefaultPreferences } from '@shared/data/preferences'
import { UpgradeChannel } from '@shared/data/preferenceTypes'
import { DefaultPreferences } from '@shared/data/preference/preferenceSchemas'
import { UpgradeChannel } from '@shared/data/preference/preferenceTypes'
import { isEmpty } from 'lodash'
import { createMigrate } from 'redux-persist'

View File

@ -2,8 +2,8 @@
* @deprecated The whole file will be removed after data refactoring
*/
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import type { SelectionActionItem } from '@shared/data/preferenceTypes'
import { SelectionFilterMode, SelectionTriggerMode } from '@shared/data/preferenceTypes'
import type { SelectionActionItem } from '@shared/data/preference/preferenceTypes'
import { SelectionFilterMode, SelectionTriggerMode } from '@shared/data/preference/preferenceTypes'
export interface SelectionState {
selectionEnabled: boolean

View File

@ -12,7 +12,7 @@ import {
} from '@renderer/types'
import { uuid } from '@renderer/utils'
import { TRANSLATE_PROMPT } from '@shared/config/prompts'
import { DefaultPreferences } from '@shared/data/preferences'
import { DefaultPreferences } from '@shared/data/preference/preferenceSchemas'
import type {
AssistantIconType,
AssistantTabSortType,
@ -20,8 +20,8 @@ import type {
MultiModelMessageStyle,
SendMessageShortcut,
SidebarIcon
} from '@shared/data/preferenceTypes'
import { ThemeMode, UpgradeChannel } from '@shared/data/preferenceTypes'
} from '@shared/data/preference/preferenceTypes'
import { ThemeMode, UpgradeChannel } from '@shared/data/preference/preferenceTypes'
import { OpenAIVerbosity } from '@types'
import { RemoteSyncState } from './backup'

View File

@ -1,4 +1,4 @@
import type { SendMessageShortcut } from '@shared/data/preferenceTypes'
import type { SendMessageShortcut } from '@shared/data/preference/preferenceTypes'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { getFilesFromDropEvent, getSendMessageShortcutLabel, isSendMessageKeyPressed } from '../input'

View File

@ -1,7 +1,7 @@
import { loggerService } from '@logger'
import { isMac, isWin } from '@renderer/config/constant'
import { FileMetadata } from '@renderer/types'
import type { SendMessageShortcut } from '@shared/data/preferenceTypes'
import type { SendMessageShortcut } from '@shared/data/preference/preferenceTypes'
const logger = loggerService.withContext('Utils:Input')

View File

@ -1,7 +1,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/preferenceTypes'
import { ThemeMode } from '@shared/data/preference/preferenceTypes'
import { Button, Card, Col, Divider, Layout, Row, Space, Typography } from 'antd'
import { Activity, AlertTriangle, Database, FlaskConical, Settings, TestTube, TrendingUp, Zap } from 'lucide-react'
import React from 'react'

View File

@ -1,5 +1,5 @@
import { usePreference } from '@renderer/data/hooks/usePreference'
import { type PreferenceKeyType, ThemeMode } from '@shared/data/preferenceTypes'
import { type PreferenceKeyType, ThemeMode } from '@shared/data/preference/preferenceTypes'
import { Button, Input, message, Select, Slider, Space, Switch, Typography } from 'antd'
import React, { useState } from 'react'
import styled from 'styled-components'

View File

@ -1,7 +1,7 @@
import { usePreference } from '@renderer/data/hooks/usePreference'
import { preferenceService } from '@renderer/data/PreferenceService'
import { loggerService } from '@renderer/services/LoggerService'
import { type PreferenceKeyType, ThemeMode } from '@shared/data/preferenceTypes'
import { type PreferenceKeyType, ThemeMode } from '@shared/data/preference/preferenceTypes'
import { Button, Card, message, Space, Typography } from 'antd'
import React, { useState } from 'react'
import styled from 'styled-components'

View File

@ -1,6 +1,6 @@
import { usePreference } from '@renderer/data/hooks/usePreference'
import { preferenceService } from '@renderer/data/PreferenceService'
import { type PreferenceKeyType, ThemeMode } from '@shared/data/preferenceTypes'
import { type PreferenceKeyType, ThemeMode } from '@shared/data/preference/preferenceTypes'
import { Button, Input, message, Space, Typography } from 'antd'
import React, { useState } from 'react'
import styled from 'styled-components'

View File

@ -20,7 +20,7 @@ import { isAbortError } from '@renderer/utils/error'
import { createMainTextBlock, createThinkingBlock } from '@renderer/utils/messageUtils/create'
import { getMainTextContent } from '@renderer/utils/messageUtils/find'
import { defaultLanguage } from '@shared/config/constant'
import { ThemeMode } from '@shared/data/preferenceTypes'
import { ThemeMode } from '@shared/data/preference/preferenceTypes'
import { IpcChannel } from '@shared/IpcChannel'
import { Divider } from 'antd'
import { cloneDeep, isEmpty } from 'lodash'

View File

@ -2,7 +2,7 @@ import { usePreference } from '@data/hooks/usePreference'
import { isMac } from '@renderer/config/constant'
import i18n from '@renderer/i18n'
import { defaultLanguage } from '@shared/config/constant'
import type { SelectionActionItem } from '@shared/data/preferenceTypes'
import type { SelectionActionItem } from '@shared/data/preference/preferenceTypes'
import { IpcChannel } from '@shared/IpcChannel'
import { Button, Slider, Tooltip } from 'antd'
import { Droplet, Minus, Pin, X } from 'lucide-react'

View File

@ -13,7 +13,7 @@ import {
import { pauseTrace } from '@renderer/services/SpanManagerService'
import { Assistant, Topic } from '@renderer/types'
import { abortCompletion } from '@renderer/utils/abortController'
import type { SelectionActionItem } from '@shared/data/preferenceTypes'
import type { SelectionActionItem } from '@shared/data/preference/preferenceTypes'
import { ChevronDown } from 'lucide-react'
import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'

View File

@ -14,7 +14,7 @@ import { runAsyncFunction } from '@renderer/utils'
import { abortCompletion } from '@renderer/utils/abortController'
import { detectLanguage } from '@renderer/utils/translate'
import { defaultLanguage } from '@shared/config/constant'
import type { SelectionActionItem } from '@shared/data/preferenceTypes'
import type { SelectionActionItem } from '@shared/data/preference/preferenceTypes'
import { Tooltip } from 'antd'
import { ArrowRightFromLine, ArrowRightToLine, ChevronDown, CircleHelp, Globe } from 'lucide-react'
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'

View File

@ -6,7 +6,7 @@ import { AppLogo } from '@renderer/config/env'
import { useTimer } from '@renderer/hooks/useTimer'
import i18n from '@renderer/i18n'
import { defaultLanguage } from '@shared/config/constant'
import type { SelectionActionItem } from '@shared/data/preferenceTypes'
import type { SelectionActionItem } from '@shared/data/preference/preferenceTypes'
import { IpcChannel } from '@shared/IpcChannel'
import { Avatar } from 'antd'
import { ClipboardCheck, ClipboardCopy, ClipboardX, MessageSquareHeart } from 'lucide-react'