mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-02-21 10:04:44 +08:00
feat: CacheService & useCache Hooks
This commit is contained in:
parent
2e07b4ea58
commit
6d89f94335
@ -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',
|
||||
|
||||
@ -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._
|
||||
|
||||
22
packages/shared/data/cache/cacheSchemas.ts
vendored
Normal file
22
packages/shared/data/cache/cacheSchemas.ts
vendored
Normal 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
|
||||
43
packages/shared/data/cache/cacheTypes.ts
vendored
Normal file
43
packages/shared/data/cache/cacheTypes.ts
vendored
Normal 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
|
||||
@ -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,
|
||||
@ -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 = {
|
||||
170
src/main/data/CacheService.ts
Normal file
170
src/main/data/CacheService.ts
Normal 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
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { SidebarIcon } from '@shared/data/preferenceTypes'
|
||||
import { SidebarIcon } from '@shared/data/preference/preferenceTypes'
|
||||
|
||||
//TODO 这个文件是否还有存在的价值? fullex @ data refactor
|
||||
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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 {
|
||||
|
||||
584
src/renderer/src/data/CacheService.ts
Normal file
584
src/renderer/src/data/CacheService.ts
Normal 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()
|
||||
@ -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')
|
||||
|
||||
|
||||
150
src/renderer/src/data/hooks/useCache.ts
Normal file
150
src/renderer/src/data/hooks/useCache.ts
Normal 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]
|
||||
}
|
||||
@ -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')
|
||||
|
||||
@ -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')
|
||||
|
||||
|
||||
@ -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 模块状态,单例模式
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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')
|
||||
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
Loading…
Reference in New Issue
Block a user