Merge branch 'main' of github.com:CherryHQ/cherry-studio into fix/verbosity
This commit is contained in:
2
.github/workflows/auto-i18n.yml
vendored
2
.github/workflows/auto-i18n.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
- name: 📦 Setting Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
package-manager-cache: false
|
||||
|
||||
- name: 📦 Install dependencies in isolated directory
|
||||
|
||||
6
.github/workflows/github-issue-tracker.yml
vendored
6
.github/workflows/github-issue-tracker.yml
vendored
@@ -5,7 +5,7 @@ on:
|
||||
types: [opened]
|
||||
schedule:
|
||||
# Run every day at 8:30 Beijing Time (00:30 UTC)
|
||||
- cron: '30 0 * * *'
|
||||
- cron: "30 0 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
if: steps.check_time.outputs.should_delay == 'false'
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: 22
|
||||
|
||||
- name: Process issue with Claude
|
||||
if: steps.check_time.outputs.should_delay == 'false'
|
||||
@@ -123,7 +123,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: 22
|
||||
|
||||
- name: Process pending issues with Claude
|
||||
uses: anthropics/claude-code-action@main
|
||||
|
||||
6
.github/workflows/nightly-build.yml
vendored
6
.github/workflows/nightly-build.yml
vendored
@@ -3,7 +3,7 @@ name: Nightly Build
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 17 * * *' # 1:00 BJ Time
|
||||
- cron: "0 17 * * *" # 1:00 BJ Time
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
|
||||
- name: macos-latest dependencies fix
|
||||
if: matrix.os == 'macos-latest'
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
brew install python-setuptools
|
||||
|
||||
- name: Install corepack
|
||||
run: corepack enable && corepack prepare yarn@4.6.0 --activate
|
||||
run: corepack enable && corepack prepare yarn@4.9.1 --activate
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
|
||||
4
.github/workflows/pr-ci.yml
vendored
4
.github/workflows/pr-ci.yml
vendored
@@ -26,10 +26,10 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
|
||||
- name: Install corepack
|
||||
run: corepack enable && corepack prepare yarn@4.6.0 --activate
|
||||
run: corepack enable && corepack prepare yarn@4.9.1 --activate
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
|
||||
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
@@ -4,9 +4,9 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag (e.g. v1.0.0)'
|
||||
description: "Release tag (e.g. v1.0.0)"
|
||||
required: true
|
||||
default: 'v1.0.0'
|
||||
default: "v1.0.0"
|
||||
push:
|
||||
tags:
|
||||
- v*.*.*
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
|
||||
- name: macos-latest dependencies fix
|
||||
if: matrix.os == 'macos-latest'
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
brew install python-setuptools
|
||||
|
||||
- name: Install corepack
|
||||
run: corepack enable && corepack prepare yarn@4.6.0 --activate
|
||||
run: corepack enable && corepack prepare yarn@4.9.1 --activate
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
@@ -127,5 +127,5 @@ jobs:
|
||||
allowUpdates: true
|
||||
makeLatest: false
|
||||
tag: ${{ steps.get-tag.outputs.tag }}
|
||||
artifacts: 'dist/*.exe,dist/*.zip,dist/*.dmg,dist/*.AppImage,dist/*.snap,dist/*.deb,dist/*.rpm,dist/*.tar.gz,dist/latest*.yml,dist/rc*.yml,dist/beta*.yml,dist/*.blockmap'
|
||||
artifacts: "dist/*.exe,dist/*.zip,dist/*.dmg,dist/*.AppImage,dist/*.snap,dist/*.deb,dist/*.rpm,dist/*.tar.gz,dist/latest*.yml,dist/rc*.yml,dist/beta*.yml,dist/*.blockmap"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -18,13 +18,13 @@ yarn
|
||||
|
||||
### Setup Node.js
|
||||
|
||||
Download and install [Node.js v20.x.x](https://nodejs.org/en/download)
|
||||
Download and install [Node.js v22.x.x](https://nodejs.org/en/download)
|
||||
|
||||
### Setup Yarn
|
||||
|
||||
```bash
|
||||
corepack enable
|
||||
corepack prepare yarn@4.6.0 --activate
|
||||
corepack prepare yarn@4.9.1 --activate
|
||||
```
|
||||
|
||||
### Install Dependencies
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
"@libsql/client": "0.14.0",
|
||||
"@libsql/win32-x64-msvc": "^0.4.7",
|
||||
"@napi-rs/system-ocr": "patch:@napi-rs/system-ocr@npm%3A1.0.2#~/.yarn/patches/@napi-rs-system-ocr-npm-1.0.2-59e7a78e8b.patch",
|
||||
"@paymoapp/electron-shutdown-handler": "^1.1.2",
|
||||
"@strongtz/win32-arm64-msvc": "^0.4.7",
|
||||
"express": "^5.1.0",
|
||||
"font-list": "^2.0.0",
|
||||
@@ -373,6 +374,7 @@
|
||||
"zod": "^4.1.5"
|
||||
},
|
||||
"resolutions": {
|
||||
"@smithy/types": "4.7.1",
|
||||
"@codemirror/language": "6.11.3",
|
||||
"@codemirror/lint": "6.8.5",
|
||||
"@codemirror/view": "6.38.1",
|
||||
|
||||
@@ -21,6 +21,7 @@ import { appMenuService } from './services/AppMenuService'
|
||||
import { configManager } from './services/ConfigManager'
|
||||
import mcpService from './services/MCPService'
|
||||
import { nodeTraceService } from './services/NodeTraceService'
|
||||
import powerMonitorService from './services/PowerMonitorService'
|
||||
import {
|
||||
CHERRY_STUDIO_PROTOCOL,
|
||||
handleProtocolUrl,
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
import selectionService, { initSelectionService } from './services/SelectionService'
|
||||
import { registerShortcuts } from './services/ShortcutService'
|
||||
import { TrayService } from './services/TrayService'
|
||||
import { versionService } from './services/VersionService'
|
||||
import { windowService } from './services/WindowService'
|
||||
import { initWebviewHotkeys } from './services/WebviewService'
|
||||
|
||||
@@ -110,6 +112,10 @@ if (!app.requestSingleInstanceLock()) {
|
||||
// Some APIs can only be used after this event occurs.
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
// Record current version for tracking
|
||||
// A preparation for v2 data refactoring
|
||||
versionService.recordCurrentVersion()
|
||||
|
||||
initWebviewHotkeys()
|
||||
// Set app user model id for windows
|
||||
electronApp.setAppUserModelId(import.meta.env.VITE_MAIN_BUNDLE_ID || 'com.kangfenmao.CherryStudio')
|
||||
@@ -127,6 +133,7 @@ if (!app.requestSingleInstanceLock()) {
|
||||
appMenuService?.setupApplicationMenu()
|
||||
|
||||
nodeTraceService.init()
|
||||
powerMonitorService.init()
|
||||
|
||||
app.on('activate', function () {
|
||||
const mainWindow = windowService.getMainWindow()
|
||||
|
||||
@@ -50,6 +50,7 @@ import * as NutstoreService from './services/NutstoreService'
|
||||
import ObsidianVaultService from './services/ObsidianVaultService'
|
||||
import { ocrService } from './services/ocr/OcrService'
|
||||
import OvmsManager from './services/OvmsManager'
|
||||
import powerMonitorService from './services/PowerMonitorService'
|
||||
import { proxyManager } from './services/ProxyManager'
|
||||
import { pythonService } from './services/PythonService'
|
||||
import { FileServiceManager } from './services/remotefile/FileServiceManager'
|
||||
@@ -115,6 +116,18 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
const appUpdater = new AppUpdater()
|
||||
const notificationService = new NotificationService()
|
||||
|
||||
// Register shutdown handlers
|
||||
powerMonitorService.registerShutdownHandler(() => {
|
||||
appUpdater.setAutoUpdate(false)
|
||||
})
|
||||
|
||||
powerMonitorService.registerShutdownHandler(() => {
|
||||
const mw = windowService.getMainWindow()
|
||||
if (mw && !mw.isDestroyed()) {
|
||||
mw.webContents.send(IpcChannel.App_SaveData)
|
||||
}
|
||||
})
|
||||
|
||||
const checkMainWindow = () => {
|
||||
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||
throw new Error('Main window does not exist or has been destroyed')
|
||||
|
||||
112
src/main/services/PowerMonitorService.ts
Normal file
112
src/main/services/PowerMonitorService.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { isLinux, isMac, isWin } from '@main/constant'
|
||||
import ElectronShutdownHandler from '@paymoapp/electron-shutdown-handler'
|
||||
import { BrowserWindow } from 'electron'
|
||||
import { powerMonitor } from 'electron'
|
||||
|
||||
const logger = loggerService.withContext('PowerMonitorService')
|
||||
|
||||
type ShutdownHandler = () => void | Promise<void>
|
||||
|
||||
export class PowerMonitorService {
|
||||
private static instance: PowerMonitorService
|
||||
private initialized = false
|
||||
private shutdownHandlers: ShutdownHandler[] = []
|
||||
|
||||
private constructor() {
|
||||
// Private constructor to prevent direct instantiation
|
||||
}
|
||||
|
||||
public static getInstance(): PowerMonitorService {
|
||||
if (!PowerMonitorService.instance) {
|
||||
PowerMonitorService.instance = new PowerMonitorService()
|
||||
}
|
||||
return PowerMonitorService.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a shutdown handler to be called when system shutdown is detected
|
||||
* @param handler - The handler function to be called on shutdown
|
||||
*/
|
||||
public registerShutdownHandler(handler: ShutdownHandler): void {
|
||||
this.shutdownHandlers.push(handler)
|
||||
logger.info('Shutdown handler registered', { totalHandlers: this.shutdownHandlers.length })
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize power monitor to listen for shutdown events
|
||||
*/
|
||||
public init(): void {
|
||||
if (this.initialized) {
|
||||
logger.warn('PowerMonitorService already initialized')
|
||||
return
|
||||
}
|
||||
|
||||
if (isWin) {
|
||||
this.initWindowsShutdownHandler()
|
||||
} else if (isMac || isLinux) {
|
||||
this.initElectronPowerMonitor()
|
||||
}
|
||||
|
||||
this.initialized = true
|
||||
logger.info('PowerMonitorService initialized', { platform: process.platform })
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute all registered shutdown handlers
|
||||
*/
|
||||
private async executeShutdownHandlers(): Promise<void> {
|
||||
logger.info('Executing shutdown handlers', { count: this.shutdownHandlers.length })
|
||||
for (const handler of this.shutdownHandlers) {
|
||||
try {
|
||||
await handler()
|
||||
} catch (error) {
|
||||
logger.error('Error executing shutdown handler', error as Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize shutdown handler for Windows using @paymoapp/electron-shutdown-handler
|
||||
*/
|
||||
private initWindowsShutdownHandler(): void {
|
||||
try {
|
||||
const zeroMemoryWindow = new BrowserWindow({ show: false })
|
||||
// Set the window handle for the shutdown handler
|
||||
ElectronShutdownHandler.setWindowHandle(zeroMemoryWindow.getNativeWindowHandle())
|
||||
|
||||
// Listen for shutdown event
|
||||
ElectronShutdownHandler.on('shutdown', async () => {
|
||||
logger.info('System shutdown event detected (Windows)')
|
||||
// Execute all registered shutdown handlers
|
||||
await this.executeShutdownHandlers()
|
||||
// Release the shutdown block to allow the system to shut down
|
||||
ElectronShutdownHandler.releaseShutdown()
|
||||
})
|
||||
|
||||
logger.info('Windows shutdown handler registered')
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize Windows shutdown handler', error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize power monitor for macOS and Linux using Electron's powerMonitor
|
||||
*/
|
||||
private initElectronPowerMonitor(): void {
|
||||
try {
|
||||
powerMonitor.on('shutdown', async () => {
|
||||
logger.info('System shutdown event detected', { platform: process.platform })
|
||||
// Execute all registered shutdown handlers
|
||||
await this.executeShutdownHandlers()
|
||||
})
|
||||
|
||||
logger.info('Electron powerMonitor shutdown listener registered')
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize Electron powerMonitor', error as Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default export as singleton instance
|
||||
export default PowerMonitorService.getInstance()
|
||||
285
src/main/services/VersionService.ts
Normal file
285
src/main/services/VersionService.ts
Normal file
@@ -0,0 +1,285 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { app } from 'electron'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
const logger = loggerService.withContext('VersionService')
|
||||
|
||||
type OS = 'win' | 'mac' | 'linux' | 'unknown'
|
||||
type Environment = 'prod' | 'dev'
|
||||
type Packaged = 'packaged' | 'unpackaged'
|
||||
type Mode = 'install' | 'portable'
|
||||
|
||||
/**
|
||||
* Version record stored in version.log
|
||||
*/
|
||||
interface VersionRecord {
|
||||
version: string
|
||||
os: OS
|
||||
environment: Environment
|
||||
packaged: Packaged
|
||||
mode: Mode
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Service for tracking application version history
|
||||
* Stores version information in userData/version.log for data migration and diagnostics
|
||||
*/
|
||||
class VersionService {
|
||||
private readonly VERSION_LOG_FILE = 'version.log'
|
||||
private versionLogPath: string | null = null
|
||||
|
||||
constructor() {
|
||||
// Lazy initialization of path since app.getPath may not be available during construction
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the full path to version.log file
|
||||
* @returns {string} Full path to version log file
|
||||
*/
|
||||
private getVersionLogPath(): string {
|
||||
if (!this.versionLogPath) {
|
||||
this.versionLogPath = path.join(app.getPath('userData'), this.VERSION_LOG_FILE)
|
||||
}
|
||||
return this.versionLogPath
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets current operating system identifier
|
||||
* @returns {OS} OS identifier
|
||||
*/
|
||||
private getCurrentOS(): OS {
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
return 'win'
|
||||
case 'darwin':
|
||||
return 'mac'
|
||||
case 'linux':
|
||||
return 'linux'
|
||||
default:
|
||||
return 'unknown'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets current environment (production or development)
|
||||
* @returns {Environment} Environment identifier
|
||||
*/
|
||||
private getCurrentEnvironment(): Environment {
|
||||
return import.meta.env.MODE === 'production' ? 'prod' : 'dev'
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets packaging status
|
||||
* @returns {Packaged} Packaging status
|
||||
*/
|
||||
private getPackagedStatus(): Packaged {
|
||||
return app.isPackaged ? 'packaged' : 'unpackaged'
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets installation mode (install or portable)
|
||||
* @returns {Mode} Installation mode
|
||||
*/
|
||||
private getInstallMode(): Mode {
|
||||
return process.env.PORTABLE_EXECUTABLE_DIR !== undefined ? 'portable' : 'install'
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates version log line for current application state
|
||||
* @returns {string} Pipe-separated version record line
|
||||
*/
|
||||
private generateCurrentVersionLine(): string {
|
||||
const version = app.getVersion()
|
||||
const os = this.getCurrentOS()
|
||||
const environment = this.getCurrentEnvironment()
|
||||
const packaged = this.getPackagedStatus()
|
||||
const mode = this.getInstallMode()
|
||||
const timestamp = new Date().toISOString()
|
||||
|
||||
return `${version}|${os}|${environment}|${packaged}|${mode}|${timestamp}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a version log line into a VersionRecord object
|
||||
* @param {string} line - Pipe-separated version record line
|
||||
* @returns {VersionRecord | null} Parsed version record or null if invalid
|
||||
*/
|
||||
private parseVersionLine(line: string): VersionRecord | null {
|
||||
try {
|
||||
const parts = line.trim().split('|')
|
||||
if (parts.length !== 6) {
|
||||
return null
|
||||
}
|
||||
|
||||
const [version, os, environment, packaged, mode, timestamp] = parts
|
||||
|
||||
// Validate data
|
||||
if (
|
||||
!version ||
|
||||
!['win', 'mac', 'linux', 'unknown'].includes(os) ||
|
||||
!['prod', 'dev'].includes(environment) ||
|
||||
!['packaged', 'unpackaged'].includes(packaged) ||
|
||||
!['install', 'portable'].includes(mode) ||
|
||||
!timestamp
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
version,
|
||||
os: os as OS,
|
||||
environment: environment as Environment,
|
||||
packaged: packaged as Packaged,
|
||||
mode: mode as Mode,
|
||||
timestamp
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to parse version line: ${line}`, error as Error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the last 1KB from version.log and returns all lines
|
||||
* Uses reverse reading from file end to avoid reading the entire file
|
||||
* @returns {string[]} Array of version lines from the last 1KB
|
||||
*/
|
||||
private readLastVersionLines(): string[] {
|
||||
const logPath = this.getVersionLogPath()
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(logPath)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const stats = fs.statSync(logPath)
|
||||
const fileSize = stats.size
|
||||
|
||||
if (fileSize === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Read from the end of the file, 1KB is enough to find previous version
|
||||
// Typical line: "1.7.0-beta.3|win|prod|packaged|install|2025-01-15T08:30:00.000Z\n" (~70 bytes)
|
||||
// 1KB can store ~14 lines, which is more than enough
|
||||
const bufferSize = Math.min(1024, fileSize)
|
||||
const buffer = Buffer.alloc(bufferSize)
|
||||
|
||||
const fd = fs.openSync(logPath, 'r')
|
||||
try {
|
||||
const startPosition = Math.max(0, fileSize - bufferSize)
|
||||
fs.readSync(fd, buffer, 0, bufferSize, startPosition)
|
||||
|
||||
const content = buffer.toString('utf-8')
|
||||
const lines = content
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter((line) => line.trim())
|
||||
|
||||
return lines
|
||||
} finally {
|
||||
fs.closeSync(fd)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to read version log:', error as Error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a version record line to version.log
|
||||
* @param {string} line - Version record line to append
|
||||
*/
|
||||
private appendVersionLine(line: string): void {
|
||||
const logPath = this.getVersionLogPath()
|
||||
|
||||
try {
|
||||
fs.appendFileSync(logPath, line + '\n', 'utf-8')
|
||||
logger.debug(`Version recorded: ${line}`)
|
||||
} catch (error) {
|
||||
logger.error('Failed to append version log:', error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Records the current version on application startup
|
||||
* Only adds a new record if the version has changed since the last run
|
||||
*/
|
||||
recordCurrentVersion(): void {
|
||||
try {
|
||||
const currentLine = this.generateCurrentVersionLine()
|
||||
const lines = this.readLastVersionLines()
|
||||
|
||||
// Add new record if this is the first run or version has changed
|
||||
if (lines.length === 0) {
|
||||
logger.info('First run detected, creating version log')
|
||||
this.appendVersionLine(currentLine)
|
||||
return
|
||||
}
|
||||
|
||||
const lastLine = lines[lines.length - 1]
|
||||
const lastRecord = this.parseVersionLine(lastLine)
|
||||
const currentVersion = app.getVersion()
|
||||
|
||||
// Check if any meaningful field has changed (version, os, environment, packaged, mode)
|
||||
const currentOS = this.getCurrentOS()
|
||||
const currentEnvironment = this.getCurrentEnvironment()
|
||||
const currentPackaged = this.getPackagedStatus()
|
||||
const currentMode = this.getInstallMode()
|
||||
|
||||
const hasMeaningfulChange =
|
||||
!lastRecord ||
|
||||
lastRecord.version !== currentVersion ||
|
||||
lastRecord.os !== currentOS ||
|
||||
lastRecord.environment !== currentEnvironment ||
|
||||
lastRecord.packaged !== currentPackaged ||
|
||||
lastRecord.mode !== currentMode
|
||||
|
||||
if (hasMeaningfulChange) {
|
||||
logger.info(`Version information changed, recording new entry`)
|
||||
this.appendVersionLine(currentLine)
|
||||
} else {
|
||||
logger.debug(`Version information not changed, skip recording`)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to record current version:', error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the previous version record (last record with different version than current)
|
||||
* Reads from the last 1KB of version.log to find the most recent different version
|
||||
* Useful for detecting version upgrades and running migrations
|
||||
* @returns {VersionRecord | null} Previous version record or null if not available
|
||||
*/
|
||||
getPreviousVersion(): VersionRecord | null {
|
||||
try {
|
||||
const lines = this.readLastVersionLines()
|
||||
if (lines.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const currentVersion = app.getVersion()
|
||||
|
||||
// Read from the end backwards to find the first different version
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
const record = this.parseVersionLine(lines[i])
|
||||
if (record && record.version !== currentVersion) {
|
||||
return record
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
} catch (error) {
|
||||
logger.error('Failed to get previous version:', error as Error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Singleton instance of VersionService
|
||||
*/
|
||||
export const versionService = new VersionService()
|
||||
@@ -21,10 +21,44 @@ vi.mock('@renderer/store', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@renderer/utils/api', () => ({
|
||||
formatApiHost: vi.fn((host, isSupportedAPIVersion = true) => {
|
||||
if (isSupportedAPIVersion === false) {
|
||||
return host // Return host as-is when isSupportedAPIVersion is false
|
||||
}
|
||||
return `${host}/v1` // Default behavior when isSupportedAPIVersion is true
|
||||
}),
|
||||
routeToEndpoint: vi.fn((host) => ({
|
||||
baseURL: host,
|
||||
endpoint: '/chat/completions'
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@renderer/config/providers', async (importOriginal) => {
|
||||
const actual = (await importOriginal()) as any
|
||||
return {
|
||||
...actual,
|
||||
isCherryAIProvider: vi.fn(),
|
||||
isAnthropicProvider: vi.fn(() => false),
|
||||
isAzureOpenAIProvider: vi.fn(() => false),
|
||||
isGeminiProvider: vi.fn(() => false),
|
||||
isNewApiProvider: vi.fn(() => false)
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@renderer/hooks/useVertexAI', () => ({
|
||||
isVertexProvider: vi.fn(() => false),
|
||||
isVertexAIConfigured: vi.fn(() => false),
|
||||
createVertexProvider: vi.fn()
|
||||
}))
|
||||
|
||||
import { isCherryAIProvider } from '@renderer/config/providers'
|
||||
import { getProviderByModel } from '@renderer/services/AssistantService'
|
||||
import type { Model, Provider } from '@renderer/types'
|
||||
import { formatApiHost } from '@renderer/utils/api'
|
||||
|
||||
import { COPILOT_DEFAULT_HEADERS, COPILOT_EDITOR_VERSION, isCopilotResponsesModel } from '../constants'
|
||||
import { providerToAiSdkConfig } from '../providerConfig'
|
||||
import { getActualProvider, providerToAiSdkConfig } from '../providerConfig'
|
||||
|
||||
const createWindowKeyv = () => {
|
||||
const store = new Map<string, string>()
|
||||
@@ -46,11 +80,21 @@ const createCopilotProvider = (): Provider => ({
|
||||
isSystem: true
|
||||
})
|
||||
|
||||
const createModel = (id: string, name = id): Model => ({
|
||||
const createModel = (id: string, name = id, provider = 'copilot'): Model => ({
|
||||
id,
|
||||
name,
|
||||
provider: 'copilot',
|
||||
group: 'copilot'
|
||||
provider,
|
||||
group: provider
|
||||
})
|
||||
|
||||
const createCherryAIProvider = (): Provider => ({
|
||||
id: 'cherryai',
|
||||
type: 'openai',
|
||||
name: 'CherryAI',
|
||||
apiKey: 'test-key',
|
||||
apiHost: 'https://api.cherryai.com',
|
||||
models: [],
|
||||
isSystem: false
|
||||
})
|
||||
|
||||
describe('Copilot responses routing', () => {
|
||||
@@ -87,3 +131,67 @@ describe('Copilot responses routing', () => {
|
||||
expect(config.options.headers?.['Copilot-Integration-Id']).toBe(COPILOT_DEFAULT_HEADERS['Copilot-Integration-Id'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('CherryAI provider configuration', () => {
|
||||
beforeEach(() => {
|
||||
;(globalThis as any).window = {
|
||||
...(globalThis as any).window,
|
||||
keyv: createWindowKeyv()
|
||||
}
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('formats CherryAI provider apiHost with false parameter', () => {
|
||||
const provider = createCherryAIProvider()
|
||||
const model = createModel('gpt-4', 'GPT-4', 'cherryai')
|
||||
|
||||
// Mock the functions to simulate CherryAI provider detection
|
||||
vi.mocked(isCherryAIProvider).mockReturnValue(true)
|
||||
vi.mocked(getProviderByModel).mockReturnValue(provider)
|
||||
|
||||
// Call getActualProvider which should trigger formatProviderApiHost
|
||||
const actualProvider = getActualProvider(model)
|
||||
|
||||
// Verify that formatApiHost was called with false as the second parameter
|
||||
expect(formatApiHost).toHaveBeenCalledWith('https://api.cherryai.com', false)
|
||||
expect(actualProvider.apiHost).toBe('https://api.cherryai.com')
|
||||
})
|
||||
|
||||
it('does not format non-CherryAI provider with false parameter', () => {
|
||||
const provider = {
|
||||
id: 'openai',
|
||||
type: 'openai',
|
||||
name: 'OpenAI',
|
||||
apiKey: 'test-key',
|
||||
apiHost: 'https://api.openai.com',
|
||||
models: [],
|
||||
isSystem: false
|
||||
} as Provider
|
||||
const model = createModel('gpt-4', 'GPT-4', 'openai')
|
||||
|
||||
// Mock the functions to simulate non-CherryAI provider
|
||||
vi.mocked(isCherryAIProvider).mockReturnValue(false)
|
||||
vi.mocked(getProviderByModel).mockReturnValue(provider)
|
||||
|
||||
// Call getActualProvider
|
||||
const actualProvider = getActualProvider(model)
|
||||
|
||||
// Verify that formatApiHost was called with default parameters (true)
|
||||
expect(formatApiHost).toHaveBeenCalledWith('https://api.openai.com')
|
||||
expect(actualProvider.apiHost).toBe('https://api.openai.com/v1')
|
||||
})
|
||||
|
||||
it('handles CherryAI provider with empty apiHost', () => {
|
||||
const provider = createCherryAIProvider()
|
||||
provider.apiHost = ''
|
||||
const model = createModel('gpt-4', 'GPT-4', 'cherryai')
|
||||
|
||||
vi.mocked(isCherryAIProvider).mockReturnValue(true)
|
||||
vi.mocked(getProviderByModel).mockReturnValue(provider)
|
||||
|
||||
const actualProvider = getActualProvider(model)
|
||||
|
||||
expect(formatApiHost).toHaveBeenCalledWith('', false)
|
||||
expect(actualProvider.apiHost).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,6 +9,7 @@ import { isOpenAIChatCompletionOnlyModel } from '@renderer/config/models'
|
||||
import {
|
||||
isAnthropicProvider,
|
||||
isAzureOpenAIProvider,
|
||||
isCherryAIProvider,
|
||||
isGeminiProvider,
|
||||
isNewApiProvider
|
||||
} from '@renderer/config/providers'
|
||||
@@ -100,6 +101,8 @@ function formatProviderApiHost(provider: Provider): Provider {
|
||||
formatted.apiHost = formatAzureOpenAIApiHost(formatted.apiHost)
|
||||
} else if (isVertexProvider(formatted)) {
|
||||
formatted.apiHost = formatVertexApiHost(formatted)
|
||||
} else if (isCherryAIProvider(formatted)) {
|
||||
formatted.apiHost = formatApiHost(formatted.apiHost, false)
|
||||
} else {
|
||||
formatted.apiHost = formatApiHost(formatted.apiHost)
|
||||
}
|
||||
|
||||
@@ -1486,6 +1486,10 @@ export const isNewApiProvider = (provider: Provider) => {
|
||||
return ['new-api', 'cherryin'].includes(provider.id) || provider.type === 'new-api'
|
||||
}
|
||||
|
||||
export function isCherryAIProvider(provider: Provider): boolean {
|
||||
return provider.id === 'cherryai'
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为 OpenAI 兼容的提供商
|
||||
* @param {Provider} provider 提供商对象
|
||||
|
||||
@@ -1097,7 +1097,6 @@
|
||||
"name": "Name",
|
||||
"no_results": "Keine Ergebnisse",
|
||||
"none": "Keine",
|
||||
"off": "Aus",
|
||||
"open": "Öffnen",
|
||||
"paste": "Einfügen",
|
||||
"placeholders": {
|
||||
@@ -4144,6 +4143,7 @@
|
||||
"default": "Standard",
|
||||
"flex": "Flexibel",
|
||||
"on_demand": "Auf Anfrage",
|
||||
"performance": "Leistung",
|
||||
"priority": "Priorität",
|
||||
"tip": "Latenz-Ebene für Anfrageverarbeitung festlegen",
|
||||
"title": "Service-Tier"
|
||||
@@ -4260,7 +4260,7 @@
|
||||
"aws-bedrock": {
|
||||
"access_key_id": "AWS-Zugriffsschlüssel-ID",
|
||||
"access_key_id_help": "Ihre AWS-Zugriffsschlüssel-ID, um auf AWS Bedrock-Dienste zuzugreifen",
|
||||
"api_key": "Bedrock API-Schlüssel",
|
||||
"api_key": "Bedrock-API-Schlüssel",
|
||||
"api_key_help": "Ihr AWS Bedrock-API-Schlüssel für die Authentifizierung",
|
||||
"auth_type": "Authentifizierungstyp",
|
||||
"auth_type_api_key": "Bedrock-API-Schlüssel",
|
||||
|
||||
@@ -1097,7 +1097,6 @@
|
||||
"name": "Όνομα",
|
||||
"no_results": "Δεν βρέθηκαν αποτελέσματα",
|
||||
"none": "Χωρίς",
|
||||
"off": "Απενεργοποιημένο",
|
||||
"open": "Άνοιγμα",
|
||||
"paste": "Επικόλληση",
|
||||
"placeholders": {
|
||||
@@ -4144,6 +4143,7 @@
|
||||
"default": "Προεπιλογή",
|
||||
"flex": "Εύκαμπτο",
|
||||
"on_demand": "κατά παραγγελία",
|
||||
"performance": "Απόδοση",
|
||||
"priority": "προτεραιότητα",
|
||||
"tip": "Καθορίστε το επίπεδο καθυστέρησης που χρησιμοποιείται για την επεξεργασία των αιτημάτων",
|
||||
"title": "Επίπεδο υπηρεσίας"
|
||||
@@ -4261,7 +4261,7 @@
|
||||
"access_key_id": "Αναγνωριστικό κλειδιού πρόσβασης AWS",
|
||||
"access_key_id_help": "Το ID του κλειδιού πρόσβασης AWS που χρησιμοποιείται για την πρόσβαση στην υπηρεσία AWS Bedrock",
|
||||
"api_key": "Κλειδί API Bedrock",
|
||||
"api_key_help": "Το κλειδί API σας για τον AWS Bedrock για πιστοποίηση",
|
||||
"api_key_help": "Το κλειδί API του AWS Bedrock για έλεγχο ταυτότητας",
|
||||
"auth_type": "Τύπος Πιστοποίησης",
|
||||
"auth_type_api_key": "Κλειδί API Bedrock",
|
||||
"auth_type_help": "Επιλέξτε μεταξύ πιστοποιητικών IAM ή πιστοποίησης με κλειδί API Bedrock",
|
||||
|
||||
@@ -1097,7 +1097,6 @@
|
||||
"name": "Nombre",
|
||||
"no_results": "Sin resultados",
|
||||
"none": "无",
|
||||
"off": "Apagado",
|
||||
"open": "Abrir",
|
||||
"paste": "Pegar",
|
||||
"placeholders": {
|
||||
@@ -4144,6 +4143,7 @@
|
||||
"default": "Predeterminado",
|
||||
"flex": "Flexible",
|
||||
"on_demand": "según demanda",
|
||||
"performance": "rendimiento",
|
||||
"priority": "prioridad",
|
||||
"tip": "Especifica el nivel de latencia utilizado para procesar la solicitud",
|
||||
"title": "Nivel de servicio"
|
||||
@@ -4261,10 +4261,10 @@
|
||||
"access_key_id": "ID de clave de acceso de AWS",
|
||||
"access_key_id_help": "Su ID de clave de acceso de AWS, utilizado para acceder al servicio AWS Bedrock",
|
||||
"api_key": "Clave de API de Bedrock",
|
||||
"api_key_help": "Tu clave de API de AWS Bedrock para la autenticación",
|
||||
"api_key_help": "Tu clave de API de AWS Bedrock para autenticación",
|
||||
"auth_type": "Tipo de autenticación",
|
||||
"auth_type_api_key": "Clave de API de Bedrock",
|
||||
"auth_type_help": "Elige entre credenciales IAM o autenticación con clave de API de Bedrock",
|
||||
"auth_type_help": "Elige entre credenciales IAM o autenticación con clave API de Bedrock",
|
||||
"auth_type_iam": "Credenciales de IAM",
|
||||
"description": "AWS Bedrock es un servicio de modelos fundamentales completamente gestionado proporcionado por Amazon, que admite diversos modelos avanzados de lenguaje de gran tamaño.",
|
||||
"region": "Región de AWS",
|
||||
|
||||
@@ -1097,7 +1097,6 @@
|
||||
"name": "Nom",
|
||||
"no_results": "Aucun résultat",
|
||||
"none": "Aucun",
|
||||
"off": "Arrêt",
|
||||
"open": "Ouvrir",
|
||||
"paste": "Coller",
|
||||
"placeholders": {
|
||||
@@ -4144,6 +4143,7 @@
|
||||
"default": "Par défaut",
|
||||
"flex": "Flexible",
|
||||
"on_demand": "à la demande",
|
||||
"performance": "performance",
|
||||
"priority": "priorité",
|
||||
"tip": "Spécifie le niveau de latence utilisé pour traiter la demande",
|
||||
"title": "Niveau de service"
|
||||
@@ -4265,7 +4265,7 @@
|
||||
"auth_type": "Type d'authentification",
|
||||
"auth_type_api_key": "Clé API Bedrock",
|
||||
"auth_type_help": "Choisissez entre l'authentification par identifiants IAM ou par clé API Bedrock",
|
||||
"auth_type_iam": "Informations d'identification IAM",
|
||||
"auth_type_iam": "Identifiants IAM",
|
||||
"description": "AWS Bedrock est un service de modèles de base entièrement géré proposé par Amazon, prenant en charge divers grands modèles linguistiques avancés.",
|
||||
"region": "Région AWS",
|
||||
"region_help": "Votre région de service AWS, par exemple us-east-1",
|
||||
|
||||
@@ -1097,7 +1097,6 @@
|
||||
"name": "名前",
|
||||
"no_results": "検索結果なし",
|
||||
"none": "無",
|
||||
"off": "オフ",
|
||||
"open": "開く",
|
||||
"paste": "貼り付け",
|
||||
"placeholders": {
|
||||
@@ -4144,6 +4143,7 @@
|
||||
"default": "デフォルト",
|
||||
"flex": "フレックス",
|
||||
"on_demand": "オンデマンド",
|
||||
"performance": "性能",
|
||||
"priority": "優先",
|
||||
"tip": "リクエスト処理に使用するレイテンシティアを指定します",
|
||||
"title": "サービスティア"
|
||||
@@ -4263,7 +4263,7 @@
|
||||
"api_key": "Bedrock APIキー",
|
||||
"api_key_help": "認証用のAWS Bedrock APIキー",
|
||||
"auth_type": "認証タイプ",
|
||||
"auth_type_api_key": "Bedrock API キー",
|
||||
"auth_type_api_key": "Bedrock APIキー",
|
||||
"auth_type_help": "IAM認証情報とBedrock APIキー認証のどちらかを選択してください",
|
||||
"auth_type_iam": "IAM認証情報",
|
||||
"description": "AWS Bedrock は、Amazon が提供する完全に管理されたベースモデルサービスで、さまざまな最先端の大言語モデルをサポートしています",
|
||||
|
||||
@@ -1097,7 +1097,6 @@
|
||||
"name": "Имя",
|
||||
"no_results": "Результатов не найдено",
|
||||
"none": "без",
|
||||
"off": "Выкл",
|
||||
"open": "Открыть",
|
||||
"paste": "Вставить",
|
||||
"placeholders": {
|
||||
@@ -4144,6 +4143,7 @@
|
||||
"default": "По умолчанию",
|
||||
"flex": "Гибкий",
|
||||
"on_demand": "по требованию",
|
||||
"performance": "производительность",
|
||||
"priority": "приоритет",
|
||||
"tip": "Указывает уровень задержки, который следует использовать для обработки запроса",
|
||||
"title": "Уровень сервиса"
|
||||
@@ -4261,7 +4261,7 @@
|
||||
"access_key_id": "AWS Ключ доступа ID",
|
||||
"access_key_id_help": "Ваш AWS Ключ доступа ID для доступа к AWS Bedrock",
|
||||
"api_key": "Ключ API Bedrock",
|
||||
"api_key_help": "Ваш API-ключ AWS Bedrock для аутентификации",
|
||||
"api_key_help": "Ваш ключ API AWS Bedrock для аутентификации",
|
||||
"auth_type": "Тип аутентификации",
|
||||
"auth_type_api_key": "Ключ API Bedrock",
|
||||
"auth_type_help": "Выберите между аутентификацией с помощью учетных данных IAM или ключа API Bedrock",
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
import type { ButtonProps } from '@heroui/react'
|
||||
import { Button, cn } from '@heroui/react'
|
||||
import { PlusIcon } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
|
||||
interface Props extends ButtonProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const AddButton: FC<Props> = ({ children, className, ...props }) => {
|
||||
const AddButton = ({ children, className, ...props }: ButtonProps) => {
|
||||
return (
|
||||
<Button
|
||||
{...props}
|
||||
onPress={props.onPress}
|
||||
className={cn(
|
||||
'h-9 w-[calc(var(--assistants-width)-20px)] justify-start rounded-lg bg-transparent px-3 text-[13px] text-[var(--color-text-2)] hover:bg-[var(--color-list-item)]',
|
||||
className
|
||||
)}
|
||||
startContent={<PlusIcon size={16} className="shrink-0" />}>
|
||||
startContent={<PlusIcon size={16} className="shrink-0" />}
|
||||
{...props}>
|
||||
{children}
|
||||
</Button>
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { cn } from '@heroui/react'
|
||||
import { DeleteIcon, EditIcon } from '@renderer/components/Icons'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
|
||||
@@ -19,14 +20,14 @@ import {
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuTrigger
|
||||
} from '@renderer/ui/context-menu'
|
||||
import { classNames } from '@renderer/utils'
|
||||
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
|
||||
import { Tooltip } from 'antd'
|
||||
import { MenuIcon, XIcon } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import React, { memo, startTransition, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { ListItem, ListItemEditInput, ListItemName, ListItemNameContainer, MenuButton, StatusIndicator } from './shared'
|
||||
|
||||
// const logger = loggerService.withContext('AgentItem')
|
||||
|
||||
@@ -67,7 +68,6 @@ const SessionItem: FC<SessionItemProps> = ({ session, agentId, onDelete, onPress
|
||||
</div>
|
||||
}>
|
||||
<MenuButton
|
||||
className="menu"
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (isConfirmingDeletion || e.ctrlKey || e.metaKey) {
|
||||
@@ -115,20 +115,21 @@ const SessionItem: FC<SessionItemProps> = ({ session, agentId, onDelete, onPress
|
||||
<>
|
||||
<ContextMenu modal={false}>
|
||||
<ContextMenuTrigger>
|
||||
<SessionListItem
|
||||
className={classNames(isActive ? 'active' : '', singlealone ? 'singlealone' : '')}
|
||||
<ListItem
|
||||
className={cn(
|
||||
isActive ? 'active' : undefined,
|
||||
singlealone ? 'singlealone' : undefined,
|
||||
isEditing ? 'cursor-default' : 'cursor-pointer',
|
||||
'rounded-[var(--list-item-border-radius)]'
|
||||
)}
|
||||
onClick={isEditing ? undefined : onPress}
|
||||
onDoubleClick={() => startEdit(session.name ?? '')}
|
||||
title={session.name ?? session.id}
|
||||
style={{
|
||||
borderRadius: 'var(--list-item-border-radius)',
|
||||
cursor: isEditing ? 'default' : 'pointer'
|
||||
}}>
|
||||
{isPending && !isActive && <PendingIndicator />}
|
||||
{isFulfilled && !isActive && <FulfilledIndicator />}
|
||||
<SessionNameContainer>
|
||||
title={session.name ?? session.id}>
|
||||
{isPending && !isActive && <StatusIndicator variant="pending" />}
|
||||
{isFulfilled && !isActive && <StatusIndicator variant="fulfilled" />}
|
||||
<ListItemNameContainer>
|
||||
{isEditing ? (
|
||||
<SessionEditInput
|
||||
<ListItemEditInput
|
||||
ref={inputRef}
|
||||
value={editValue}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleValueChange(e.target.value)}
|
||||
@@ -138,14 +139,14 @@ const SessionItem: FC<SessionItemProps> = ({ session, agentId, onDelete, onPress
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<SessionName>
|
||||
<ListItemName>
|
||||
<SessionLabel session={session} />
|
||||
</SessionName>
|
||||
</ListItemName>
|
||||
<DeleteButton />
|
||||
</>
|
||||
)}
|
||||
</SessionNameContainer>
|
||||
</SessionListItem>
|
||||
</ListItemNameContainer>
|
||||
</ListItem>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem
|
||||
@@ -188,121 +189,4 @@ const SessionItem: FC<SessionItemProps> = ({ session, agentId, onDelete, onPress
|
||||
)
|
||||
}
|
||||
|
||||
const SessionListItem = styled.div`
|
||||
padding: 7px 12px;
|
||||
border-radius: var(--list-item-border-radius);
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
width: calc(var(--assistants-width) - 20px);
|
||||
margin-bottom: 8px;
|
||||
|
||||
.menu {
|
||||
opacity: 0;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-list-item-hover);
|
||||
transition: background-color 0.1s;
|
||||
|
||||
.menu {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--color-list-item);
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
.menu {
|
||||
opacity: 1;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.singlealone {
|
||||
border-radius: 0 !important;
|
||||
&:hover {
|
||||
background-color: var(--color-background-soft);
|
||||
}
|
||||
&.active {
|
||||
border-left: 2px solid var(--color-primary);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const SessionNameContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
height: 20px;
|
||||
justify-content: space-between;
|
||||
`
|
||||
|
||||
const SessionName = styled.div`
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
position: relative;
|
||||
`
|
||||
|
||||
const SessionEditInput = styled.input`
|
||||
background: var(--color-background);
|
||||
border: none;
|
||||
color: var(--color-text-1);
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
padding: 2px 6px;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
`
|
||||
|
||||
const MenuButton = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: 20px;
|
||||
min-height: 20px;
|
||||
.anticon {
|
||||
font-size: 12px;
|
||||
}
|
||||
`
|
||||
|
||||
const PendingIndicator = styled.div.attrs({
|
||||
className: 'animation-pulse'
|
||||
})`
|
||||
--pulse-size: 5px;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
position: absolute;
|
||||
left: 3px;
|
||||
top: 15px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-status-warning);
|
||||
`
|
||||
|
||||
const FulfilledIndicator = styled.div.attrs({
|
||||
className: 'animation-pulse'
|
||||
})`
|
||||
--pulse-size: 5px;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
position: absolute;
|
||||
left: 3px;
|
||||
top: 15px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-status-success);
|
||||
`
|
||||
|
||||
export default memo(SessionItem)
|
||||
|
||||
@@ -17,6 +17,7 @@ import { useTranslation } from 'react-i18next'
|
||||
|
||||
import AddButton from './AddButton'
|
||||
import SessionItem from './SessionItem'
|
||||
import { ListContainer } from './shared'
|
||||
|
||||
// const logger = loggerService.withContext('SessionsTab')
|
||||
|
||||
@@ -95,7 +96,7 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
||||
if (error) return <Alert color="danger" content={t('agent.session.get.error.failed')} />
|
||||
|
||||
return (
|
||||
<div className="sessions-tab flex h-full w-full flex-col p-2">
|
||||
<ListContainer className="sessions-tab">
|
||||
<AddButton onPress={createDefaultSession} className="mb-2" isDisabled={creatingSession}>
|
||||
{t('agent.session.add.title')}
|
||||
</AddButton>
|
||||
@@ -118,7 +119,7 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
||||
/>
|
||||
)}
|
||||
</DynamicVirtualList>
|
||||
</div>
|
||||
</ListContainer>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { cn } from '@heroui/react'
|
||||
import { DraggableVirtualList } from '@renderer/components/DraggableList'
|
||||
import { CopyIcon, DeleteIcon, EditIcon } from '@renderer/components/Icons'
|
||||
import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup'
|
||||
@@ -17,7 +18,7 @@ import store from '@renderer/store'
|
||||
import { newMessagesActions } from '@renderer/store/newMessage'
|
||||
import { setGenerating } from '@renderer/store/runtime'
|
||||
import type { Assistant, Topic } from '@renderer/types'
|
||||
import { classNames, removeSpecialCharactersForFileName } from '@renderer/utils'
|
||||
import { removeSpecialCharactersForFileName } from '@renderer/utils'
|
||||
import { copyTopicAsMarkdown, copyTopicAsPlainText } from '@renderer/utils/copy'
|
||||
import {
|
||||
exportMarkdownToJoplin,
|
||||
@@ -53,6 +54,15 @@ import { useDispatch, useSelector } from 'react-redux'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import AddButton from './AddButton'
|
||||
import {
|
||||
ListContainer,
|
||||
ListItem,
|
||||
ListItemEditInput,
|
||||
ListItemName,
|
||||
ListItemNameContainer,
|
||||
MenuButton,
|
||||
StatusIndicator
|
||||
} from './shared'
|
||||
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
@@ -73,8 +83,6 @@ export const Topics: React.FC<Props> = ({ assistant: _assistant, activeTopic, se
|
||||
const topicFulfilledQuery = useSelector((state: RootState) => state.messages.fulfilledByTopic)
|
||||
const newlyRenamedTopics = useSelector((state: RootState) => state.runtime.chat.newlyRenamedTopics)
|
||||
|
||||
const borderRadius = showTopicTime ? 12 : 'var(--list-item-border-radius)'
|
||||
|
||||
const [deletingTopicId, setDeletingTopicId] = useState<string | null>(null)
|
||||
const deleteTimerRef = useRef<NodeJS.Timeout>(null)
|
||||
const [editingTopicId, setEditingTopicId] = useState<string | null>(null)
|
||||
@@ -489,252 +497,107 @@ export const Topics: React.FC<Props> = ({ assistant: _assistant, activeTopic, se
|
||||
const singlealone = topicPosition === 'right' && position === 'right'
|
||||
|
||||
return (
|
||||
<DraggableVirtualList
|
||||
className="topics-tab"
|
||||
list={sortedTopics}
|
||||
onUpdate={updateTopics}
|
||||
style={{ height: '100%', padding: '11px 0 10px 10px' }}
|
||||
itemContainerStyle={{ paddingBottom: '8px' }}
|
||||
header={
|
||||
<AddButton onPress={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)} className="mb-2">
|
||||
{t('chat.add.topic.title')}
|
||||
</AddButton>
|
||||
}>
|
||||
{(topic) => {
|
||||
const isActive = topic.id === activeTopic?.id
|
||||
const topicName = topic.name.replace('`', '')
|
||||
const topicPrompt = topic.prompt
|
||||
const fullTopicPrompt = t('common.prompt') + ': ' + topicPrompt
|
||||
<ListContainer className="topics-tab">
|
||||
<AddButton onPress={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)} className="mb-2">
|
||||
{t('chat.add.topic.title')}
|
||||
</AddButton>
|
||||
<DraggableVirtualList list={sortedTopics} onUpdate={updateTopics} className="overflow-y-auto overflow-x-hidden">
|
||||
{(topic) => {
|
||||
const isActive = topic.id === activeTopic?.id
|
||||
const topicName = topic.name.replace('`', '')
|
||||
const topicPrompt = topic.prompt
|
||||
const fullTopicPrompt = t('common.prompt') + ': ' + topicPrompt
|
||||
|
||||
const getTopicNameClassName = () => {
|
||||
if (isRenaming(topic.id)) return 'shimmer'
|
||||
if (isNewlyRenamed(topic.id)) return 'typing'
|
||||
return ''
|
||||
}
|
||||
const getTopicNameClassName = () => {
|
||||
if (isRenaming(topic.id)) return 'shimmer'
|
||||
if (isNewlyRenamed(topic.id)) return 'typing'
|
||||
return ''
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown menu={{ items: getTopicMenuItems }} trigger={['contextMenu']}>
|
||||
<TopicListItem
|
||||
onContextMenu={() => setTargetTopic(topic)}
|
||||
className={classNames(isActive ? 'active' : '', singlealone ? 'singlealone' : '')}
|
||||
onClick={editingTopicId === topic.id && topicEdit.isEditing ? undefined : () => onSwitchTopic(topic)}
|
||||
style={{
|
||||
borderRadius,
|
||||
cursor: editingTopicId === topic.id && topicEdit.isEditing ? 'default' : 'pointer'
|
||||
}}>
|
||||
{isPending(topic.id) && !isActive && <PendingIndicator />}
|
||||
{isFulfilled(topic.id) && !isActive && <FulfilledIndicator />}
|
||||
<TopicNameContainer>
|
||||
{editingTopicId === topic.id && topicEdit.isEditing ? (
|
||||
<TopicEditInput
|
||||
ref={topicEdit.inputRef}
|
||||
value={topicEdit.editValue}
|
||||
onChange={topicEdit.handleInputChange}
|
||||
onKeyDown={topicEdit.handleKeyDown}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
) : (
|
||||
<TopicName
|
||||
className={getTopicNameClassName()}
|
||||
title={topicName}
|
||||
onDoubleClick={() => {
|
||||
setEditingTopicId(topic.id)
|
||||
topicEdit.startEdit(topic.name)
|
||||
}}>
|
||||
{topicName}
|
||||
</TopicName>
|
||||
return (
|
||||
<Dropdown menu={{ items: getTopicMenuItems }} trigger={['contextMenu']}>
|
||||
<ListItem
|
||||
onContextMenu={() => setTargetTopic(topic)}
|
||||
className={cn(
|
||||
isActive ? 'active' : undefined,
|
||||
singlealone ? 'singlealone' : undefined,
|
||||
editingTopicId === topic.id && topicEdit.isEditing ? 'cursor-default' : 'cursor-pointer',
|
||||
showTopicTime ? 'rounded-2xl' : 'rounded-[var(--list-item-border-radius)]'
|
||||
)}
|
||||
{!topic.pinned && (
|
||||
<Tooltip
|
||||
placement="bottom"
|
||||
mouseEnterDelay={0.7}
|
||||
mouseLeaveDelay={0}
|
||||
title={
|
||||
<div style={{ fontSize: '12px', opacity: 0.8, fontStyle: 'italic' }}>
|
||||
{t('chat.topics.delete.shortcut', { key: isMac ? '⌘' : 'Ctrl' })}
|
||||
</div>
|
||||
}>
|
||||
<MenuButton
|
||||
className="menu"
|
||||
onClick={(e) => {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
handleConfirmDelete(topic, e)
|
||||
} else if (deletingTopicId === topic.id) {
|
||||
handleConfirmDelete(topic, e)
|
||||
} else {
|
||||
handleDeleteClick(topic.id, e)
|
||||
}
|
||||
onClick={editingTopicId === topic.id && topicEdit.isEditing ? undefined : () => onSwitchTopic(topic)}>
|
||||
{isPending(topic.id) && !isActive && <StatusIndicator variant="pending" />}
|
||||
{isFulfilled(topic.id) && !isActive && <StatusIndicator variant="fulfilled" />}
|
||||
<ListItemNameContainer>
|
||||
{editingTopicId === topic.id && topicEdit.isEditing ? (
|
||||
<ListItemEditInput
|
||||
ref={topicEdit.inputRef}
|
||||
value={topicEdit.editValue}
|
||||
onChange={topicEdit.handleInputChange}
|
||||
onKeyDown={topicEdit.handleKeyDown}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
) : (
|
||||
<ListItemName
|
||||
className={getTopicNameClassName()}
|
||||
title={topicName}
|
||||
onDoubleClick={() => {
|
||||
setEditingTopicId(topic.id)
|
||||
topicEdit.startEdit(topic.name)
|
||||
}}>
|
||||
{deletingTopicId === topic.id ? (
|
||||
<DeleteIcon size={14} color="var(--color-error)" style={{ pointerEvents: 'none' }} />
|
||||
) : (
|
||||
<XIcon size={14} color="var(--color-text-3)" style={{ pointerEvents: 'none' }} />
|
||||
)}
|
||||
{topicName}
|
||||
</ListItemName>
|
||||
)}
|
||||
{!topic.pinned && (
|
||||
<Tooltip
|
||||
placement="bottom"
|
||||
mouseEnterDelay={0.7}
|
||||
mouseLeaveDelay={0}
|
||||
title={
|
||||
<div style={{ fontSize: '12px', opacity: 0.8, fontStyle: 'italic' }}>
|
||||
{t('chat.topics.delete.shortcut', { key: isMac ? '⌘' : 'Ctrl' })}
|
||||
</div>
|
||||
}>
|
||||
<MenuButton
|
||||
onClick={(e) => {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
handleConfirmDelete(topic, e)
|
||||
} else if (deletingTopicId === topic.id) {
|
||||
handleConfirmDelete(topic, e)
|
||||
} else {
|
||||
handleDeleteClick(topic.id, e)
|
||||
}
|
||||
}}>
|
||||
{deletingTopicId === topic.id ? (
|
||||
<DeleteIcon size={14} color="var(--color-error)" style={{ pointerEvents: 'none' }} />
|
||||
) : (
|
||||
<XIcon size={14} color="var(--color-text-3)" style={{ pointerEvents: 'none' }} />
|
||||
)}
|
||||
</MenuButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{topic.pinned && (
|
||||
<MenuButton className="pin">
|
||||
<PinIcon size={14} color="var(--color-text-3)" />
|
||||
</MenuButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</ListItemNameContainer>
|
||||
{topicPrompt && (
|
||||
<TopicPromptText className="prompt" title={fullTopicPrompt}>
|
||||
{fullTopicPrompt}
|
||||
</TopicPromptText>
|
||||
)}
|
||||
{topic.pinned && (
|
||||
<MenuButton className="pin">
|
||||
<PinIcon size={14} color="var(--color-text-3)" />
|
||||
</MenuButton>
|
||||
{showTopicTime && (
|
||||
<TopicTime className="time">{dayjs(topic.createdAt).format('MM/DD HH:mm')}</TopicTime>
|
||||
)}
|
||||
</TopicNameContainer>
|
||||
{topicPrompt && (
|
||||
<TopicPromptText className="prompt" title={fullTopicPrompt}>
|
||||
{fullTopicPrompt}
|
||||
</TopicPromptText>
|
||||
)}
|
||||
{showTopicTime && <TopicTime className="time">{dayjs(topic.createdAt).format('MM/DD HH:mm')}</TopicTime>}
|
||||
</TopicListItem>
|
||||
</Dropdown>
|
||||
)
|
||||
}}
|
||||
</DraggableVirtualList>
|
||||
</ListItem>
|
||||
</Dropdown>
|
||||
)
|
||||
}}
|
||||
</DraggableVirtualList>
|
||||
</ListContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const TopicListItem = styled.div`
|
||||
padding: 7px 12px;
|
||||
border-radius: var(--list-item-border-radius);
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
width: calc(var(--assistants-width) - 20px);
|
||||
|
||||
.menu {
|
||||
opacity: 0;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-list-item-hover);
|
||||
transition: background-color 0.1s;
|
||||
|
||||
.menu {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--color-list-item);
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
.menu {
|
||||
opacity: 1;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
}
|
||||
}
|
||||
&.singlealone {
|
||||
border-radius: 0 !important;
|
||||
&:hover {
|
||||
background-color: var(--color-background-soft);
|
||||
}
|
||||
&.active {
|
||||
border-left: 2px solid var(--color-primary);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const TopicNameContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
height: 20px;
|
||||
justify-content: space-between;
|
||||
`
|
||||
|
||||
const TopicName = styled.div`
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
position: relative;
|
||||
will-change: background-position, width;
|
||||
|
||||
--color-shimmer-mid: var(--color-text-1);
|
||||
--color-shimmer-end: color-mix(in srgb, var(--color-text-1) 25%, transparent);
|
||||
|
||||
&.shimmer {
|
||||
background: linear-gradient(to left, var(--color-shimmer-end), var(--color-shimmer-mid), var(--color-shimmer-end));
|
||||
background-size: 200% 100%;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
animation: shimmer 3s linear infinite;
|
||||
}
|
||||
|
||||
&.typing {
|
||||
display: block;
|
||||
-webkit-line-clamp: unset;
|
||||
-webkit-box-orient: unset;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
animation: typewriter 0.5s steps(40, end);
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes typewriter {
|
||||
from {
|
||||
width: 0;
|
||||
}
|
||||
to {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const TopicEditInput = styled.input`
|
||||
background: var(--color-background);
|
||||
border: none;
|
||||
color: var(--color-text-1);
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
padding: 2px 6px;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
`
|
||||
|
||||
const PendingIndicator = styled.div.attrs({
|
||||
className: 'animation-pulse'
|
||||
})`
|
||||
--pulse-size: 5px;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
position: absolute;
|
||||
left: 3px;
|
||||
top: 15px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-status-warning);
|
||||
`
|
||||
|
||||
const FulfilledIndicator = styled.div.attrs({
|
||||
className: 'animation-pulse'
|
||||
})`
|
||||
--pulse-size: 5px;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
position: absolute;
|
||||
left: 3px;
|
||||
top: 15px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-status-success);
|
||||
`
|
||||
|
||||
const TopicPromptText = styled.div`
|
||||
color: var(--color-text-2);
|
||||
font-size: 12px;
|
||||
@@ -751,15 +614,3 @@ const TopicTime = styled.div`
|
||||
color: var(--color-text-3);
|
||||
font-size: 11px;
|
||||
`
|
||||
|
||||
const MenuButton = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: 20px;
|
||||
min-height: 20px;
|
||||
.anticon {
|
||||
font-size: 12px;
|
||||
}
|
||||
`
|
||||
|
||||
131
src/renderer/src/pages/home/Tabs/components/shared.tsx
Normal file
131
src/renderer/src/pages/home/Tabs/components/shared.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import { cn } from '@heroui/react'
|
||||
import type { ComponentPropsWithoutRef, ComponentPropsWithRef } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const ListItem = ({ children, className, ...props }: ComponentPropsWithoutRef<'div'>) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'mb-2 flex w-[calc(var(--assistants-width)-20px)] cursor-pointer flex-col justify-between rounded-lg px-3 py-2 text-sm',
|
||||
'transition-colors duration-100',
|
||||
'hover:bg-[var(--color-list-item-hover)]',
|
||||
'[.active]:bg-[var(--color-list-item)] [.active]:shadow-[0_1px_2px_0_rgba(0,0,0,0.05)]',
|
||||
'[&_.menu]:text-[var(--color-text-3)] [&_.menu]:opacity-0',
|
||||
'hover:[&_.menu]:opacity-100',
|
||||
'[.active]:[&_.menu]:opacity-100 [.active]:[&_.menu]:hover:text-[var(--color-text-2)]',
|
||||
'[.singlealone.active]:border-[var(--color-primary)] [.singlealone.active]:shadow-none [.singlealone]:rounded-none [.singlealone]:border-transparent [.singlealone]:border-l-2 [.singlealone]:hover:bg-[var(--color-background-soft)]',
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export const ListItemNameContainer = ({ children, className, ...props }: ComponentPropsWithoutRef<'div'>) => {
|
||||
return (
|
||||
<div className={cn('flex h-5 flex-row items-center justify-between gap-1', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// This component involves complex animations and will not be migrated for now.
|
||||
export const ListItemName = styled.div`
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
will-change: background-position, width;
|
||||
|
||||
--color-shimmer-mid: var(--color-text-1);
|
||||
--color-shimmer-end: color-mix(in srgb, var(--color-text-1) 25%, transparent);
|
||||
|
||||
&.shimmer {
|
||||
background: linear-gradient(to left, var(--color-shimmer-end), var(--color-shimmer-mid), var(--color-shimmer-end));
|
||||
background-size: 200% 100%;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
animation: shimmer 3s linear infinite;
|
||||
}
|
||||
|
||||
&.typing {
|
||||
display: block;
|
||||
-webkit-line-clamp: unset;
|
||||
-webkit-box-orient: unset;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
animation: typewriter 0.5s steps(40, end);
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes typewriter {
|
||||
from {
|
||||
width: 0;
|
||||
}
|
||||
to {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const ListItemEditInput = ({ className, ...props }: ComponentPropsWithRef<'input'>) => {
|
||||
return (
|
||||
<input
|
||||
className={cn(
|
||||
'w-full border-none bg-[var(--color-background)] p-0 font-inherit text-[var(--color-text-1)] text-sm outline-none',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const ListContainer = ({ children, className, ...props }: ComponentPropsWithoutRef<'div'>) => {
|
||||
return (
|
||||
<div className={cn('flex h-full w-full flex-col p-2', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const MenuButton = ({ children, className, ...props }: ComponentPropsWithoutRef<'div'>) => {
|
||||
return (
|
||||
<div className={cn('menu', 'flex min-h-5 min-w-5 flex-row items-center justify-center', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const StatusIndicator = ({ variant }: { variant: 'pending' | 'fulfilled' }) => {
|
||||
const colors = useMemo(() => {
|
||||
switch (variant) {
|
||||
case 'pending':
|
||||
return {
|
||||
wave: 'bg-warning-400',
|
||||
back: 'bg-warning-500'
|
||||
}
|
||||
case 'fulfilled':
|
||||
return {
|
||||
wave: 'bg-success-400',
|
||||
back: 'bg-success-500'
|
||||
}
|
||||
}
|
||||
}, [variant])
|
||||
return (
|
||||
<div className="absolute top-4 left-1 flex size-1">
|
||||
<span className={cn('absolute inline-flex h-full w-full animate-ping rounded-full opacity-75', colors.wave)} />
|
||||
<span className={cn('relative inline-flex size-1 rounded-full bg-warning-500', colors.back)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -96,11 +96,14 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
return editImageFiles
|
||||
}, [editImageFiles])
|
||||
|
||||
const updatePaintingState = (updates: Partial<PaintingAction>) => {
|
||||
const updatedPainting = { ...painting, providerId: newApiProvider.id, ...updates }
|
||||
setPainting(updatedPainting)
|
||||
updatePainting(mode, updatedPainting)
|
||||
}
|
||||
const updatePaintingState = useCallback(
|
||||
(updates: Partial<PaintingAction>) => {
|
||||
const updatedPainting = { ...painting, providerId: newApiProvider.id, ...updates }
|
||||
setPainting(updatedPainting)
|
||||
updatePainting(mode, updatedPainting)
|
||||
},
|
||||
[painting, newApiProvider.id, mode, updatePainting]
|
||||
)
|
||||
|
||||
// ---------------- Model Related Configurations ----------------
|
||||
// const modelOptions = MODELS.map((m) => ({ label: m.name, value: m.name }))
|
||||
|
||||
88
yarn.lock
88
yarn.lock
@@ -6787,6 +6787,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@paymoapp/electron-shutdown-handler@npm:^1.1.2":
|
||||
version: 1.1.2
|
||||
resolution: "@paymoapp/electron-shutdown-handler@npm:1.1.2"
|
||||
dependencies:
|
||||
node-addon-api: "npm:^5.0.0"
|
||||
node-gyp: "npm:latest"
|
||||
prebuild-install: "npm:^7.1.2"
|
||||
checksum: 10c0/c774ded900870cd0eae79f2281e971561328b9d2f555b8763a75773f12d953edfa3923067f257bbda5001f9c934343d55344f7a7aac5eff8739762c91e9f37a7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@pdf-lib/standard-fonts@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@pdf-lib/standard-fonts@npm:1.0.0"
|
||||
@@ -9673,25 +9684,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@smithy/types@npm:^4.3.1":
|
||||
version: 4.3.1
|
||||
resolution: "@smithy/types@npm:4.3.1"
|
||||
dependencies:
|
||||
tslib: "npm:^2.6.2"
|
||||
checksum: 10c0/8b350562b9ed4ff97465025b4ae77a34bb07b9d47fb6f9781755aac9401b0355a63c2fef307393e2dae3fa0277149dd7d83f5bc2a63d4ad3519ea32fd56b5cda
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@smithy/types@npm:^4.3.2":
|
||||
version: 4.3.2
|
||||
resolution: "@smithy/types@npm:4.3.2"
|
||||
dependencies:
|
||||
tslib: "npm:^2.6.2"
|
||||
checksum: 10c0/120c5d38f6362c86e6493cce3b9ca9902cd986dab773b39664ff6a95b787c45481f1b1d230f45a6f5ad0c045fb690dc96b51b9ca7b5e9487714a652ed98231f6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@smithy/types@npm:^4.7.1":
|
||||
"@smithy/types@npm:4.7.1":
|
||||
version: 4.7.1
|
||||
resolution: "@smithy/types@npm:4.7.1"
|
||||
dependencies:
|
||||
@@ -12816,6 +12809,7 @@ __metadata:
|
||||
"@opentelemetry/sdk-trace-node": "npm:^2.0.0"
|
||||
"@opentelemetry/sdk-trace-web": "npm:^2.0.0"
|
||||
"@opeoginni/github-copilot-openai-compatible": "npm:0.1.19"
|
||||
"@paymoapp/electron-shutdown-handler": "npm:^1.1.2"
|
||||
"@playwright/test": "npm:^1.52.0"
|
||||
"@radix-ui/react-context-menu": "npm:^2.2.16"
|
||||
"@reduxjs/toolkit": "npm:^2.2.5"
|
||||
@@ -16036,6 +16030,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"detect-libc@npm:^2.0.0":
|
||||
version: 2.1.2
|
||||
resolution: "detect-libc@npm:2.1.2"
|
||||
checksum: 10c0/acc675c29a5649fa1fb6e255f993b8ee829e510b6b56b0910666949c80c364738833417d0edb5f90e4e46be17228b0f2b66a010513984e18b15deeeac49369c4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"detect-libc@npm:^2.0.1":
|
||||
version: 2.0.3
|
||||
resolution: "detect-libc@npm:2.0.3"
|
||||
@@ -22425,6 +22426,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"napi-build-utils@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "napi-build-utils@npm:2.0.0"
|
||||
checksum: 10c0/5833aaeb5cc5c173da47a102efa4680a95842c13e0d9cc70428bd3ee8d96bb2172f8860d2811799b5daa5cbeda779933601492a2028a6a5351c6d0fcf6de83db
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"native-promise-only@npm:0.8.1":
|
||||
version: 0.8.1
|
||||
resolution: "native-promise-only@npm:0.8.1"
|
||||
@@ -22508,6 +22516,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-addon-api@npm:^5.0.0":
|
||||
version: 5.1.0
|
||||
resolution: "node-addon-api@npm:5.1.0"
|
||||
dependencies:
|
||||
node-gyp: "npm:latest"
|
||||
checksum: 10c0/0eb269786124ba6fad9df8007a149e03c199b3e5a3038125dfb3e747c2d5113d406a4e33f4de1ea600aa2339be1f137d55eba1a73ee34e5fff06c52a5c296d1d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-addon-api@npm:^8.4.0":
|
||||
version: 8.4.0
|
||||
resolution: "node-addon-api@npm:8.4.0"
|
||||
@@ -23722,6 +23739,28 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prebuild-install@npm:^7.1.2":
|
||||
version: 7.1.3
|
||||
resolution: "prebuild-install@npm:7.1.3"
|
||||
dependencies:
|
||||
detect-libc: "npm:^2.0.0"
|
||||
expand-template: "npm:^2.0.3"
|
||||
github-from-package: "npm:0.0.0"
|
||||
minimist: "npm:^1.2.3"
|
||||
mkdirp-classic: "npm:^0.5.3"
|
||||
napi-build-utils: "npm:^2.0.0"
|
||||
node-abi: "npm:^3.3.0"
|
||||
pump: "npm:^3.0.0"
|
||||
rc: "npm:^1.2.7"
|
||||
simple-get: "npm:^4.0.0"
|
||||
tar-fs: "npm:^2.0.0"
|
||||
tunnel-agent: "npm:^0.6.0"
|
||||
bin:
|
||||
prebuild-install: bin.js
|
||||
checksum: 10c0/25919a42b52734606a4036ab492d37cfe8b601273d8dfb1fa3c84e141a0a475e7bad3ab848c741d2f810cef892fcf6059b8c7fe5b29f98d30e0c29ad009bedff
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prelude-ls@npm:^1.2.1":
|
||||
version: 1.2.1
|
||||
resolution: "prelude-ls@npm:1.2.1"
|
||||
@@ -26301,6 +26340,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"simple-get@npm:^4.0.0":
|
||||
version: 4.0.1
|
||||
resolution: "simple-get@npm:4.0.1"
|
||||
dependencies:
|
||||
decompress-response: "npm:^6.0.0"
|
||||
once: "npm:^1.3.1"
|
||||
simple-concat: "npm:^1.0.0"
|
||||
checksum: 10c0/b0649a581dbca741babb960423248899203165769747142033479a7dc5e77d7b0fced0253c731cd57cf21e31e4d77c9157c3069f4448d558ebc96cf9e1eebcf0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"simple-swizzle@npm:^0.2.2":
|
||||
version: 0.2.2
|
||||
resolution: "simple-swizzle@npm:0.2.2"
|
||||
|
||||
Reference in New Issue
Block a user