Compare commits
28 Commits
feat/sora2
...
fix/react-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca44133e90 | ||
|
|
b4810bb487 | ||
|
|
dc0f9c5f08 | ||
|
|
595fd878a6 | ||
|
|
9d45991181 | ||
|
|
cf2f2fd707 | ||
|
|
d4b1db0407 | ||
|
|
51dcdf94fb | ||
|
|
254051cf62 | ||
|
|
24d2e6e6ce | ||
|
|
cf9bfce43c | ||
|
|
76bf78b810 | ||
|
|
f4441e2a55 | ||
|
|
84f590ec7b | ||
|
|
a5865cfd01 | ||
|
|
4e7c714ea2 | ||
|
|
d2c4231458 | ||
|
|
b5004e2a51 | ||
|
|
e0c334b5ed | ||
|
|
d482e661fb | ||
|
|
3ac1caca69 | ||
|
|
94c112c066 | ||
|
|
2e694a87f8 | ||
|
|
4ae30db53a | ||
|
|
f4a6dd91cf | ||
|
|
c08a570c27 | ||
|
|
9c318c9526 | ||
|
|
4cee09870a |
@@ -126,112 +126,60 @@ artifactBuildCompleted: scripts/artifact-build-completed.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
<!--LANG:en-->
|
||||
What's New in v1.7.0-beta.1
|
||||
What's New in v1.7.0-beta.2
|
||||
|
||||
Major Features:
|
||||
- Agent System: Introducing intelligent Agent capabilities alongside Assistants. Agents can autonomously solve complex problems using Claude Code SDK with tool calling, file operations, and multi-turn reasoning
|
||||
- Agent Management: Create, configure, and manage agents with custom settings including model selection, tool permissions, accessible paths, and MCP server integrations
|
||||
- Agent Sessions: Dedicated session management for agent interactions with persistent message history and context tracking
|
||||
- Unified UI: Streamlined interface combining Assistants and Agents tabs with improved navigation and settings management
|
||||
New Features:
|
||||
- Session Settings: Manage session-specific settings and model configurations independently
|
||||
- Notes Full-Text Search: Search across all notes with match highlighting
|
||||
- Built-in DiDi MCP Server: Integration with DiDi ride-hailing services (China only)
|
||||
- Intel OV OCR: Hardware-accelerated OCR using Intel NPU
|
||||
- Auto-start API Server: Automatically starts when agents exist
|
||||
|
||||
Agent Features:
|
||||
- Tool Support: Web search, file operations, bash commands, and custom MCP tools
|
||||
- Advanced Configuration: Max turns, temperature, token limits
|
||||
- Permission Control: Configurable tool approval modes (manual, automatic, none)
|
||||
- Session Persistence: Automatic message saving with optimized streaming and database integration
|
||||
- Model Selection: API-based model filtering with provider-specific support
|
||||
|
||||
UI/UX Improvements:
|
||||
- Unified assistant/agent tabs with smooth animations
|
||||
- In-place session name editing
|
||||
- Virtual list rendering for improved performance
|
||||
- Session count indicators for active agents
|
||||
- Enhanced settings popup with tabbed interface
|
||||
- Webview keyboard shortcut interception for search functionality
|
||||
|
||||
API & Infrastructure:
|
||||
- RESTful API for agent and session management
|
||||
- Drizzle ORM integration for agent database
|
||||
- OAuth support for Claude Code authentication
|
||||
- Express validator for request validation
|
||||
- Comprehensive error handling with Zod schemas
|
||||
|
||||
Model Updates:
|
||||
- Gemini 2.5 Image Flash support
|
||||
- Grok 4 Fast with reasoning capabilities
|
||||
- Qwen3-omni and Qwen3-vl thinking models
|
||||
- DeepSeek, Claude 4.5, GLM 4.6 support
|
||||
- GitHub Copilot CLI integration with gpt-5-codex
|
||||
Improvements:
|
||||
- Agent model selection now requires explicit user choice
|
||||
- Added Mistral AI provider support
|
||||
- Added NewAPI generic provider support
|
||||
- Improved navbar layout consistency across different modes
|
||||
- Enhanced chat component responsiveness
|
||||
- Better code block display on small screens
|
||||
- Updated OVMS to 2025.3 official release
|
||||
- Added Greek language support
|
||||
|
||||
Bug Fixes:
|
||||
- Fix Swagger UI accessibility issues
|
||||
- Fix AI SDK error display with syntax highlighting
|
||||
- Fix webview search shortcut handling
|
||||
- Fix agent model visibility for CherryIn provider
|
||||
- Fix session message ordering and persistence
|
||||
- Fix anthropic model visibility in agent configuration
|
||||
- Fix knowledge base deletion and web search RAG errors
|
||||
- Fix migration for missing providers
|
||||
|
||||
Technical Updates:
|
||||
- React 19.2.0 upgrade
|
||||
- Enhanced Claude Code service with streaming support
|
||||
- Improved message transformation and streaming lifecycle
|
||||
- Database migration system with automatic schema sync
|
||||
- Optimized bundle size and dependency management
|
||||
- Fixed GitHub Copilot gpt-5-codex streaming issues
|
||||
- Fixed assistant creation failures
|
||||
- Fixed translate auto-copy functionality
|
||||
- Fixed miniapps external link opening
|
||||
- Fixed message layout and overflow issues
|
||||
- Fixed API key parsing to preserve spaces
|
||||
- Fixed agent display in different navbar layouts
|
||||
|
||||
<!--LANG:zh-CN-->
|
||||
v1.7.0-beta.1 新特性
|
||||
v1.7.0-beta.2 新特性
|
||||
|
||||
核心功能:
|
||||
- Agent 系统:引入智能 Agent 能力,与助手(Assistant)并存。Agent 基于 Claude Code SDK 构建,具备工具调用、文件操作和多轮推理能力,可自主解决复杂问题
|
||||
- Agent 管理:创建、配置和管理 Agent,支持自定义模型选择、工具权限、可访问路径和 MCP 服务器集成
|
||||
- Agent 会话:专属会话管理系统,支持持久化消息历史和上下文追踪
|
||||
- 统一界面:精简的助手和 Agent 标签页界面,改进导航和设置管理体验
|
||||
新功能:
|
||||
- 会话设置:独立管理会话特定的设置和模型配置
|
||||
- 笔记全文搜索:跨所有笔记搜索并高亮匹配内容
|
||||
- 内置滴滴 MCP 服务器:集成滴滴打车服务(仅限中国地区)
|
||||
- Intel OV OCR:使用 Intel NPU 的硬件加速 OCR
|
||||
- 自动启动 API 服务器:当存在 Agent 时自动启动
|
||||
|
||||
Agent 功能特性:
|
||||
- 工具支持:网页搜索、文件操作、Bash 命令执行和自定义 MCP 工具
|
||||
- 高级配置:最大轮次、温度、Token 限制
|
||||
- 权限控制:可配置的工具批准模式(手动、自动、无需批准)
|
||||
- 会话持久化:自动消息保存,优化的流式传输和数据库集成
|
||||
- 模型选择:基于 API 的模型过滤,支持特定提供商
|
||||
|
||||
界面与交互优化:
|
||||
- 统一的助手/Agent 标签页,带有流畅动画效果
|
||||
- 会话名称原地编辑功能
|
||||
- 虚拟列表渲染,提升性能表现
|
||||
- 活跃 Agent 的会话计数指示器
|
||||
- 增强的设置弹窗,采用标签页界面
|
||||
- Webview 键盘快捷键拦截,支持搜索功能
|
||||
|
||||
API 与基础设施:
|
||||
- RESTful API 用于 Agent 和会话管理
|
||||
- 集成 Drizzle ORM 管理 Agent 数据库
|
||||
- Claude Code OAuth 认证支持
|
||||
- Express validator 请求验证
|
||||
- 基于 Zod 模式的完善错误处理
|
||||
|
||||
模型更新:
|
||||
- 支持 Gemini 2.5 Image Flash
|
||||
- Grok 4 Fast 推理能力
|
||||
- Qwen3-omni 和 Qwen3-vl 思考模型
|
||||
- DeepSeek、Claude 4.5、GLM 4.6 支持
|
||||
- GitHub Copilot CLI 集成 gpt-5-codex
|
||||
改进:
|
||||
- Agent 模型选择现在需要用户显式选择
|
||||
- 添加 Mistral AI 提供商支持
|
||||
- 添加 NewAPI 通用提供商支持
|
||||
- 改进不同模式下的导航栏布局一致性
|
||||
- 增强聊天组件响应式设计
|
||||
- 优化小屏幕代码块显示
|
||||
- 更新 OVMS 至 2025.3 正式版
|
||||
- 添加希腊语支持
|
||||
|
||||
问题修复:
|
||||
- 修复 Swagger UI 无法打开
|
||||
- 修复 AI SDK 错误显示,添加语法高亮
|
||||
- 修复 Webview 搜索快捷键处理
|
||||
- 修复 CherryIn 提供商的 Agent 模型可见性
|
||||
- 修复会话消息排序和持久化
|
||||
- 修复 Anthropic 模型在 Agent 配置中的可见性
|
||||
- 修复知识库删除和网页搜索 RAG 错误
|
||||
- 修复缺失提供商的迁移问题
|
||||
|
||||
技术更新:
|
||||
- 升级至 React 19.2.0
|
||||
- 增强 Claude Code 服务流式传输支持
|
||||
- 改进消息转换和流式生命周期
|
||||
- 数据库迁移系统,支持自动模式同步
|
||||
- 优化打包大小和依赖管理
|
||||
- 修复 GitHub Copilot gpt-5-codex 流式传输问题
|
||||
- 修复助手创建失败
|
||||
- 修复翻译自动复制功能
|
||||
- 修复小程序外部链接打开
|
||||
- 修复消息布局和溢出问题
|
||||
- 修复 API 密钥解析以保留空格
|
||||
- 修复不同导航栏布局中的 Agent 显示
|
||||
<!--LANG:END-->
|
||||
|
||||
@@ -12,7 +12,7 @@ export default defineConfig([
|
||||
eslint.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
eslintReact.configs['recommended-typescript'],
|
||||
reactHooks.configs['recommended-latest'],
|
||||
reactHooks.configs.flat.recommended,
|
||||
{
|
||||
plugins: {
|
||||
'simple-import-sort': simpleImportSort,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "CherryStudio",
|
||||
"version": "1.7.0-beta.1",
|
||||
"version": "1.7.0-beta.2",
|
||||
"private": true,
|
||||
"description": "A powerful AI assistant for producer.",
|
||||
"main": "./out/main/index.js",
|
||||
@@ -261,7 +261,7 @@
|
||||
"eslint": "^9.22.0",
|
||||
"eslint-plugin-import-zod": "^1.2.0",
|
||||
"eslint-plugin-oxlint": "^1.15.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-hooks": "^7.0.0",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"express-validator": "^7.2.1",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createServer } from 'node:http'
|
||||
|
||||
import { loggerService } from '@logger'
|
||||
|
||||
import { agentService } from '../services/agents'
|
||||
import { loggerService } from '../services/LoggerService'
|
||||
import { app } from './app'
|
||||
import { config } from './config'
|
||||
|
||||
@@ -15,11 +16,17 @@ export class ApiServer {
|
||||
private server: ReturnType<typeof createServer> | null = null
|
||||
|
||||
async start(): Promise<void> {
|
||||
if (this.server) {
|
||||
if (this.server && this.server.listening) {
|
||||
logger.warn('Server already running')
|
||||
return
|
||||
}
|
||||
|
||||
// Clean up any failed server instance
|
||||
if (this.server && !this.server.listening) {
|
||||
logger.warn('Cleaning up failed server instance')
|
||||
this.server = null
|
||||
}
|
||||
|
||||
// Load config
|
||||
const { port, host } = await config.load()
|
||||
|
||||
@@ -39,7 +46,11 @@ export class ApiServer {
|
||||
resolve()
|
||||
})
|
||||
|
||||
this.server!.on('error', reject)
|
||||
this.server!.on('error', (error) => {
|
||||
// Clean up the server instance if listen fails
|
||||
this.server = null
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { ProviderSpecificError } from '@renderer/types/provider-specific-error'
|
||||
import { formatErrorMessage } from '@renderer/utils/error'
|
||||
import { convertLinks, flushLinkConverterBuffer } from '@renderer/utils/linkConverter'
|
||||
import type { ClaudeCodeRawValue } from '@shared/agents/claudecode/types'
|
||||
import type { TextStreamPart, ToolSet } from 'ai'
|
||||
import { AISDKError, type TextStreamPart, type ToolSet } from 'ai'
|
||||
|
||||
import { ToolCallChunkHandler } from './handleToolCallChunk'
|
||||
|
||||
@@ -357,11 +357,14 @@ export class AiSdkToChunkAdapter {
|
||||
case 'error':
|
||||
this.onChunk({
|
||||
type: ChunkType.ERROR,
|
||||
error: new ProviderSpecificError({
|
||||
message: formatErrorMessage(chunk.error),
|
||||
provider: 'unknown',
|
||||
cause: chunk.error
|
||||
})
|
||||
error:
|
||||
chunk.error instanceof AISDKError
|
||||
? chunk.error
|
||||
: new ProviderSpecificError({
|
||||
message: formatErrorMessage(chunk.error),
|
||||
provider: 'unknown',
|
||||
cause: chunk.error
|
||||
})
|
||||
})
|
||||
break
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
Button,
|
||||
cn,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
@@ -34,7 +33,7 @@ import {
|
||||
UpdateAgentForm
|
||||
} from '@renderer/types'
|
||||
import { AlertTriangleIcon } from 'lucide-react'
|
||||
import { ChangeEvent, FormEvent, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { ChangeEvent, FormEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { ErrorBoundary } from '../../ErrorBoundary'
|
||||
@@ -57,43 +56,30 @@ const buildAgentForm = (existing?: AgentWithTools): BaseAgentForm => ({
|
||||
name: existing?.name ?? 'Claude Code',
|
||||
description: existing?.description,
|
||||
instructions: existing?.instructions,
|
||||
model: existing?.model ?? 'claude-4-sonnet',
|
||||
model: existing?.model ?? '',
|
||||
accessible_paths: existing?.accessible_paths ? [...existing.accessible_paths] : [],
|
||||
allowed_tools: existing?.allowed_tools ? [...existing.allowed_tools] : [],
|
||||
mcps: existing?.mcps ? [...existing.mcps] : [],
|
||||
configuration: AgentConfigurationSchema.parse(existing?.configuration ?? {})
|
||||
})
|
||||
|
||||
interface BaseProps {
|
||||
type Props = {
|
||||
agent?: AgentWithTools
|
||||
}
|
||||
|
||||
interface TriggerProps extends BaseProps {
|
||||
trigger: { content: ReactNode; className?: string }
|
||||
isOpen?: never
|
||||
onClose?: never
|
||||
}
|
||||
|
||||
interface StateProps extends BaseProps {
|
||||
trigger?: never
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
type Props = TriggerProps | StateProps
|
||||
|
||||
/**
|
||||
* Modal component for creating or editing an agent.
|
||||
*
|
||||
* Either trigger or isOpen and onClose is given.
|
||||
* @param agent - Optional agent entity for editing mode.
|
||||
* @param trigger - Optional trigger element that opens the modal. It MUST propagate the click event to trigger the modal.
|
||||
* @param isOpen - Optional controlled modal open state. From useDisclosure.
|
||||
* @param onClose - Optional callback when modal closes. From useDisclosure.
|
||||
* @returns Modal component for agent creation/editing
|
||||
*/
|
||||
export const AgentModal: React.FC<Props> = ({ agent, trigger, isOpen: _isOpen, onClose: _onClose }) => {
|
||||
const { isOpen, onClose, onOpen } = useDisclosure({ isOpen: _isOpen, onClose: _onClose })
|
||||
export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _onClose }) => {
|
||||
const { isOpen, onClose } = useDisclosure({ isOpen: _isOpen, onClose: _onClose })
|
||||
const { t } = useTranslation()
|
||||
const loadingRef = useRef(false)
|
||||
// const { setTimeoutTimer } = useTimer()
|
||||
@@ -359,23 +345,6 @@ export const AgentModal: React.FC<Props> = ({ agent, trigger, isOpen: _isOpen, o
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
{/* NOTE: Hero UI Modal Pattern: Combine the Button and Modal components into a single
|
||||
encapsulated component. This is because the Modal component needs to bind the onOpen
|
||||
event handler to the Button for proper focus management.
|
||||
|
||||
Or just use external isOpen/onOpen/onClose to control modal state.
|
||||
*/}
|
||||
|
||||
{trigger && (
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onOpen()
|
||||
}}
|
||||
className={cn('w-full', trigger.className)}>
|
||||
{trigger.content}
|
||||
</div>
|
||||
)}
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
|
||||
@@ -67,14 +67,14 @@ const NavbarContainer = styled.div<{ $isFullScreen: boolean }>`
|
||||
flex-direction: row;
|
||||
min-height: ${isMac ? 'env(titlebar-area-height)' : 'var(--navbar-height)'};
|
||||
max-height: var(--navbar-height);
|
||||
margin-left: ${isMac ? 'calc(var(--sidebar-width) * -1)' : 0};
|
||||
margin-left: ${isMac ? 'calc(var(--sidebar-width) * -1 + 2px)' : 0};
|
||||
padding-left: ${({ $isFullScreen }) =>
|
||||
isMac ? ($isFullScreen ? 'var(--sidebar-width)' : 'env(titlebar-area-x)') : 0};
|
||||
-webkit-app-region: drag;
|
||||
`
|
||||
|
||||
const NavbarLeftContainer = styled.div`
|
||||
min-width: ${isMac ? 'calc(var(--assistants-width) - 20px)' : 'var(--assistants-width)'};
|
||||
/* min-width: ${isMac ? 'calc(var(--assistants-width) - 20px)' : 'var(--assistants-width)'}; */
|
||||
padding: 0 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@@ -430,6 +430,12 @@ export const SYSTEM_MODELS: Record<SystemProviderId | 'defaultModel', Model[]> =
|
||||
}
|
||||
],
|
||||
anthropic: [
|
||||
{
|
||||
id: 'claude-haiku-4-5-20251001',
|
||||
provider: 'anthropic',
|
||||
name: 'Claude Haiku 4.5',
|
||||
group: 'Claude 4.5'
|
||||
},
|
||||
{
|
||||
id: 'claude-sonnet-4-5-20250929',
|
||||
provider: 'anthropic',
|
||||
|
||||
@@ -335,7 +335,8 @@ export function isClaudeReasoningModel(model?: Model): boolean {
|
||||
modelId.includes('claude-3-7-sonnet') ||
|
||||
modelId.includes('claude-3.7-sonnet') ||
|
||||
modelId.includes('claude-sonnet-4') ||
|
||||
modelId.includes('claude-opus-4')
|
||||
modelId.includes('claude-opus-4') ||
|
||||
modelId.includes('claude-haiku-4')
|
||||
)
|
||||
}
|
||||
|
||||
@@ -493,8 +494,9 @@ export const THINKING_TOKEN_MAP: Record<string, { min: number; max: number }> =
|
||||
'qwen3-(?!max).*$': { min: 1024, max: 38_912 },
|
||||
|
||||
// Claude models
|
||||
'claude-3[.-]7.*sonnet.*$': { min: 1024, max: 64000 },
|
||||
'claude-(:?sonnet|opus)-4.*$': { min: 1024, max: 32000 }
|
||||
'claude-3[.-]7.*sonnet.*$': { min: 1024, max: 64_000 },
|
||||
'claude-(:?haiku|sonnet)-4.*$': { min: 1024, max: 64_000 },
|
||||
'claude-opus-4-1.*$': { min: 1024, max: 32_000 }
|
||||
}
|
||||
|
||||
export const findTokenLimit = (modelId: string): { min: number; max: number } | undefined => {
|
||||
|
||||
@@ -15,6 +15,7 @@ const visionAllowedModels = [
|
||||
'gemini-(flash|pro|flash-lite)-latest',
|
||||
'gemini-exp',
|
||||
'claude-3',
|
||||
'claude-haiku-4',
|
||||
'claude-sonnet-4',
|
||||
'claude-opus-4',
|
||||
'vision',
|
||||
|
||||
@@ -7,7 +7,7 @@ import { isAnthropicModel } from './utils'
|
||||
import { isPureGenerateImageModel, isTextToImageModel } from './vision'
|
||||
|
||||
export const CLAUDE_SUPPORTED_WEBSEARCH_REGEX = new RegExp(
|
||||
`\\b(?:claude-3(-|\\.)(7|5)-sonnet(?:-[\\w-]+)|claude-3(-|\\.)5-haiku(?:-[\\w-]+)|claude-sonnet-4(?:-[\\w-]+)?|claude-opus-4(?:-[\\w-]+)?)\\b`,
|
||||
`\\b(?:claude-3(-|\\.)(7|5)-sonnet(?:-[\\w-]+)|claude-3(-|\\.)5-haiku(?:-[\\w-]+)|claude-(haiku|sonnet|opus)-4(?:-[\\w-]+)?)\\b`,
|
||||
'i'
|
||||
)
|
||||
|
||||
|
||||
@@ -140,9 +140,7 @@ const Chat: FC<Props> = (props) => {
|
||||
firstUpdateOrNoFirstUpdateHandler()
|
||||
}
|
||||
|
||||
const mainHeight = isTopNavbar
|
||||
? 'calc(100vh - var(--navbar-height) - var(--navbar-height) - 12px)'
|
||||
: 'calc(100vh - var(--navbar-height))'
|
||||
const mainHeight = isTopNavbar ? 'calc(100vh - var(--navbar-height) - 6px)' : 'calc(100vh - var(--navbar-height))'
|
||||
|
||||
const SessionMessages = useMemo(() => {
|
||||
if (activeAgentId === null) {
|
||||
@@ -192,66 +190,84 @@ const Chat: FC<Props> = (props) => {
|
||||
</div>
|
||||
)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Container id="chat" className={classNames([messageStyle, { 'multi-select-mode': isMultiSelectMode }])}>
|
||||
{isTopNavbar && (
|
||||
<ChatNavbar
|
||||
activeAssistant={props.assistant}
|
||||
activeTopic={props.activeTopic}
|
||||
setActiveTopic={props.setActiveTopic}
|
||||
setActiveAssistant={props.setActiveAssistant}
|
||||
position="left"
|
||||
/>
|
||||
)}
|
||||
<HStack>
|
||||
<Main
|
||||
ref={mainRef}
|
||||
id="chat-main"
|
||||
vertical
|
||||
flex={1}
|
||||
justify="space-between"
|
||||
style={{ maxWidth: chatMaxWidth, height: mainHeight }}>
|
||||
<QuickPanelProvider>
|
||||
{activeTopicOrSession === 'topic' && (
|
||||
<>
|
||||
<Messages
|
||||
key={props.activeTopic.id}
|
||||
assistant={assistant}
|
||||
topic={props.activeTopic}
|
||||
setActiveTopic={props.setActiveTopic}
|
||||
onComponentUpdate={messagesComponentUpdateHandler}
|
||||
onFirstUpdate={messagesComponentFirstUpdateHandler}
|
||||
/>
|
||||
<ContentSearch
|
||||
ref={contentSearchRef}
|
||||
searchTarget={mainRef as React.RefObject<HTMLElement>}
|
||||
filter={contentSearchFilter}
|
||||
includeUser={filterIncludeUser}
|
||||
onIncludeUserChange={userOutlinedItemClickHandler}
|
||||
/>
|
||||
{messageNavigation === 'buttons' && <ChatNavigation containerId="messages" />}
|
||||
<Inputbar assistant={assistant} setActiveTopic={props.setActiveTopic} topic={props.activeTopic} />
|
||||
</>
|
||||
)}
|
||||
{activeTopicOrSession === 'session' && !activeAgentId && <AgentInvalid />}
|
||||
{activeTopicOrSession === 'session' && activeAgentId && !activeSessionId && <SessionInvalid />}
|
||||
{activeTopicOrSession === 'session' && activeAgentId && activeSessionId && (
|
||||
<>
|
||||
<SessionMessages />
|
||||
<SessionInputBar />
|
||||
</>
|
||||
)}
|
||||
{isMultiSelectMode && <MultiSelectActionPopup topic={props.activeTopic} />}
|
||||
</QuickPanelProvider>
|
||||
</Main>
|
||||
<motion.div
|
||||
animate={{
|
||||
marginRight: topicPosition === 'right' && showTopics ? 'var(--assistants-width)' : 0
|
||||
}}
|
||||
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
||||
style={{ flex: 1, display: 'flex', minWidth: 0 }}>
|
||||
<Main
|
||||
ref={mainRef}
|
||||
id="chat-main"
|
||||
vertical
|
||||
flex={1}
|
||||
justify="space-between"
|
||||
style={{ maxWidth: chatMaxWidth, height: mainHeight }}>
|
||||
<QuickPanelProvider>
|
||||
<ChatNavbar
|
||||
activeAssistant={props.assistant}
|
||||
activeTopic={props.activeTopic}
|
||||
setActiveTopic={props.setActiveTopic}
|
||||
setActiveAssistant={props.setActiveAssistant}
|
||||
position="left"
|
||||
/>
|
||||
<div
|
||||
className="flex flex-1 flex-col justify-between"
|
||||
style={{ height: `calc(${mainHeight} - var(--navbar-height))` }}>
|
||||
{activeTopicOrSession === 'topic' && (
|
||||
<>
|
||||
<Messages
|
||||
key={props.activeTopic.id}
|
||||
assistant={assistant}
|
||||
topic={props.activeTopic}
|
||||
setActiveTopic={props.setActiveTopic}
|
||||
onComponentUpdate={messagesComponentUpdateHandler}
|
||||
onFirstUpdate={messagesComponentFirstUpdateHandler}
|
||||
/>
|
||||
<ContentSearch
|
||||
ref={contentSearchRef}
|
||||
searchTarget={mainRef as React.RefObject<HTMLElement>}
|
||||
filter={contentSearchFilter}
|
||||
includeUser={filterIncludeUser}
|
||||
onIncludeUserChange={userOutlinedItemClickHandler}
|
||||
/>
|
||||
{messageNavigation === 'buttons' && <ChatNavigation containerId="messages" />}
|
||||
<Inputbar assistant={assistant} setActiveTopic={props.setActiveTopic} topic={props.activeTopic} />
|
||||
</>
|
||||
)}
|
||||
{activeTopicOrSession === 'session' && !activeAgentId && <AgentInvalid />}
|
||||
{activeTopicOrSession === 'session' && activeAgentId && !activeSessionId && <SessionInvalid />}
|
||||
{activeTopicOrSession === 'session' && activeAgentId && activeSessionId && (
|
||||
<>
|
||||
<SessionMessages />
|
||||
<SessionInputBar />
|
||||
</>
|
||||
)}
|
||||
{isMultiSelectMode && <MultiSelectActionPopup topic={props.activeTopic} />}
|
||||
</div>
|
||||
</QuickPanelProvider>
|
||||
</Main>
|
||||
</motion.div>
|
||||
<AnimatePresence initial={false}>
|
||||
{topicPosition === 'right' && showTopics && (
|
||||
<motion.div
|
||||
initial={{ width: 0, opacity: 0 }}
|
||||
animate={{ width: 'auto', opacity: 1 }}
|
||||
exit={{ width: 0, opacity: 0 }}
|
||||
key="right-tabs"
|
||||
initial={{ x: 'var(--assistants-width)' }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: 'var(--assistants-width)' }}
|
||||
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
||||
style={{ overflow: 'hidden' }}>
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: isTopNavbar ? 0 : 'calc(var(--navbar-height) + 1px)',
|
||||
width: 'var(--assistants-width)',
|
||||
height: '100%',
|
||||
zIndex: 10
|
||||
}}>
|
||||
<Tabs
|
||||
activeAssistant={assistant}
|
||||
activeTopic={props.activeTopic}
|
||||
@@ -269,13 +285,14 @@ const Chat: FC<Props> = (props) => {
|
||||
|
||||
export const useChatMaxWidth = () => {
|
||||
const { showTopics, topicPosition } = useSettings()
|
||||
const { isLeftNavbar } = useNavbarPosition()
|
||||
const { isLeftNavbar, isTopNavbar } = useNavbarPosition()
|
||||
const { showAssistants } = useShowAssistants()
|
||||
const showRightTopics = showTopics && topicPosition === 'right'
|
||||
const minusAssistantsWidth = showAssistants ? '- var(--assistants-width)' : ''
|
||||
const minusRightTopicsWidth = showRightTopics ? '- var(--assistants-width)' : ''
|
||||
const minusBorderWidth = isTopNavbar ? (showTopics ? '- 12px' : '- 6px') : ''
|
||||
const sidebarWidth = isLeftNavbar ? '- var(--sidebar-width)' : ''
|
||||
return `calc(100vw ${sidebarWidth} ${minusAssistantsWidth} ${minusRightTopicsWidth})`
|
||||
return `calc(100vw ${sidebarWidth} ${minusAssistantsWidth} ${minusRightTopicsWidth} ${minusBorderWidth})`
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
|
||||
@@ -3,7 +3,7 @@ import { HStack } from '@renderer/components/Layout'
|
||||
import SearchPopup from '@renderer/components/Popups/SearchPopup'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { modelGenerating } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
@@ -34,6 +34,7 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
|
||||
const { showAssistants, toggleShowAssistants } = useShowAssistants()
|
||||
const { topicPosition, narrowMode } = useSettings()
|
||||
const { showTopics, toggleShowTopics } = useShowTopics()
|
||||
const { isTopNavbar } = useNavbarPosition()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
useShortcut('toggle_show_assistants', toggleShowAssistants)
|
||||
@@ -73,16 +74,16 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
|
||||
// )
|
||||
|
||||
return (
|
||||
<NavbarHeader className="home-navbar">
|
||||
<div className="flex min-w-0 flex-1 shrink items-center overflow-auto">
|
||||
{showAssistants && (
|
||||
<NavbarHeader className="home-navbar" style={{ height: 'var(--navbar-height)' }}>
|
||||
<div className="flex h-full min-w-0 flex-1 shrink items-center overflow-auto">
|
||||
{isTopNavbar && showAssistants && (
|
||||
<Tooltip title={t('navbar.hide_sidebar')} mouseEnterDelay={0.8}>
|
||||
<NavbarIcon onClick={toggleShowAssistants}>
|
||||
<PanelLeftClose size={18} />
|
||||
</NavbarIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!showAssistants && (
|
||||
{isTopNavbar && !showAssistants && (
|
||||
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={0.8}>
|
||||
<NavbarIcon onClick={() => toggleShowAssistants()} style={{ marginRight: 8 }}>
|
||||
<PanelRightClose size={18} />
|
||||
@@ -90,13 +91,13 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
|
||||
</Tooltip>
|
||||
)}
|
||||
<AnimatePresence initial={false}>
|
||||
{!showAssistants && (
|
||||
{!showAssistants && isTopNavbar && (
|
||||
<motion.div
|
||||
initial={{ width: 0, opacity: 0 }}
|
||||
animate={{ width: 'auto', opacity: 1 }}
|
||||
exit={{ width: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.3, ease: 'easeInOut' }}>
|
||||
<NavbarIcon onClick={onShowAssistantsDrawer} style={{ marginRight: 8 }}>
|
||||
<NavbarIcon onClick={onShowAssistantsDrawer} style={{ marginRight: 5 }}>
|
||||
<Menu size={18} />
|
||||
</NavbarIcon>
|
||||
</motion.div>
|
||||
@@ -105,25 +106,29 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
|
||||
<ChatNavbarContent assistant={assistant} />
|
||||
</div>
|
||||
<HStack alignItems="center" gap={8}>
|
||||
<UpdateAppButton />
|
||||
<Tooltip title={t('navbar.expand')} mouseEnterDelay={0.8}>
|
||||
<NarrowIcon onClick={handleNarrowModeToggle}>
|
||||
<i className="iconfont icon-icon-adaptive-width"></i>
|
||||
</NarrowIcon>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('chat.assistant.search.placeholder')} mouseEnterDelay={0.8}>
|
||||
<NavbarIcon onClick={() => SearchPopup.show()}>
|
||||
<Search size={18} />
|
||||
</NavbarIcon>
|
||||
</Tooltip>
|
||||
{topicPosition === 'right' && !showTopics && (
|
||||
{isTopNavbar && <UpdateAppButton />}
|
||||
{isTopNavbar && (
|
||||
<Tooltip title={t('navbar.expand')} mouseEnterDelay={0.8}>
|
||||
<NarrowIcon onClick={handleNarrowModeToggle}>
|
||||
<i className="iconfont icon-icon-adaptive-width"></i>
|
||||
</NarrowIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isTopNavbar && (
|
||||
<Tooltip title={t('chat.assistant.search.placeholder')} mouseEnterDelay={0.8}>
|
||||
<NavbarIcon onClick={() => SearchPopup.show()}>
|
||||
<Search size={18} />
|
||||
</NavbarIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isTopNavbar && topicPosition === 'right' && !showTopics && (
|
||||
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={2}>
|
||||
<NavbarIcon onClick={toggleShowTopics}>
|
||||
<PanelLeftClose size={18} />
|
||||
</NavbarIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
{topicPosition === 'right' && showTopics && (
|
||||
{isTopNavbar && topicPosition === 'right' && showTopics && (
|
||||
<Tooltip title={t('navbar.hide_sidebar')} mouseEnterDelay={2}>
|
||||
<NavbarIcon onClick={toggleShowTopics}>
|
||||
<PanelRightClose size={18} />
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { Navbar, NavbarCenter, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import SearchPopup from '@renderer/components/Popups/SearchPopup'
|
||||
import { isLinux, isMac, isWin } from '@renderer/config/constant'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { isLinux, isWin } from '@renderer/config/constant'
|
||||
import { modelGenerating } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
|
||||
import { useChatMaxWidth } from '@renderer/pages/home/Chat'
|
||||
import ChatNavbarContent from '@renderer/pages/home/components/ChatNavbarContent'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setNarrowMode } from '@renderer/store/settings'
|
||||
@@ -17,11 +14,10 @@ import { Tooltip } from 'antd'
|
||||
import { t } from 'i18next'
|
||||
import { Menu, PanelLeftClose, PanelRightClose, Search } from 'lucide-react'
|
||||
import { AnimatePresence, motion } from 'motion/react'
|
||||
import React, { FC } from 'react'
|
||||
import { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import AssistantsDrawer from './components/AssistantsDrawer'
|
||||
import SelectModelButton from './components/SelectModelButton'
|
||||
import UpdateAppButton from './components/UpdateAppButton'
|
||||
|
||||
interface Props {
|
||||
@@ -40,11 +36,9 @@ const HeaderNavbar: FC<Props> = ({
|
||||
setActiveTopic,
|
||||
activeTopicOrSession
|
||||
}) => {
|
||||
const { assistant } = useAssistant(activeAssistant.id)
|
||||
const { showAssistants, toggleShowAssistants } = useShowAssistants()
|
||||
const { topicPosition, narrowMode } = useSettings()
|
||||
const { showTopics, toggleShowTopics } = useShowTopics()
|
||||
const chatMaxWidth = useChatMaxWidth()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
useShortcut('toggle_show_assistants', toggleShowAssistants)
|
||||
@@ -101,7 +95,7 @@ const HeaderNavbar: FC<Props> = ({
|
||||
justifyContent: 'flex-start',
|
||||
borderRight: 'none',
|
||||
paddingLeft: 0,
|
||||
paddingRight: 10,
|
||||
paddingRight: 0,
|
||||
minWidth: 'auto'
|
||||
}}>
|
||||
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={0.8}>
|
||||
@@ -123,22 +117,7 @@ const HeaderNavbar: FC<Props> = ({
|
||||
</AnimatePresence>
|
||||
</NavbarLeft>
|
||||
)}
|
||||
<NavbarCenter>
|
||||
{activeTopicOrSession === 'topic' ? (
|
||||
<HStack alignItems="center" gap={6} ml={!isMac ? 16 : 0}>
|
||||
<SelectModelButton assistant={assistant} />
|
||||
</HStack>
|
||||
) : (
|
||||
<ChatNavbarContainer
|
||||
style={{
|
||||
maxWidth: chatMaxWidth,
|
||||
marginLeft: !isMac ? 16 : 0
|
||||
}}>
|
||||
<ChatNavbarContent assistant={assistant} />
|
||||
</ChatNavbarContainer>
|
||||
)}
|
||||
</NavbarCenter>
|
||||
|
||||
<NavbarCenter></NavbarCenter>
|
||||
<NavbarRight
|
||||
style={{
|
||||
justifyContent: 'flex-end',
|
||||
@@ -220,15 +199,4 @@ const NarrowIcon = styled(NavbarIcon)`
|
||||
}
|
||||
`
|
||||
|
||||
const ChatNavbarContainer: React.FC<{ children: React.ReactNode; style?: React.CSSProperties }> = ({
|
||||
children,
|
||||
style
|
||||
}) => {
|
||||
return (
|
||||
<div className="nodrag flex min-w-0 flex-1 items-center justify-start gap-1.5 overflow-hidden" style={style}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HeaderNavbar
|
||||
|
||||
@@ -180,7 +180,7 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
||||
const Container = styled(Scrollbar)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
padding: 12px 10px;
|
||||
`
|
||||
|
||||
export default AssistantsTab
|
||||
|
||||
@@ -30,7 +30,7 @@ const SessionSettingsTab: FC<Props> = ({ session, update }) => {
|
||||
|
||||
return (
|
||||
<div className="w-[var(--assistants-width)] p-2 px-3 pt-4">
|
||||
<EssentialSettings agentBase={session} update={update} />
|
||||
<EssentialSettings agentBase={session} update={update} showModelSetting={false} />
|
||||
<AdvancedSettings agentBase={session} update={update} />
|
||||
<Divider className="my-2" />
|
||||
<Button size="sm" fullWidth onPress={onMoreSetting}>
|
||||
|
||||
@@ -14,7 +14,6 @@ const SessionsTab: FC<SessionsTabProps> = () => {
|
||||
const { activeAgentId } = chat
|
||||
const { t } = useTranslation()
|
||||
const { apiServer } = useSettings()
|
||||
const { topicPosition, navbarPosition } = useSettings()
|
||||
|
||||
if (!apiServer.enabled) {
|
||||
return (
|
||||
@@ -34,15 +33,7 @@ const SessionsTab: FC<SessionsTabProps> = () => {
|
||||
|
||||
return (
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
initial={{ width: 0, opacity: 0 }}
|
||||
animate={{ width: 'var(--assistants-width)', opacity: 1 }}
|
||||
exit={{ width: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.5, ease: 'easeInOut' }}
|
||||
className={cn(
|
||||
'overflow-hidden',
|
||||
topicPosition === 'right' && navbarPosition === 'top' ? 'rounded-l-2xl border-t border-b border-l' : undefined
|
||||
)}>
|
||||
<motion.div className={cn('overflow-hidden', 'h-full')}>
|
||||
<Sessions agentId={activeAgentId} />
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
|
||||
@@ -44,9 +44,16 @@ const AgentItem: FC<AgentItemProps> = ({ agent, isActive, onDelete, onPress }) =
|
||||
<AgentLabel agent={agent} />
|
||||
</AgentNameWrapper>
|
||||
</AssistantNameRow>
|
||||
<MenuButton>
|
||||
{isActive ? <SessionCount>{sessions.length}</SessionCount> : <Bot size={14} className="text-primary" />}
|
||||
</MenuButton>
|
||||
{isActive && (
|
||||
<MenuButton>
|
||||
<SessionCount>{sessions.length}</SessionCount>
|
||||
</MenuButton>
|
||||
)}
|
||||
{!isActive && (
|
||||
<BotIcon>
|
||||
<Bot size={16} className="text-primary" />
|
||||
</BotIcon>
|
||||
)}
|
||||
</Container>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
@@ -110,6 +117,16 @@ export const MenuButton: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ cla
|
||||
/>
|
||||
)
|
||||
|
||||
export const BotIcon: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, ...props }) => (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute top-[8px] right-[12px] flex flex-row items-center justify-center rounded-full text-[14px] text-[var(--color-text)]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
export const SessionCount: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, ...props }) => (
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Button, cn, Input, Tooltip } from '@heroui/react'
|
||||
import { DeleteIcon, EditIcon } from '@renderer/components/Icons'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
|
||||
@@ -11,9 +10,19 @@ import { SessionLabel } from '@renderer/pages/settings/AgentSettings/shared'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { newMessagesActions } from '@renderer/store/newMessage'
|
||||
import { AgentSessionEntity } from '@renderer/types'
|
||||
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@renderer/ui/context-menu'
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuSub,
|
||||
ContextMenuSubContent,
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuTrigger
|
||||
} from '@renderer/ui/context-menu'
|
||||
import { classNames } from '@renderer/utils'
|
||||
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
|
||||
import { XIcon } from 'lucide-react'
|
||||
import { Tooltip } from 'antd'
|
||||
import { MenuIcon, XIcon } from 'lucide-react'
|
||||
import React, { FC, memo, startTransition, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@@ -24,13 +33,11 @@ interface SessionItemProps {
|
||||
session: AgentSessionEntity
|
||||
// use external agentId as SSOT, instead of session.agent_id
|
||||
agentId: string
|
||||
isDisabled?: boolean
|
||||
isLoading?: boolean
|
||||
onDelete: () => void
|
||||
onPress: () => void
|
||||
}
|
||||
|
||||
const SessionItem: FC<SessionItemProps> = ({ session, agentId, isDisabled, isLoading, onDelete, onPress }) => {
|
||||
const SessionItem: FC<SessionItemProps> = ({ session, agentId, onDelete, onPress }) => {
|
||||
const { t } = useTranslation()
|
||||
const { chat } = useRuntime()
|
||||
const { updateSession } = useUpdateSession(agentId)
|
||||
@@ -50,16 +57,16 @@ const SessionItem: FC<SessionItemProps> = ({ session, agentId, isDisabled, isLoa
|
||||
const DeleteButton = () => {
|
||||
return (
|
||||
<Tooltip
|
||||
content={t('chat.topics.delete.shortcut', { key: isMac ? '⌘' : 'Ctrl' })}
|
||||
classNames={{ content: 'text-xs' }}
|
||||
delay={500}
|
||||
closeDelay={0}>
|
||||
<div
|
||||
role="button"
|
||||
className={cn(
|
||||
'mr-2 flex aspect-square h-6 w-6 items-center justify-center rounded-2xl',
|
||||
isConfirmingDeletion ? 'hover:bg-danger-100' : 'hover:bg-foreground-300'
|
||||
)}
|
||||
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: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (isConfirmingDeletion || e.ctrlKey || e.metaKey) {
|
||||
@@ -78,17 +85,11 @@ const SessionItem: FC<SessionItemProps> = ({ session, agentId, isDisabled, isLoa
|
||||
}
|
||||
}}>
|
||||
{isConfirmingDeletion ? (
|
||||
<DeleteIcon
|
||||
size={14}
|
||||
className="opacity-0 transition-colors-opacity group-hover:text-danger group-hover:opacity-100"
|
||||
/>
|
||||
<DeleteIcon size={14} color="var(--color-error)" style={{ pointerEvents: 'none' }} />
|
||||
) : (
|
||||
<XIcon
|
||||
size={14}
|
||||
className={cn(isActive ? 'opacity-100' : 'opacity-0', 'group-hover:opacity-100', 'transition-opacity')}
|
||||
/>
|
||||
<XIcon size={14} color="var(--color-text-3)" style={{ pointerEvents: 'none' }} />
|
||||
)}
|
||||
</div>
|
||||
</MenuButton>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
@@ -106,44 +107,44 @@ const SessionItem: FC<SessionItemProps> = ({ session, agentId, isDisabled, isLoa
|
||||
}
|
||||
}, [activeSessionId, dispatch, isFulfilled, session.id, sessionTopicId])
|
||||
|
||||
const { topicPosition, setTopicPosition } = useSettings()
|
||||
const singlealone = topicPosition === 'right'
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContextMenu modal={false}>
|
||||
<ContextMenuTrigger>
|
||||
<ButtonContainer
|
||||
isDisabled={isDisabled}
|
||||
isLoading={isLoading}
|
||||
onPress={onPress}
|
||||
isActive={isActive}
|
||||
<SessionListItem
|
||||
className={classNames(isActive ? 'active' : '', singlealone ? 'singlealone' : '')}
|
||||
onClick={isEditing ? undefined : onPress}
|
||||
onDoubleClick={() => startEdit(session.name ?? '')}
|
||||
className="group">
|
||||
<SessionLabelContainer className="name h-full w-full pl-1" title={session.name ?? session.id}>
|
||||
{isPending && !isActive && <PendingIndicator />}
|
||||
{isFulfilled && !isActive && <FulfilledIndicator />}
|
||||
{isEditing && (
|
||||
<Input
|
||||
title={session.name ?? session.id}
|
||||
style={{
|
||||
borderRadius: 'var(--list-item-border-radius)',
|
||||
cursor: isEditing ? 'default' : 'pointer'
|
||||
}}>
|
||||
{isPending && !isActive && <PendingIndicator />}
|
||||
{isFulfilled && !isActive && <FulfilledIndicator />}
|
||||
<SessionNameContainer>
|
||||
{isEditing ? (
|
||||
<SessionEditInput
|
||||
ref={inputRef}
|
||||
variant="bordered"
|
||||
value={editValue}
|
||||
onValueChange={handleValueChange}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleValueChange(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
classNames={{
|
||||
base: 'h-full',
|
||||
mainWrapper: 'h-full',
|
||||
inputWrapper: 'h-full min-h-0 px-1.5',
|
||||
input: isSaving ? 'brightness-50' : undefined
|
||||
}}
|
||||
onClick={(e: React.MouseEvent) => e.stopPropagation()}
|
||||
style={{ opacity: isSaving ? 0.5 : 1 }}
|
||||
/>
|
||||
)}
|
||||
{!isEditing && (
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<SessionLabel session={session} />
|
||||
) : (
|
||||
<>
|
||||
<SessionName>
|
||||
<SessionLabel session={session} />
|
||||
</SessionName>
|
||||
<DeleteButton />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</SessionLabelContainer>
|
||||
</ButtonContainer>
|
||||
</SessionNameContainer>
|
||||
</SessionListItem>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem
|
||||
@@ -157,6 +158,20 @@ const SessionItem: FC<SessionItemProps> = ({ session, agentId, isDisabled, isLoa
|
||||
<EditIcon size={14} />
|
||||
{t('common.edit')}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSub>
|
||||
<ContextMenuSubTrigger className="gap-2">
|
||||
<MenuIcon size={14} />
|
||||
{t('settings.topic.position.label')}
|
||||
</ContextMenuSubTrigger>
|
||||
<ContextMenuSubContent>
|
||||
<ContextMenuItem key="left" onClick={() => setTopicPosition('left')}>
|
||||
{t('settings.topic.position.left')}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem key="right" onClick={() => setTopicPosition('right')}>
|
||||
{t('settings.topic.position.right')}
|
||||
</ContextMenuItem>
|
||||
</ContextMenuSubContent>
|
||||
</ContextMenuSub>
|
||||
<ContextMenuItem
|
||||
key="delete"
|
||||
className="text-danger"
|
||||
@@ -172,38 +187,96 @@ const SessionItem: FC<SessionItemProps> = ({ session, agentId, isDisabled, isLoa
|
||||
)
|
||||
}
|
||||
|
||||
const ButtonContainer: React.FC<React.ComponentProps<typeof Button> & { isActive?: boolean }> = ({
|
||||
isActive,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
const { topicPosition } = useSettings()
|
||||
const activeBg = topicPosition === 'left' ? 'bg-[var(--color-list-item)]' : 'bg-foreground-100'
|
||||
return (
|
||||
<Button
|
||||
{...props}
|
||||
variant="light"
|
||||
className={cn(
|
||||
'relative mb-2 flex h-9 flex-row justify-between p-0',
|
||||
'rounded-[var(--list-item-border-radius)]',
|
||||
'border-[0.5px] border-transparent',
|
||||
'w-[calc(var(--assistants-width)_-_20px)]',
|
||||
'cursor-pointer',
|
||||
isActive ? cn(activeBg, 'shadow-sm') : undefined,
|
||||
className
|
||||
)}>
|
||||
{children}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
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;
|
||||
|
||||
const SessionLabelContainer: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, ...props }) => (
|
||||
<div
|
||||
{...props}
|
||||
className={cn('text-[13px] text-[var(--color-text)]', 'flex flex-row items-center gap-2', className)}
|
||||
/>
|
||||
)
|
||||
.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'
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from '@renderer/store/runtime'
|
||||
import { CreateSessionForm } from '@renderer/types'
|
||||
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { motion } from 'framer-motion'
|
||||
import { memo, useCallback, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@@ -30,7 +30,7 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
||||
const { agent } = useAgent(agentId)
|
||||
const { sessions, isLoading, error, deleteSession, createSession } = useSessions(agentId)
|
||||
const { chat } = useRuntime()
|
||||
const { activeSessionIdMap, sessionWaiting } = chat
|
||||
const { activeSessionIdMap } = chat
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const setActiveSessionId = useCallback(
|
||||
@@ -109,45 +109,30 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
||||
if (error) return <Alert color="danger" content={t('agent.session.get.error.failed')} />
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="sessions-tab flex h-full w-full flex-col p-2">
|
||||
<motion.div initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }}>
|
||||
<AddButton onPress={handleCreateSession} className="mb-2">
|
||||
{t('agent.session.add.title')}
|
||||
</AddButton>
|
||||
</motion.div>
|
||||
<AnimatePresence>
|
||||
{/* h-9 */}
|
||||
<DynamicVirtualList
|
||||
list={sessions}
|
||||
estimateSize={() => 9 * 4}
|
||||
scrollerStyle={{
|
||||
// FIXME: This component only supports CSSProperties
|
||||
overflowX: 'hidden'
|
||||
}}
|
||||
autoHideScrollbar>
|
||||
{(session) => (
|
||||
<motion.div
|
||||
key={session.id}
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.3 }}>
|
||||
<SessionItem
|
||||
session={session}
|
||||
agentId={agentId}
|
||||
isDisabled={sessionWaiting[session.id]}
|
||||
isLoading={sessionWaiting[session.id]}
|
||||
onDelete={() => handleDeleteSession(session.id)}
|
||||
onPress={() => setActiveSessionId(agentId, session.id)}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</DynamicVirtualList>
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
<div className="sessions-tab flex h-full w-full flex-col p-2">
|
||||
<AddButton onPress={handleCreateSession} className="mb-2">
|
||||
{t('agent.session.add.title')}
|
||||
</AddButton>
|
||||
{/* h-9 */}
|
||||
<DynamicVirtualList
|
||||
list={sessions}
|
||||
estimateSize={() => 9 * 4}
|
||||
scrollerStyle={{
|
||||
// FIXME: This component only supports CSSProperties
|
||||
overflowX: 'hidden'
|
||||
}}
|
||||
autoHideScrollbar>
|
||||
{(session) => (
|
||||
<SessionItem
|
||||
key={session.id}
|
||||
session={session}
|
||||
agentId={agentId}
|
||||
onDelete={() => handleDeleteSession(session.id)}
|
||||
onPress={() => setActiveSessionId(agentId, session.id)}
|
||||
/>
|
||||
)}
|
||||
</DynamicVirtualList>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Popover, PopoverContent, PopoverTrigger } from '@heroui/react'
|
||||
import { Button, Popover, PopoverContent, PopoverTrigger, useDisclosure } from '@heroui/react'
|
||||
import { AgentModal } from '@renderer/components/Popups/agent/AgentModal'
|
||||
import { Bot, MessageSquare } from 'lucide-react'
|
||||
import { FC, useState } from 'react'
|
||||
@@ -13,7 +13,7 @@ interface UnifiedAddButtonProps {
|
||||
const UnifiedAddButton: FC<UnifiedAddButtonProps> = ({ onCreateAssistant }) => {
|
||||
const { t } = useTranslation()
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false)
|
||||
const [isAgentModalOpen, setIsAgentModalOpen] = useState(false)
|
||||
const { isOpen: isAgentModalOpen, onOpen: onAgentModalOpen, onClose: onAgentModalClose } = useDisclosure()
|
||||
|
||||
const handleAddAssistant = () => {
|
||||
setIsPopoverOpen(false)
|
||||
@@ -22,7 +22,7 @@ const UnifiedAddButton: FC<UnifiedAddButtonProps> = ({ onCreateAssistant }) => {
|
||||
|
||||
const handleAddAgent = () => {
|
||||
setIsPopoverOpen(false)
|
||||
setIsAgentModalOpen(true)
|
||||
onAgentModalOpen()
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -53,7 +53,7 @@ const UnifiedAddButton: FC<UnifiedAddButtonProps> = ({ onCreateAssistant }) => {
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<AgentModal isOpen={isAgentModalOpen} onClose={() => setIsAgentModalOpen(false)} />
|
||||
<AgentModal isOpen={isAgentModalOpen} onClose={onAgentModalClose} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -189,10 +189,7 @@ const Container = styled.div`
|
||||
background-color: var(--color-background);
|
||||
}
|
||||
[navbar-position='top'] & {
|
||||
height: calc(100vh - var(--navbar-height) - 12px);
|
||||
&.right {
|
||||
height: calc(100vh - var(--navbar-height) - var(--navbar-height) - 12px);
|
||||
}
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
}
|
||||
overflow: hidden;
|
||||
.collapsed {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { BreadcrumbItem, Breadcrumbs, Chip, cn } from '@heroui/react'
|
||||
import { BreadcrumbItem, Breadcrumbs, cn } from '@heroui/react'
|
||||
import HorizontalScrollContainer from '@renderer/components/HorizontalScrollContainer'
|
||||
import { permissionModeCards } from '@renderer/constants/permissionModes'
|
||||
import { useActiveAgent } from '@renderer/hooks/agents/useActiveAgent'
|
||||
import { useActiveSession } from '@renderer/hooks/agents/useActiveSession'
|
||||
import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { AgentEntity, AgentSessionEntity, ApiModel, Assistant, PermissionMode } from '@renderer/types'
|
||||
import { AgentEntity, AgentSessionEntity, ApiModel, Assistant } from '@renderer/types'
|
||||
import { formatErrorMessageWithPrefix } from '@renderer/utils/error'
|
||||
import { t } from 'i18next'
|
||||
import { Folder } from 'lucide-react'
|
||||
import { FC, ReactNode, useCallback } from 'react'
|
||||
|
||||
import { AgentSettingsPopup, SessionSettingsPopup } from '../../settings/AgentSettings'
|
||||
@@ -38,24 +38,15 @@ const ChatNavbarContent: FC<Props> = ({ assistant }) => {
|
||||
<>
|
||||
{activeTopicOrSession === 'topic' && <SelectModelButton assistant={assistant} />}
|
||||
{activeTopicOrSession === 'session' && activeAgent && (
|
||||
<HorizontalScrollContainer>
|
||||
<Breadcrumbs
|
||||
classNames={{
|
||||
base: 'flex',
|
||||
list: 'flex-nowrap'
|
||||
}}>
|
||||
<HorizontalScrollContainer className="ml-2 flex-initial">
|
||||
<Breadcrumbs classNames={{ base: 'flex', list: 'flex-nowrap' }}>
|
||||
<BreadcrumbItem
|
||||
onPress={() => AgentSettingsPopup.show({ agentId: activeAgent.id })}
|
||||
classNames={{
|
||||
base: 'self-stretch',
|
||||
item: 'h-full'
|
||||
}}>
|
||||
<Chip size="md" variant="light" className="h-full transition-background hover:bg-foreground-100">
|
||||
<AgentLabel
|
||||
agent={activeAgent}
|
||||
classNames={{ name: 'max-w-40 font-bold text-xs', avatar: 'h-4.5 w-4.5', container: 'gap-1.5' }}
|
||||
/>
|
||||
</Chip>
|
||||
classNames={{ base: 'self-stretch', item: 'h-full' }}>
|
||||
<AgentLabel
|
||||
agent={activeAgent}
|
||||
classNames={{ name: 'max-w-40 text-xs', avatar: 'h-4.5 w-4.5', container: 'gap-1.5' }}
|
||||
/>
|
||||
</BreadcrumbItem>
|
||||
{activeSession && (
|
||||
<BreadcrumbItem
|
||||
@@ -65,13 +56,8 @@ const ChatNavbarContent: FC<Props> = ({ assistant }) => {
|
||||
sessionId: activeSession.id
|
||||
})
|
||||
}
|
||||
classNames={{
|
||||
base: 'self-stretch',
|
||||
item: 'h-full'
|
||||
}}>
|
||||
<Chip size="md" variant="light" className="h-full transition-background hover:bg-foreground-100">
|
||||
<SessionLabel session={activeSession} className="max-w-40 font-bold text-xs" />
|
||||
</Chip>
|
||||
classNames={{ base: 'self-stretch', item: 'h-full' }}>
|
||||
<SessionLabel session={activeSession} className="max-w-40 text-xs" />
|
||||
</BreadcrumbItem>
|
||||
)}
|
||||
{activeSession && (
|
||||
@@ -97,11 +83,11 @@ const SessionWorkspaceMeta: FC<{ agent: AgentEntity; session: AgentSessionEntity
|
||||
}
|
||||
|
||||
const firstAccessiblePath = session.accessible_paths?.[0]
|
||||
const permissionMode = (session.configuration?.permission_mode ?? 'default') as PermissionMode
|
||||
const permissionModeCard = permissionModeCards.find((card) => card.mode === permissionMode)
|
||||
const permissionModeLabel = permissionModeCard
|
||||
? t(permissionModeCard.titleKey, permissionModeCard.titleFallback)
|
||||
: permissionMode
|
||||
// const permissionMode = (session.configuration?.permission_mode ?? 'default') as PermissionMode
|
||||
// const permissionModeCard = permissionModeCards.find((card) => card.mode === permissionMode)
|
||||
// const permissionModeLabel = permissionModeCard
|
||||
// ? t(permissionModeCard.titleKey, permissionModeCard.titleFallback)
|
||||
// : permissionMode
|
||||
|
||||
const infoItems: ReactNode[] = []
|
||||
|
||||
@@ -117,12 +103,13 @@ const SessionWorkspaceMeta: FC<{ agent: AgentEntity; session: AgentSessionEntity
|
||||
}) => (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-medium border border-default-200 px-2 py-1 text-foreground-500 text-xs dark:text-foreground-400',
|
||||
'flex items-center gap-1.5 text-foreground-500 text-xs dark:text-foreground-400',
|
||||
onClick !== undefined ? 'cursor-pointer' : undefined,
|
||||
className
|
||||
)}
|
||||
title={text}
|
||||
onClick={onClick}>
|
||||
<Folder className="h-3.5 w-3.5 shrink-0" />
|
||||
<span className="block truncate">{text}</span>
|
||||
</div>
|
||||
)
|
||||
@@ -148,7 +135,7 @@ const SessionWorkspaceMeta: FC<{ agent: AgentEntity; session: AgentSessionEntity
|
||||
)
|
||||
}
|
||||
|
||||
infoItems.push(<InfoTag key="permission-mode" text={permissionModeLabel} className="max-w-50" />)
|
||||
// infoItems.push(<InfoTag key="permission-mode" text={permissionModeLabel} className="max-w-50" />)
|
||||
|
||||
if (infoItems.length === 0) {
|
||||
return null
|
||||
|
||||
@@ -38,12 +38,12 @@ const SelectAgentBaseModelButton: FC<Props> = ({ agentBase: agent, onSelect, isD
|
||||
<Button
|
||||
size="sm"
|
||||
variant="light"
|
||||
className="nodrag rounded-2xl px-1 py-3"
|
||||
className="nodrag h-[28px] rounded-2xl px-1"
|
||||
onPress={onSelectModel}
|
||||
isDisabled={isDisabled}>
|
||||
<div className="flex items-center gap-1.5 overflow-x-hidden">
|
||||
<ModelAvatar model={model ? apiModelAdapter(model) : undefined} size={20} />
|
||||
<span className="truncate font-medium">
|
||||
<span className="truncate text-[var(--color-text)]">
|
||||
{model ? model.name : t('button.select_model')} {providerName ? ' | ' + providerName : ''}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -87,6 +87,7 @@ const ButtonContent = styled.div`
|
||||
const ModelName = styled.span`
|
||||
font-weight: 500;
|
||||
margin-right: -2px;
|
||||
font-size: 12px;
|
||||
`
|
||||
|
||||
export default SelectModelButton
|
||||
|
||||
@@ -7,7 +7,7 @@ import TabsService from '@renderer/services/TabsService'
|
||||
import { getWebviewLoaded, onWebviewStateChange, setWebviewLoaded } from '@renderer/utils/webviewStateManager'
|
||||
import { Avatar } from 'antd'
|
||||
import { WebviewTag } from 'electron'
|
||||
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { FC, startTransition, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import BeatLoader from 'react-spinners/BeatLoader'
|
||||
import styled from 'styled-components'
|
||||
@@ -28,7 +28,9 @@ const MinAppPage: FC = () => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
// Remember the initial navbar position when component mounts
|
||||
const initialIsTopNavbar = useRef<boolean>(isTopNavbar)
|
||||
// It's immutable state
|
||||
const [initialIsTopNavbar] = useState<boolean>(isTopNavbar)
|
||||
const initialIsTopNavbarRef = useRef<boolean>(initialIsTopNavbar)
|
||||
const hasRedirected = useRef<boolean>(false)
|
||||
|
||||
// Initialize TabsService with cache reference
|
||||
@@ -40,8 +42,8 @@ const MinAppPage: FC = () => {
|
||||
|
||||
// Debug: track navbar position changes
|
||||
useEffect(() => {
|
||||
if (initialIsTopNavbar.current !== isTopNavbar) {
|
||||
logger.debug(`NavBar position changed from ${initialIsTopNavbar.current} to ${isTopNavbar}`)
|
||||
if (initialIsTopNavbarRef.current !== isTopNavbar) {
|
||||
logger.debug(`NavBar position changed from ${initialIsTopNavbarRef.current} to ${isTopNavbar}`)
|
||||
}
|
||||
}, [isTopNavbar])
|
||||
|
||||
@@ -69,7 +71,7 @@ const MinAppPage: FC = () => {
|
||||
|
||||
// For sidebar navigation, redirect to apps list and open popup
|
||||
// Only check once and only if we haven't already redirected
|
||||
if (!initialIsTopNavbar.current && !hasRedirected.current) {
|
||||
if (!initialIsTopNavbarRef.current && !hasRedirected.current) {
|
||||
hasRedirected.current = true
|
||||
navigate('/apps')
|
||||
// Open popup after navigation
|
||||
@@ -80,15 +82,20 @@ const MinAppPage: FC = () => {
|
||||
}
|
||||
|
||||
// For top navbar mode, integrate with cache system
|
||||
if (initialIsTopNavbar.current) {
|
||||
if (initialIsTopNavbarRef.current) {
|
||||
// 无论是否已在缓存,都调用以确保 currentMinappId 同步到路由切换的新 appId
|
||||
openMinappKeepAlive(app)
|
||||
}
|
||||
}, [app, navigate, openMinappKeepAlive, initialIsTopNavbar])
|
||||
}, [app, navigate, openMinappKeepAlive])
|
||||
|
||||
// -------------- 新的 Tab Shell 逻辑 --------------
|
||||
// 注意:Hooks 必须在任何 return 之前调用,因此提前定义,并在内部判空
|
||||
const webviewRef = useRef<WebviewTag | null>(null)
|
||||
const [webview, setWebview] = useState<WebviewTag | null>(null)
|
||||
const webviewRef = useRef<WebviewTag | null>(webview)
|
||||
useEffect(() => {
|
||||
webviewRef.current = webview
|
||||
}, [webview])
|
||||
|
||||
const [isReady, setIsReady] = useState<boolean>(() => (app ? getWebviewLoaded(app.id) : false))
|
||||
const [currentUrl, setCurrentUrl] = useState<string | null>(app?.url ?? null)
|
||||
|
||||
@@ -103,7 +110,7 @@ const MinAppPage: FC = () => {
|
||||
|
||||
if (webviewRef.current === el) return true // 已附着
|
||||
|
||||
webviewRef.current = el
|
||||
setWebview(el)
|
||||
const handleInPageNav = (e: any) => setCurrentUrl(e.url)
|
||||
el.addEventListener('did-navigate-in-page', handleInPageNav)
|
||||
webviewCleanupRef.current = () => {
|
||||
@@ -137,7 +144,10 @@ const MinAppPage: FC = () => {
|
||||
if (!app) return
|
||||
if (getWebviewLoaded(app.id)) {
|
||||
// 已经加载
|
||||
if (!isReady) setIsReady(true)
|
||||
if (!isReady)
|
||||
startTransition(() => {
|
||||
setIsReady(true)
|
||||
})
|
||||
return
|
||||
}
|
||||
let mounted = true
|
||||
@@ -155,7 +165,7 @@ const MinAppPage: FC = () => {
|
||||
}, [app, isReady])
|
||||
|
||||
// 如果条件不满足,提前返回(所有 hooks 已调用)
|
||||
if (!app || !initialIsTopNavbar.current) {
|
||||
if (!app || !initialIsTopNavbar) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -185,7 +195,7 @@ const MinAppPage: FC = () => {
|
||||
onOpenDevTools={handleOpenDevTools}
|
||||
/>
|
||||
</ToolbarWrapper>
|
||||
<WebviewSearch webviewRef={webviewRef} isWebviewReady={isReady} appId={app.id} />
|
||||
<WebviewSearch activeWebview={webview} isWebviewReady={isReady} appId={app.id} />
|
||||
{!isReady && (
|
||||
<LoadingMask>
|
||||
<Avatar src={app.logo} size={60} style={{ border: '1px solid var(--color-border)' }} />
|
||||
|
||||
@@ -8,14 +8,14 @@ import { useTranslation } from 'react-i18next'
|
||||
type FoundInPageResult = Electron.FoundInPageResult
|
||||
|
||||
interface WebviewSearchProps {
|
||||
webviewRef: React.RefObject<WebviewTag | null>
|
||||
activeWebview: WebviewTag | null
|
||||
isWebviewReady: boolean
|
||||
appId: string
|
||||
}
|
||||
|
||||
const logger = loggerService.withContext('WebviewSearch')
|
||||
|
||||
const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, appId }) => {
|
||||
const WebviewSearch: FC<WebviewSearchProps> = ({ activeWebview, isWebviewReady, appId }) => {
|
||||
const { t } = useTranslation()
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
const [query, setQuery] = useState('')
|
||||
@@ -25,7 +25,6 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
|
||||
const focusFrameRef = useRef<number | null>(null)
|
||||
const lastAppIdRef = useRef<string>(appId)
|
||||
const attachedWebviewRef = useRef<WebviewTag | null>(null)
|
||||
const activeWebview = webviewRef.current ?? null
|
||||
|
||||
const focusInput = useCallback(() => {
|
||||
if (focusFrameRef.current !== null) {
|
||||
@@ -81,7 +80,7 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
|
||||
)
|
||||
|
||||
const getUsableWebview = useCallback(() => {
|
||||
const candidates = [webviewRef.current, attachedWebviewRef.current]
|
||||
const candidates = [activeWebview, attachedWebviewRef.current]
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const usable = ensureWebviewReady(candidate)
|
||||
@@ -91,7 +90,7 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
|
||||
}
|
||||
|
||||
return null
|
||||
}, [ensureWebviewReady, webviewRef])
|
||||
}, [ensureWebviewReady, activeWebview])
|
||||
|
||||
const stopSearch = useCallback(() => {
|
||||
const target = getUsableWebview()
|
||||
|
||||
@@ -136,9 +136,8 @@ describe('WebviewSearch', () => {
|
||||
|
||||
it('opens the search overlay with keyboard shortcut', async () => {
|
||||
const { webview } = createWebviewMock()
|
||||
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
||||
|
||||
render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
|
||||
render(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
|
||||
|
||||
expect(screen.queryByPlaceholderText('Search')).not.toBeInTheDocument()
|
||||
|
||||
@@ -149,9 +148,8 @@ describe('WebviewSearch', () => {
|
||||
|
||||
it('opens the search overlay when webview shortcut is forwarded', async () => {
|
||||
const { webview } = createWebviewMock()
|
||||
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
||||
|
||||
render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
|
||||
render(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onFindShortcutMock).toHaveBeenCalled()
|
||||
@@ -170,13 +168,12 @@ describe('WebviewSearch', () => {
|
||||
;(webview as any).getWebContentsId = vi.fn(() => {
|
||||
throw error
|
||||
})
|
||||
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
||||
|
||||
const getWebContentsIdMock = vi.fn(() => {
|
||||
throw error
|
||||
})
|
||||
;(webview as any).getWebContentsId = getWebContentsIdMock
|
||||
const { rerender } = render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
|
||||
const { rerender } = render(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getWebContentsIdMock).toHaveBeenCalled()
|
||||
@@ -185,8 +182,8 @@ describe('WebviewSearch', () => {
|
||||
|
||||
;(webview as any).getWebContentsId = vi.fn(() => 1)
|
||||
|
||||
rerender(<WebviewSearch webviewRef={webviewRef} isWebviewReady={false} appId="app-1" />)
|
||||
rerender(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
|
||||
rerender(<WebviewSearch activeWebview={webview} isWebviewReady={false} appId="app-1" />)
|
||||
rerender(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onFindShortcutMock).toHaveBeenCalled()
|
||||
@@ -200,9 +197,8 @@ describe('WebviewSearch', () => {
|
||||
throw error
|
||||
})
|
||||
;(webview as any).getWebContentsId = getWebContentsIdMock
|
||||
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
||||
|
||||
const { rerender, unmount } = render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
|
||||
const { rerender, unmount } = render(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getWebContentsIdMock).toHaveBeenCalled()
|
||||
@@ -212,7 +208,7 @@ describe('WebviewSearch', () => {
|
||||
throw new Error('should not be called')
|
||||
})
|
||||
|
||||
rerender(<WebviewSearch webviewRef={webviewRef} isWebviewReady={false} appId="app-1" />)
|
||||
rerender(<WebviewSearch activeWebview={webview} isWebviewReady={false} appId="app-1" />)
|
||||
expect(stopFindInPageMock).not.toHaveBeenCalled()
|
||||
|
||||
unmount()
|
||||
@@ -221,9 +217,8 @@ describe('WebviewSearch', () => {
|
||||
|
||||
it('closes the search overlay when escape is forwarded from the webview', async () => {
|
||||
const { webview } = createWebviewMock()
|
||||
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
||||
|
||||
render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
|
||||
render(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onFindShortcutMock).toHaveBeenCalled()
|
||||
@@ -245,10 +240,9 @@ describe('WebviewSearch', () => {
|
||||
|
||||
it('performs searches and navigates between results', async () => {
|
||||
const { emit, findInPageMock, webview } = createWebviewMock()
|
||||
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
|
||||
render(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
|
||||
await openSearchOverlay()
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
@@ -286,10 +280,9 @@ describe('WebviewSearch', () => {
|
||||
|
||||
it('navigates results when enter is forwarded from the webview', async () => {
|
||||
const { findInPageMock, webview } = createWebviewMock()
|
||||
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
|
||||
render(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onFindShortcutMock).toHaveBeenCalled()
|
||||
@@ -325,10 +318,9 @@ describe('WebviewSearch', () => {
|
||||
|
||||
it('clears search state when appId changes', async () => {
|
||||
const { findInPageMock, stopFindInPageMock, webview } = createWebviewMock()
|
||||
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
||||
const user = userEvent.setup()
|
||||
|
||||
const { rerender } = render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
|
||||
const { rerender } = render(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
|
||||
await openSearchOverlay()
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
@@ -338,7 +330,7 @@ describe('WebviewSearch', () => {
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
rerender(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-2" />)
|
||||
rerender(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-2" />)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -352,10 +344,9 @@ describe('WebviewSearch', () => {
|
||||
findInPageMock.mockImplementation(() => {
|
||||
throw new Error('findInPage failed')
|
||||
})
|
||||
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
|
||||
render(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
|
||||
await openSearchOverlay()
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
@@ -368,9 +359,8 @@ describe('WebviewSearch', () => {
|
||||
|
||||
it('stops search when component unmounts', async () => {
|
||||
const { stopFindInPageMock, webview } = createWebviewMock()
|
||||
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
||||
|
||||
const { unmount } = render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
|
||||
const { unmount } = render(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
|
||||
await openSearchOverlay()
|
||||
|
||||
stopFindInPageMock.mockClear()
|
||||
@@ -382,9 +372,8 @@ describe('WebviewSearch', () => {
|
||||
|
||||
it('ignores keyboard shortcut when webview is not ready', async () => {
|
||||
const { findInPageMock, webview } = createWebviewMock()
|
||||
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
||||
|
||||
render(<WebviewSearch webviewRef={webviewRef} isWebviewReady={false} appId="app-1" />)
|
||||
render(<WebviewSearch activeWebview={webview} isWebviewReady={false} appId="app-1" />)
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.keyDown(window, { key: 'f', ctrlKey: true })
|
||||
|
||||
@@ -9,7 +9,7 @@ import { findNode } from '@renderer/services/NotesTreeService'
|
||||
import { Dropdown, Input, Tooltip } from 'antd'
|
||||
import { t } from 'i18next'
|
||||
import { MoreHorizontal, PanelLeftClose, PanelRightClose, Star } from 'lucide-react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { startTransition, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { menuItems } from './MenuConfig'
|
||||
@@ -19,9 +19,6 @@ const logger = loggerService.withContext('HeaderNavbar')
|
||||
const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar, onExpandPath, onRenameNode }) => {
|
||||
const { showWorkspace, toggleShowWorkspace } = useShowWorkspace()
|
||||
const { activeNode } = useActiveNode(notesTree)
|
||||
const [breadcrumbItems, setBreadcrumbItems] = useState<
|
||||
Array<{ key: string; title: string; treePath: string; isFolder: boolean }>
|
||||
>([])
|
||||
const [titleValue, setTitleValue] = useState('')
|
||||
const titleInputRef = useRef<any>(null)
|
||||
const { settings, updateSettings } = useNotesSettings()
|
||||
@@ -141,18 +138,17 @@ const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar, onExpand
|
||||
// 同步标题值
|
||||
useEffect(() => {
|
||||
if (activeNode?.type === 'file') {
|
||||
setTitleValue(activeNode.name.replace('.md', ''))
|
||||
startTransition(() => setTitleValue(activeNode.name.replace('.md', '')))
|
||||
}
|
||||
}, [activeNode])
|
||||
|
||||
// 构建面包屑路径
|
||||
useEffect(() => {
|
||||
const breadcrumbItems = useMemo(() => {
|
||||
if (!activeNode || !notesTree) {
|
||||
setBreadcrumbItems([])
|
||||
return
|
||||
return []
|
||||
}
|
||||
const node = findNode(notesTree, activeNode.id)
|
||||
if (!node) return
|
||||
if (!node) return []
|
||||
|
||||
const pathParts = node.treePath.split('/').filter(Boolean)
|
||||
const items = pathParts.map((part, index) => {
|
||||
@@ -166,7 +162,7 @@ const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar, onExpand
|
||||
}
|
||||
})
|
||||
|
||||
setBreadcrumbItems(items)
|
||||
return items
|
||||
}, [activeNode, notesTree])
|
||||
|
||||
return (
|
||||
|
||||
@@ -63,39 +63,6 @@ const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ tab, ag
|
||||
] as const satisfies { key: AgentSettingPopupTab; label: string }[]
|
||||
).filter(Boolean)
|
||||
|
||||
const ModalContent = () => {
|
||||
if (isLoading) {
|
||||
// TODO: use skeleton for better ux
|
||||
return <Spinner />
|
||||
}
|
||||
if (error) {
|
||||
return (
|
||||
<div>
|
||||
<Alert color="danger" title={t('agent.get.error.failed')} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="flex w-full flex-1">
|
||||
<LeftMenu>
|
||||
<StyledMenu
|
||||
defaultSelectedKeys={[tab || 'essential'] satisfies AgentSettingPopupTab[]}
|
||||
mode="vertical"
|
||||
selectedKeys={[menu]}
|
||||
items={items}
|
||||
onSelect={({ key }) => setMenu(key as AgentSettingPopupTab)}
|
||||
/>
|
||||
</LeftMenu>
|
||||
<Settings>
|
||||
{menu === 'essential' && <EssentialSettings agentBase={agent} update={updateAgent} />}
|
||||
{menu === 'prompt' && <PromptSettings agentBase={agent} update={updateAgent} />}
|
||||
{menu === 'tooling' && <ToolingSettings agentBase={agent} update={updateAgent} />}
|
||||
{menu === 'advanced' && <AdvancedSettings agentBase={agent} update={updateAgent} />}
|
||||
</Settings>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledModal
|
||||
open={open}
|
||||
@@ -104,7 +71,7 @@ const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ tab, ag
|
||||
afterClose={afterClose}
|
||||
maskClosable={false}
|
||||
footer={null}
|
||||
title={<AgentLabel agent={agent} classNames={{ name: 'text-lg font-extrabold' }} />}
|
||||
title={<AgentLabel agent={agent} />}
|
||||
transitionName="animation-move-down"
|
||||
styles={{
|
||||
content: {
|
||||
@@ -129,7 +96,31 @@ const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ tab, ag
|
||||
}}
|
||||
width="min(800px, 70vw)"
|
||||
centered>
|
||||
<ModalContent />
|
||||
{isLoading && <Spinner />}
|
||||
{!isLoading && error && (
|
||||
<div>
|
||||
<Alert color="danger" title={t('agent.get.error.failed')} />
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && !error && (
|
||||
<div className="flex w-full flex-1">
|
||||
<LeftMenu>
|
||||
<StyledMenu
|
||||
defaultSelectedKeys={[tab || 'essential'] satisfies AgentSettingPopupTab[]}
|
||||
mode="vertical"
|
||||
selectedKeys={[menu]}
|
||||
items={items}
|
||||
onSelect={({ key }) => setMenu(key as AgentSettingPopupTab)}
|
||||
/>
|
||||
</LeftMenu>
|
||||
<Settings>
|
||||
{menu === 'essential' && <EssentialSettings agentBase={agent} update={updateAgent} />}
|
||||
{menu === 'prompt' && <PromptSettings agentBase={agent} update={updateAgent} />}
|
||||
{menu === 'tooling' && <ToolingSettings agentBase={agent} update={updateAgent} />}
|
||||
{menu === 'advanced' && <AdvancedSettings agentBase={agent} update={updateAgent} />}
|
||||
</Settings>
|
||||
</div>
|
||||
)}
|
||||
</StyledModal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,13 +20,15 @@ type EssentialSettingsProps =
|
||||
| {
|
||||
agentBase: GetAgentResponse | undefined | null
|
||||
update: ReturnType<typeof useUpdateAgent>['updateAgent']
|
||||
showModelSetting?: boolean
|
||||
}
|
||||
| {
|
||||
agentBase: GetAgentSessionResponse | undefined | null
|
||||
update: ReturnType<typeof useUpdateSession>['updateSession']
|
||||
showModelSetting?: boolean
|
||||
}
|
||||
|
||||
const EssentialSettings: FC<EssentialSettingsProps> = ({ agentBase, update }) => {
|
||||
const EssentialSettings: FC<EssentialSettingsProps> = ({ agentBase, update, showModelSetting = true }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!agentBase) return null
|
||||
@@ -46,7 +48,7 @@ const EssentialSettings: FC<EssentialSettingsProps> = ({ agentBase, update }) =>
|
||||
)}
|
||||
{isAgent && <AvatarSetting agent={agentBase} update={update} />}
|
||||
<NameSetting base={agentBase} update={update} />
|
||||
<ModelSetting base={agentBase} update={update} />
|
||||
{showModelSetting && <ModelSetting base={agentBase} update={update} />}
|
||||
<AccessibleDirsSetting base={agentBase} update={update} />
|
||||
<DescriptionSetting base={agentBase} update={update} />
|
||||
</SettingsContainer>
|
||||
|
||||
@@ -65,39 +65,6 @@ const SessionSettingPopupContainer: React.FC<SessionSettingPopupParams> = ({ tab
|
||||
] as const satisfies { key: AgentSettingPopupTab; label: string }[]
|
||||
).filter(Boolean)
|
||||
|
||||
const ModalContent = () => {
|
||||
if (isLoading) {
|
||||
// TODO: use skeleton for better ux
|
||||
return <Spinner />
|
||||
}
|
||||
if (error) {
|
||||
return (
|
||||
<div>
|
||||
<Alert color="danger" title={t('agent.get.error.failed')} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="flex w-full flex-1">
|
||||
<LeftMenu>
|
||||
<StyledMenu
|
||||
defaultSelectedKeys={[tab || 'essential'] satisfies AgentSettingPopupTab[]}
|
||||
mode="vertical"
|
||||
selectedKeys={[menu]}
|
||||
items={items}
|
||||
onSelect={({ key }) => setMenu(key as AgentSettingPopupTab)}
|
||||
/>
|
||||
</LeftMenu>
|
||||
<Settings>
|
||||
{menu === 'essential' && <EssentialSettings agentBase={session} update={updateSession} />}
|
||||
{menu === 'prompt' && <PromptSettings agentBase={session} update={updateSession} />}
|
||||
{menu === 'tooling' && <ToolingSettings agentBase={session} update={updateSession} />}
|
||||
{menu === 'advanced' && <AdvancedSettings agentBase={session} update={updateSession} />}
|
||||
</Settings>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledModal
|
||||
open={open}
|
||||
@@ -106,7 +73,7 @@ const SessionSettingPopupContainer: React.FC<SessionSettingPopupParams> = ({ tab
|
||||
afterClose={afterClose}
|
||||
maskClosable={false}
|
||||
footer={null}
|
||||
title={<SessionLabel session={session} className="font-extrabold text-lg" />}
|
||||
title={<SessionLabel session={session} />}
|
||||
transitionName="animation-move-down"
|
||||
styles={{
|
||||
content: {
|
||||
@@ -125,7 +92,31 @@ const SessionSettingPopupContainer: React.FC<SessionSettingPopupParams> = ({ tab
|
||||
}}
|
||||
width="min(800px, 70vw)"
|
||||
centered>
|
||||
<ModalContent />
|
||||
{isLoading && <Spinner />}
|
||||
{!isLoading && error && (
|
||||
<div>
|
||||
<Alert color="danger" title={t('agent.get.error.failed')} />
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && !error && (
|
||||
<div className="flex w-full flex-1">
|
||||
<LeftMenu>
|
||||
<StyledMenu
|
||||
defaultSelectedKeys={[tab || 'essential'] satisfies AgentSettingPopupTab[]}
|
||||
mode="vertical"
|
||||
selectedKeys={[menu]}
|
||||
items={items}
|
||||
onSelect={({ key }) => setMenu(key as AgentSettingPopupTab)}
|
||||
/>
|
||||
</LeftMenu>
|
||||
<Settings>
|
||||
{menu === 'essential' && <EssentialSettings agentBase={session} update={updateSession} />}
|
||||
{menu === 'prompt' && <PromptSettings agentBase={session} update={updateSession} />}
|
||||
{menu === 'tooling' && <ToolingSettings agentBase={session} update={updateSession} />}
|
||||
{menu === 'advanced' && <AdvancedSettings agentBase={session} update={updateSession} />}
|
||||
</Settings>
|
||||
</div>
|
||||
)}
|
||||
</StyledModal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export const AgentLabel: React.FC<AgentLabelProps> = ({ agent, classNames }) =>
|
||||
return (
|
||||
<div className={cn('flex w-full items-center gap-2 truncate', classNames?.container)}>
|
||||
<EmojiIcon emoji={emoji || '⭐️'} className={classNames?.avatar} />
|
||||
<span className={cn('truncate', classNames?.name)}>
|
||||
<span className={cn('truncate', 'text-[var(--color-text)]', classNames?.name)}>
|
||||
{agent?.name ?? (agent?.type ? getAgentTypeLabel(agent.type) : '')}
|
||||
</span>
|
||||
</div>
|
||||
@@ -52,7 +52,7 @@ export const SessionLabel: React.FC<SessionLabelProps> = ({ session, className }
|
||||
const displayName = session?.name ?? session?.id
|
||||
return (
|
||||
<>
|
||||
<span className={cn('truncate px-2 text-sm', className)}>{displayName}</span>
|
||||
<span className={cn('truncate text-[var(--color-text)] text-sm', className)}>{displayName}</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import FileItem from '@renderer/pages/files/FileItem'
|
||||
import { Assistant, QuickPhrase } from '@renderer/types'
|
||||
import { Button, Flex, Input, Modal, Popconfirm, Space } from 'antd'
|
||||
import { PlusIcon } from 'lucide-react'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { FC, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
@@ -21,15 +21,12 @@ interface AssistantRegularPromptsSettingsProps {
|
||||
|
||||
const AssistantRegularPromptsSettings: FC<AssistantRegularPromptsSettingsProps> = ({ assistant, updateAssistant }) => {
|
||||
const { t } = useTranslation()
|
||||
const [promptsList, setPromptsList] = useState<QuickPhrase[]>([])
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
const [editingPrompt, setEditingPrompt] = useState<QuickPhrase | null>(null)
|
||||
const [formData, setFormData] = useState({ title: '', content: '' })
|
||||
const [dragging, setDragging] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setPromptsList(assistant.regularPhrases || [])
|
||||
}, [assistant.regularPhrases])
|
||||
const promptsList: QuickPhrase[] = useMemo(() => assistant.regularPhrases || [], [assistant.regularPhrases])
|
||||
|
||||
const handleAdd = () => {
|
||||
setEditingPrompt(null)
|
||||
@@ -45,7 +42,6 @@ const AssistantRegularPromptsSettings: FC<AssistantRegularPromptsSettingsProps>
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
const updatedPrompts = promptsList.filter((prompt) => prompt.id !== id)
|
||||
setPromptsList(updatedPrompts)
|
||||
updateAssistant({ ...assistant, regularPhrases: updatedPrompts })
|
||||
}
|
||||
|
||||
@@ -68,13 +64,11 @@ const AssistantRegularPromptsSettings: FC<AssistantRegularPromptsSettingsProps>
|
||||
}
|
||||
updatedPrompts = [...promptsList, newPrompt]
|
||||
}
|
||||
setPromptsList(updatedPrompts)
|
||||
updateAssistant({ ...assistant, regularPhrases: updatedPrompts })
|
||||
setIsModalOpen(false)
|
||||
}
|
||||
|
||||
const handleUpdateOrder = async (newPrompts: QuickPhrase[]) => {
|
||||
setPromptsList(newPrompts)
|
||||
updateAssistant({ ...assistant, regularPhrases: newPrompts })
|
||||
}
|
||||
|
||||
|
||||
@@ -27,25 +27,6 @@ const OcrProviderSettings = ({ provider }: Props) => {
|
||||
return null
|
||||
}
|
||||
|
||||
const ProviderSettings = () => {
|
||||
if (isBuiltinOcrProvider(provider)) {
|
||||
switch (provider.id) {
|
||||
case 'tesseract':
|
||||
return <OcrTesseractSettings />
|
||||
case 'system':
|
||||
return <OcrSystemSettings />
|
||||
case 'paddleocr':
|
||||
return <OcrPpocrSettings />
|
||||
case 'ovocr':
|
||||
return <OcrOVSettings />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
throw new Error('Not supported OCR provider')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingGroup theme={themeMode}>
|
||||
<SettingTitle>
|
||||
@@ -56,7 +37,7 @@ const OcrProviderSettings = ({ provider }: Props) => {
|
||||
</SettingTitle>
|
||||
<Divider style={{ width: '100%', margin: '10px 0' }} />
|
||||
<ErrorBoundary>
|
||||
<ProviderSettings />
|
||||
<ProviderSettings provider={provider} />
|
||||
</ErrorBoundary>
|
||||
</SettingGroup>
|
||||
)
|
||||
@@ -67,4 +48,23 @@ const ProviderName = styled.span`
|
||||
font-weight: 500;
|
||||
`
|
||||
|
||||
const ProviderSettings = ({ provider }: { provider: OcrProvider }) => {
|
||||
if (isBuiltinOcrProvider(provider)) {
|
||||
switch (provider.id) {
|
||||
case 'tesseract':
|
||||
return <OcrTesseractSettings />
|
||||
case 'system':
|
||||
return <OcrSystemSettings />
|
||||
case 'paddleocr':
|
||||
return <OcrPpocrSettings />
|
||||
case 'ovocr':
|
||||
return <OcrOVSettings />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
throw new Error('Not supported OCR provider')
|
||||
}
|
||||
}
|
||||
|
||||
export default OcrProviderSettings
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { TraceModal } from '@renderer/trace/pages/TraceModel'
|
||||
import { Divider } from 'antd/lib'
|
||||
import dayjs from 'dayjs'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
|
||||
import { Box, GridItem, HStack, IconButton, SimpleGrid, Text } from './Component'
|
||||
import { ProgressBar } from './ProgressBar'
|
||||
@@ -38,12 +39,10 @@ export const convertTime = (time: number | null): string => {
|
||||
const TreeNode: React.FC<TreeNodeProps> = ({ node, handleClick, treeData, paddingLeft = 2 }) => {
|
||||
const [isOpen, setIsOpen] = useState(true)
|
||||
const hasChildren = node.children && node.children.length > 0
|
||||
const [usedTime, setUsedTime] = useState('--')
|
||||
|
||||
// 只在 endTime 或 node 变化时更新 usedTime
|
||||
useEffect(() => {
|
||||
const endTime = node.endTime || Date.now()
|
||||
setUsedTime(convertTime(endTime - node.startTime))
|
||||
const usedTime = useMemo(() => {
|
||||
const endTime = node.endTime || dayjs().valueOf()
|
||||
return convertTime(endTime - node.startTime)
|
||||
}, [node])
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,9 +3,10 @@ import './Trace.css'
|
||||
import { SpanEntity } from '@mcp-trace/trace-core'
|
||||
import { TraceModal } from '@renderer/trace/pages/TraceModel'
|
||||
import { Divider } from 'antd/lib'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { findNodeById, mergeTraceModals, updatePercentAndStart } from '../utils'
|
||||
import { Box, GridItem, SimpleGrid, Text, VStack } from './Component'
|
||||
import SpanDetail from './SpanDetail'
|
||||
import TraceTree from './TraceTree'
|
||||
@@ -19,44 +20,12 @@ export interface TracePageProp {
|
||||
|
||||
export const TracePage: React.FC<TracePageProp> = ({ topicId, traceId, modelName, reload = false }) => {
|
||||
const [spans, setSpans] = useState<TraceModal[]>([])
|
||||
const [selectNode, setSelectNode] = useState<TraceModal | null>(null)
|
||||
const [showList, setShowList] = useState(true)
|
||||
const [selectNodeId, setSelectNodeId] = useState<string | null>(null)
|
||||
const selectNode = useMemo(() => (selectNodeId ? findNodeById(spans, selectNodeId) : null), [selectNodeId, spans])
|
||||
const showList = useMemo(() => selectNodeId === null || !selectNode, [selectNode, selectNodeId])
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const mergeTraceModals = useCallback((oldNodes: TraceModal[], newNodes: TraceModal[]): TraceModal[] => {
|
||||
const oldMap = new Map(oldNodes.map((n) => [n.id, n]))
|
||||
return newNodes.map((newNode) => {
|
||||
const oldNode = oldMap.get(newNode.id)
|
||||
if (oldNode) {
|
||||
// 如果旧节点已经结束,则直接返回旧节点
|
||||
if (oldNode.endTime) {
|
||||
return oldNode
|
||||
}
|
||||
oldNode.children = mergeTraceModals(oldNode.children, newNode.children)
|
||||
Object.assign(oldNode, newNode)
|
||||
return oldNode
|
||||
} else {
|
||||
return newNode
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
const updatePercentAndStart = useCallback((nodes: TraceModal[], rootStart?: number, rootEnd?: number) => {
|
||||
nodes.forEach((node) => {
|
||||
const _rootStart = rootStart || node.startTime
|
||||
const _rootEnd = rootEnd || node.endTime || Date.now()
|
||||
const endTime = node.endTime || _rootEnd
|
||||
const usedTime = endTime - node.startTime
|
||||
const duration = _rootEnd - _rootStart
|
||||
node.start = ((node.startTime - _rootStart) * 100) / duration
|
||||
node.percent = duration === 0 ? 0 : (usedTime * 100) / duration
|
||||
if (node.children) {
|
||||
updatePercentAndStart(node.children, _rootStart, _rootEnd)
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
const getRootSpan = (spans: SpanEntity[]): TraceModal[] => {
|
||||
const map: Map<string, TraceModal> = new Map()
|
||||
|
||||
@@ -78,17 +47,6 @@ export const TracePage: React.FC<TracePageProp> = ({ topicId, traceId, modelName
|
||||
)
|
||||
}
|
||||
|
||||
const findNodeById = useCallback((nodes: TraceModal[], id: string): TraceModal | null => {
|
||||
for (const n of nodes) {
|
||||
if (n.id === id) return n
|
||||
if (n.children) {
|
||||
const found = findNodeById(n.children, id)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}, [])
|
||||
|
||||
const getTraceData = useCallback(async (): Promise<boolean> => {
|
||||
const datas = topicId && traceId ? await window.api.trace.getData(topicId, traceId, modelName) : []
|
||||
const matchedSpans = getRootSpan(datas)
|
||||
@@ -96,19 +54,14 @@ export const TracePage: React.FC<TracePageProp> = ({ topicId, traceId, modelName
|
||||
setSpans((prev) => mergeTraceModals(prev, matchedSpans))
|
||||
const isEnded = !matchedSpans.find((e) => !e.endTime || e.endTime <= 0)
|
||||
return isEnded
|
||||
}, [topicId, traceId, modelName, updatePercentAndStart, mergeTraceModals])
|
||||
}, [topicId, traceId, modelName])
|
||||
|
||||
const handleNodeClick = (nodeId: string) => {
|
||||
const latestNode = findNodeById(spans, nodeId)
|
||||
if (latestNode) {
|
||||
setSelectNode(latestNode)
|
||||
setShowList(false)
|
||||
}
|
||||
setSelectNodeId(nodeId)
|
||||
}
|
||||
|
||||
const handleShowList = () => {
|
||||
setShowList(true)
|
||||
setSelectNode(null)
|
||||
setSelectNodeId(null)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -138,18 +91,6 @@ export const TracePage: React.FC<TracePageProp> = ({ topicId, traceId, modelName
|
||||
}
|
||||
}, [getTraceData, traceId, topicId, reload])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectNode) {
|
||||
const latest = findNodeById(spans, selectNode.id)
|
||||
if (!latest) {
|
||||
setShowList(true)
|
||||
setSelectNode(null)
|
||||
} else if (latest !== selectNode) {
|
||||
setSelectNode(latest)
|
||||
}
|
||||
}
|
||||
}, [spans, selectNode, findNodeById])
|
||||
|
||||
return (
|
||||
<div className="trace-window">
|
||||
<div className="tab-container_trace">
|
||||
|
||||
45
src/renderer/src/trace/utils/index.ts
Normal file
45
src/renderer/src/trace/utils/index.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { TraceModal } from '../pages/TraceModel'
|
||||
|
||||
export const updatePercentAndStart = (nodes: TraceModal[], rootStart?: number, rootEnd?: number) => {
|
||||
nodes.forEach((node) => {
|
||||
const _rootStart = rootStart || node.startTime
|
||||
const _rootEnd = rootEnd || node.endTime || Date.now()
|
||||
const endTime = node.endTime || _rootEnd
|
||||
const usedTime = endTime - node.startTime
|
||||
const duration = _rootEnd - _rootStart
|
||||
node.start = ((node.startTime - _rootStart) * 100) / duration
|
||||
node.percent = duration === 0 ? 0 : (usedTime * 100) / duration
|
||||
if (node.children) {
|
||||
updatePercentAndStart(node.children, _rootStart, _rootEnd)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const findNodeById = (nodes: TraceModal[], id: string): TraceModal | null => {
|
||||
for (const n of nodes) {
|
||||
if (n.id === id) return n
|
||||
if (n.children) {
|
||||
const found = findNodeById(n.children, id)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export const mergeTraceModals = (oldNodes: TraceModal[], newNodes: TraceModal[]): TraceModal[] => {
|
||||
const oldMap = new Map(oldNodes.map((n) => [n.id, n]))
|
||||
return newNodes.map((newNode) => {
|
||||
const oldNode = oldMap.get(newNode.id)
|
||||
if (oldNode) {
|
||||
// 如果旧节点已经结束,则直接返回旧节点
|
||||
if (oldNode.endTime) {
|
||||
return oldNode
|
||||
}
|
||||
oldNode.children = mergeTraceModals(oldNode.children, newNode.children)
|
||||
Object.assign(oldNode, newNode)
|
||||
return oldNode
|
||||
} else {
|
||||
return newNode
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -33,15 +33,15 @@ const Footer: FC<FooterProps> = ({
|
||||
onEsc()
|
||||
})
|
||||
|
||||
useHotkeys('c', () => {
|
||||
handleCopy()
|
||||
})
|
||||
|
||||
const handleCopy = () => {
|
||||
if (loading || !onCopy) return
|
||||
onCopy()
|
||||
}
|
||||
|
||||
useHotkeys('c', () => {
|
||||
handleCopy()
|
||||
})
|
||||
|
||||
return (
|
||||
<WindowFooter className="drag">
|
||||
<FooterText>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { Input as AntdInput } from 'antd'
|
||||
import { InputRef } from 'rc-input/lib/interface'
|
||||
import React, { useRef } from 'react'
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface InputBarProps {
|
||||
@@ -27,9 +27,13 @@ const InputBar = ({
|
||||
}: InputBarProps & { ref?: React.RefObject<HTMLDivElement | null> }) => {
|
||||
const inputRef = useRef<InputRef>(null)
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
if (!loading) {
|
||||
setTimeoutTimer('focus', () => inputRef.current?.input?.focus(), 0)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
setTimeoutTimer('focus', () => inputRef.current?.input?.focus(), 0)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<InputWrapper ref={ref}>
|
||||
{assistant.model && <ModelAvatar model={assistant.model} size={30} />}
|
||||
|
||||
@@ -36,32 +36,38 @@ const ActionGeneral: FC<Props> = React.memo(({ action, scrollToBottom }) => {
|
||||
const [isContented, setIsContented] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [contentToCopy, setContentToCopy] = useState('')
|
||||
const [assistant] = useState<Assistant>(() => {
|
||||
const assistant = action.assistantId
|
||||
? getAssistantById(action.assistantId) || getDefaultAssistant()
|
||||
: getDefaultAssistant()
|
||||
if (!assistant.model) {
|
||||
return { ...assistant, model: getDefaultModel() }
|
||||
} else {
|
||||
return assistant
|
||||
}
|
||||
})
|
||||
const [topic] = useState<Topic | null>(getDefaultTopic(assistant.id))
|
||||
const initialized = useRef(false)
|
||||
|
||||
// Use useRef for values that shouldn't trigger re-renders
|
||||
const assistantRef = useRef<Assistant | null>(null)
|
||||
const topicRef = useRef<Topic | null>(null)
|
||||
const topicRef = useRef<Topic | null>(topic)
|
||||
const promptContentRef = useRef('')
|
||||
const askId = useRef('')
|
||||
|
||||
// Sync refs
|
||||
useEffect(() => {
|
||||
assistantRef.current = assistant
|
||||
}, [assistant])
|
||||
useEffect(() => {
|
||||
topicRef.current = topic
|
||||
}, [assistant, topic])
|
||||
|
||||
// Initialize values only once when action changes
|
||||
useEffect(() => {
|
||||
if (initialized.current) return
|
||||
initialized.current = true
|
||||
|
||||
// Initialize assistant
|
||||
const currentAssistant = action.assistantId
|
||||
? getAssistantById(action.assistantId) || getDefaultAssistant()
|
||||
: getDefaultAssistant()
|
||||
|
||||
assistantRef.current = {
|
||||
...currentAssistant,
|
||||
model: currentAssistant.model || getDefaultModel()
|
||||
}
|
||||
|
||||
// Initialize topic
|
||||
topicRef.current = getDefaultTopic(currentAssistant.id)
|
||||
|
||||
// Initialize prompt content
|
||||
let userContent = ''
|
||||
switch (action.id) {
|
||||
@@ -128,7 +134,7 @@ const ActionGeneral: FC<Props> = React.memo(({ action, scrollToBottom }) => {
|
||||
fetchResult()
|
||||
}, [fetchResult])
|
||||
|
||||
const allMessages = useTopicMessages(topicRef.current?.id || '')
|
||||
const allMessages = useTopicMessages(topic?.id || '')
|
||||
|
||||
// Memoize the messages to prevent unnecessary re-renders
|
||||
const messageContent = useMemo(() => {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import useTranslate from '@renderer/hooks/useTranslate'
|
||||
import MessageContent from '@renderer/pages/home/Messages/MessageContent'
|
||||
import { getDefaultTopic, getDefaultTranslateAssistant } from '@renderer/services/AssistantService'
|
||||
import { Assistant, Topic, TranslateLanguage, TranslateLanguageCode } from '@renderer/types'
|
||||
import { Topic, TranslateLanguage, TranslateLanguageCode } from '@renderer/types'
|
||||
import type { ActionItem } from '@renderer/types/selectionTypes'
|
||||
import { runAsyncFunction } from '@renderer/utils'
|
||||
import { abortCompletion } from '@renderer/utils/abortController'
|
||||
@@ -31,7 +31,7 @@ const logger = loggerService.withContext('ActionTranslate')
|
||||
|
||||
const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
|
||||
const { t } = useTranslation()
|
||||
const { translateModelPrompt, language } = useSettings()
|
||||
const { language } = useSettings()
|
||||
|
||||
const [targetLanguage, setTargetLanguage] = useState<TranslateLanguage>(LanguagesEnum.enUS)
|
||||
const [alterLanguage, setAlterLanguage] = useState<TranslateLanguage>(LanguagesEnum.zhCN)
|
||||
@@ -41,14 +41,20 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
|
||||
const [isContented, setIsContented] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [contentToCopy, setContentToCopy] = useState('')
|
||||
// use default assistant.
|
||||
// FIXME: this component create a new topic every time, but related data would not be cleared.
|
||||
const [topic] = useState<Topic>(getDefaultTopic('default'))
|
||||
const { getLanguageByLangcode } = useTranslate()
|
||||
|
||||
// Use useRef for values that shouldn't trigger re-renders
|
||||
const initialized = useRef(false)
|
||||
const assistantRef = useRef<Assistant | null>(null)
|
||||
const topicRef = useRef<Topic | null>(null)
|
||||
const topicRef = useRef<Topic | null>(topic)
|
||||
const askId = useRef('')
|
||||
|
||||
// Sync ref
|
||||
useEffect(() => {
|
||||
topicRef.current = topic
|
||||
}, [topic])
|
||||
|
||||
useEffect(() => {
|
||||
runAsyncFunction(async () => {
|
||||
const biDirectionLangPair = await db.settings.get({ id: 'translate:bidirectional:pair' })
|
||||
@@ -79,22 +85,8 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
|
||||
})
|
||||
}, [getLanguageByLangcode, language])
|
||||
|
||||
// Initialize values only once when action changes
|
||||
useEffect(() => {
|
||||
if (initialized.current || !action.selectedText) return
|
||||
initialized.current = true
|
||||
|
||||
// Initialize assistant
|
||||
const currentAssistant = getDefaultTranslateAssistant(targetLanguage, action.selectedText)
|
||||
|
||||
assistantRef.current = currentAssistant
|
||||
|
||||
// Initialize topic
|
||||
topicRef.current = getDefaultTopic(currentAssistant.id)
|
||||
}, [action, targetLanguage, translateModelPrompt])
|
||||
|
||||
const fetchResult = useCallback(async () => {
|
||||
if (!assistantRef.current || !topicRef.current || !action.selectedText) return
|
||||
if (!topicRef.current || !action.selectedText) return
|
||||
|
||||
const setAskId = (id: string) => {
|
||||
askId.current = id
|
||||
@@ -112,8 +104,6 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
|
||||
setError(error.message)
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
let sourceLanguageCode: TranslateLanguageCode
|
||||
|
||||
try {
|
||||
@@ -139,7 +129,6 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
|
||||
}
|
||||
|
||||
const assistant = getDefaultTranslateAssistant(translateLang, action.selectedText)
|
||||
assistantRef.current = assistant
|
||||
processMessages(assistant, topicRef.current, assistant.content, setAskId, onStream, onFinish, onError)
|
||||
}, [action, targetLanguage, alterLanguage, scrollToBottom])
|
||||
|
||||
@@ -147,7 +136,7 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
|
||||
fetchResult()
|
||||
}, [fetchResult])
|
||||
|
||||
const allMessages = useTopicMessages(topicRef.current?.id || '')
|
||||
const allMessages = useTopicMessages(topic?.id || '')
|
||||
|
||||
const messageContent = useMemo(() => {
|
||||
const assistantMessages = allMessages.filter((message) => message.role === 'assistant')
|
||||
|
||||
@@ -30,19 +30,6 @@ const WindowFooter: FC<FooterProps> = ({
|
||||
const hideTimerRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('focus', handleWindowFocus)
|
||||
window.addEventListener('blur', handleWindowBlur)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('focus', handleWindowFocus)
|
||||
window.removeEventListener('blur', handleWindowBlur)
|
||||
if (hideTimerRef.current) {
|
||||
clearTimeout(hideTimerRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
hideTimerRef.current = setTimeout(() => {
|
||||
setIsShowMe(false)
|
||||
@@ -68,21 +55,6 @@ const WindowFooter: FC<FooterProps> = ({
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
useHotkeys('c', () => {
|
||||
showMePeriod()
|
||||
handleCopy()
|
||||
})
|
||||
|
||||
useHotkeys('r', () => {
|
||||
showMePeriod()
|
||||
handleRegenerate()
|
||||
})
|
||||
|
||||
useHotkeys('esc', () => {
|
||||
showMePeriod()
|
||||
handleEsc()
|
||||
})
|
||||
|
||||
const handleEsc = () => {
|
||||
setIsEscHovered(true)
|
||||
setTimeoutTimer(
|
||||
@@ -100,6 +72,11 @@ const WindowFooter: FC<FooterProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
useHotkeys('esc', () => {
|
||||
showMePeriod()
|
||||
handleEsc()
|
||||
})
|
||||
|
||||
const handleRegenerate = () => {
|
||||
setIsRegenerateHovered(true)
|
||||
setTimeoutTimer(
|
||||
@@ -126,6 +103,11 @@ const WindowFooter: FC<FooterProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
useHotkeys('r', () => {
|
||||
showMePeriod()
|
||||
handleRegenerate()
|
||||
})
|
||||
|
||||
const handleCopy = () => {
|
||||
if (!content || loading) return
|
||||
|
||||
@@ -147,6 +129,11 @@ const WindowFooter: FC<FooterProps> = ({
|
||||
})
|
||||
}
|
||||
|
||||
useHotkeys('c', () => {
|
||||
showMePeriod()
|
||||
handleCopy()
|
||||
})
|
||||
|
||||
const handleWindowFocus = () => {
|
||||
setIsWindowFocus(true)
|
||||
}
|
||||
@@ -155,6 +142,19 @@ const WindowFooter: FC<FooterProps> = ({
|
||||
setIsWindowFocus(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('focus', handleWindowFocus)
|
||||
window.addEventListener('blur', handleWindowBlur)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('focus', handleWindowFocus)
|
||||
window.removeEventListener('blur', handleWindowBlur)
|
||||
if (hideTimerRef.current) {
|
||||
clearTimeout(hideTimerRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Container
|
||||
onMouseEnter={() => setIsContainerHovered(true)}
|
||||
|
||||
@@ -204,31 +204,6 @@ const SelectionToolbar: FC<{ demo?: boolean }> = ({ demo = false }) => {
|
||||
}
|
||||
}, [setTimeoutTimer])
|
||||
|
||||
const handleAction = useCallback(
|
||||
(action: ActionItem) => {
|
||||
if (demo) return
|
||||
|
||||
/** avoid mutating the original action, it will cause syncing issue */
|
||||
const newAction = { ...action, selectedText: selectedText.current }
|
||||
|
||||
switch (action.id) {
|
||||
case 'copy':
|
||||
handleCopy()
|
||||
break
|
||||
case 'search':
|
||||
handleSearch(newAction)
|
||||
break
|
||||
case 'quote':
|
||||
handleQuote(newAction)
|
||||
break
|
||||
default:
|
||||
handleDefaultAction(newAction)
|
||||
break
|
||||
}
|
||||
},
|
||||
[demo, handleCopy]
|
||||
)
|
||||
|
||||
const handleSearch = (action: ActionItem) => {
|
||||
if (!action.searchEngine) return
|
||||
|
||||
@@ -256,6 +231,31 @@ const SelectionToolbar: FC<{ demo?: boolean }> = ({ demo = false }) => {
|
||||
window.api?.selection.hideToolbar()
|
||||
}
|
||||
|
||||
const handleAction = useCallback(
|
||||
(action: ActionItem) => {
|
||||
if (demo) return
|
||||
|
||||
/** avoid mutating the original action, it will cause syncing issue */
|
||||
const newAction = { ...action, selectedText: selectedText.current }
|
||||
|
||||
switch (action.id) {
|
||||
case 'copy':
|
||||
handleCopy()
|
||||
break
|
||||
case 'search':
|
||||
handleSearch(newAction)
|
||||
break
|
||||
case 'quote':
|
||||
handleQuote(newAction)
|
||||
break
|
||||
default:
|
||||
handleDefaultAction(newAction)
|
||||
break
|
||||
}
|
||||
},
|
||||
[demo, handleCopy]
|
||||
)
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<LogoWrapper $draggable={!demo}>
|
||||
|
||||
146
yarn.lock
146
yarn.lock
@@ -2098,6 +2098,29 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/core@npm:^7.24.4":
|
||||
version: 7.28.4
|
||||
resolution: "@babel/core@npm:7.28.4"
|
||||
dependencies:
|
||||
"@babel/code-frame": "npm:^7.27.1"
|
||||
"@babel/generator": "npm:^7.28.3"
|
||||
"@babel/helper-compilation-targets": "npm:^7.27.2"
|
||||
"@babel/helper-module-transforms": "npm:^7.28.3"
|
||||
"@babel/helpers": "npm:^7.28.4"
|
||||
"@babel/parser": "npm:^7.28.4"
|
||||
"@babel/template": "npm:^7.27.2"
|
||||
"@babel/traverse": "npm:^7.28.4"
|
||||
"@babel/types": "npm:^7.28.4"
|
||||
"@jridgewell/remapping": "npm:^2.3.5"
|
||||
convert-source-map: "npm:^2.0.0"
|
||||
debug: "npm:^4.1.0"
|
||||
gensync: "npm:^1.0.0-beta.2"
|
||||
json5: "npm:^2.2.3"
|
||||
semver: "npm:^6.3.1"
|
||||
checksum: 10c0/ef5a6c3c6bf40d3589b5593f8118cfe2602ce737412629fb6e26d595be2fcbaae0807b43027a5c42ec4fba5b895ff65891f2503b5918c8a3ea3542ab44d4c278
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/core@npm:^7.27.7":
|
||||
version: 7.28.0
|
||||
resolution: "@babel/core@npm:7.28.0"
|
||||
@@ -2134,6 +2157,19 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/generator@npm:^7.28.3":
|
||||
version: 7.28.3
|
||||
resolution: "@babel/generator@npm:7.28.3"
|
||||
dependencies:
|
||||
"@babel/parser": "npm:^7.28.3"
|
||||
"@babel/types": "npm:^7.28.2"
|
||||
"@jridgewell/gen-mapping": "npm:^0.3.12"
|
||||
"@jridgewell/trace-mapping": "npm:^0.3.28"
|
||||
jsesc: "npm:^3.0.2"
|
||||
checksum: 10c0/0ff58bcf04f8803dcc29479b547b43b9b0b828ec1ee0668e92d79f9e90f388c28589056637c5ff2fd7bcf8d153c990d29c448d449d852bf9d1bc64753ca462bc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-compilation-targets@npm:^7.27.2":
|
||||
version: 7.27.2
|
||||
resolution: "@babel/helper-compilation-targets@npm:7.27.2"
|
||||
@@ -2177,6 +2213,19 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-module-transforms@npm:^7.28.3":
|
||||
version: 7.28.3
|
||||
resolution: "@babel/helper-module-transforms@npm:7.28.3"
|
||||
dependencies:
|
||||
"@babel/helper-module-imports": "npm:^7.27.1"
|
||||
"@babel/helper-validator-identifier": "npm:^7.27.1"
|
||||
"@babel/traverse": "npm:^7.28.3"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0
|
||||
checksum: 10c0/549be62515a6d50cd4cfefcab1b005c47f89bd9135a22d602ee6a5e3a01f27571868ada10b75b033569f24dc4a2bb8d04bfa05ee75c16da7ade2d0db1437fcdb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-plugin-utils@npm:^7.27.1":
|
||||
version: 7.27.1
|
||||
resolution: "@babel/helper-plugin-utils@npm:7.27.1"
|
||||
@@ -2215,6 +2264,27 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helpers@npm:^7.28.4":
|
||||
version: 7.28.4
|
||||
resolution: "@babel/helpers@npm:7.28.4"
|
||||
dependencies:
|
||||
"@babel/template": "npm:^7.27.2"
|
||||
"@babel/types": "npm:^7.28.4"
|
||||
checksum: 10c0/aaa5fb8098926dfed5f223adf2c5e4c7fbba4b911b73dfec2d7d3083f8ba694d201a206db673da2d9b3ae8c01793e795767654558c450c8c14b4c2175b4fcb44
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/parser@npm:^7.24.4, @babel/parser@npm:^7.28.3, @babel/parser@npm:^7.28.4":
|
||||
version: 7.28.4
|
||||
resolution: "@babel/parser@npm:7.28.4"
|
||||
dependencies:
|
||||
"@babel/types": "npm:^7.28.4"
|
||||
bin:
|
||||
parser: ./bin/babel-parser.js
|
||||
checksum: 10c0/58b239a5b1477ac7ed7e29d86d675cc81075ca055424eba6485872626db2dc556ce63c45043e5a679cd925e999471dba8a3ed4864e7ab1dbf64306ab72c52707
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/parser@npm:^7.25.4, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.27.5, @babel/parser@npm:^7.27.7, @babel/parser@npm:^7.28.0":
|
||||
version: 7.28.0
|
||||
resolution: "@babel/parser@npm:7.28.0"
|
||||
@@ -2284,6 +2354,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/traverse@npm:^7.28.3, @babel/traverse@npm:^7.28.4":
|
||||
version: 7.28.4
|
||||
resolution: "@babel/traverse@npm:7.28.4"
|
||||
dependencies:
|
||||
"@babel/code-frame": "npm:^7.27.1"
|
||||
"@babel/generator": "npm:^7.28.3"
|
||||
"@babel/helper-globals": "npm:^7.28.0"
|
||||
"@babel/parser": "npm:^7.28.4"
|
||||
"@babel/template": "npm:^7.27.2"
|
||||
"@babel/types": "npm:^7.28.4"
|
||||
debug: "npm:^4.3.1"
|
||||
checksum: 10c0/ee678fdd49c9f54a32e07e8455242390d43ce44887cea6567b233fe13907b89240c377e7633478a32c6cf1be0e17c2f7f3b0c59f0666e39c5074cc47b968489c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/types@npm:^7.25.4, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.6, @babel/types@npm:^7.28.0":
|
||||
version: 7.28.1
|
||||
resolution: "@babel/types@npm:7.28.1"
|
||||
@@ -2304,6 +2389,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/types@npm:^7.28.4":
|
||||
version: 7.28.4
|
||||
resolution: "@babel/types@npm:7.28.4"
|
||||
dependencies:
|
||||
"@babel/helper-string-parser": "npm:^7.27.1"
|
||||
"@babel/helper-validator-identifier": "npm:^7.27.1"
|
||||
checksum: 10c0/ac6f909d6191319e08c80efbfac7bd9a25f80cc83b43cd6d82e7233f7a6b9d6e7b90236f3af7400a3f83b576895bcab9188a22b584eb0f224e80e6d4e95f4517
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@bcoe/v8-coverage@npm:^1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "@bcoe/v8-coverage@npm:1.0.2"
|
||||
@@ -5998,7 +6093,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@jridgewell/remapping@npm:^2.3.4":
|
||||
"@jridgewell/remapping@npm:^2.3.4, @jridgewell/remapping@npm:^2.3.5":
|
||||
version: 2.3.5
|
||||
resolution: "@jridgewell/remapping@npm:2.3.5"
|
||||
dependencies:
|
||||
@@ -14113,7 +14208,7 @@ __metadata:
|
||||
eslint: "npm:^9.22.0"
|
||||
eslint-plugin-import-zod: "npm:^1.2.0"
|
||||
eslint-plugin-oxlint: "npm:^1.15.0"
|
||||
eslint-plugin-react-hooks: "npm:^5.2.0"
|
||||
eslint-plugin-react-hooks: "npm:^7.0.0"
|
||||
eslint-plugin-simple-import-sort: "npm:^12.1.1"
|
||||
eslint-plugin-unused-imports: "npm:^4.1.4"
|
||||
express: "npm:^5.1.0"
|
||||
@@ -18225,6 +18320,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eslint-plugin-react-hooks@npm:^7.0.0":
|
||||
version: 7.0.0
|
||||
resolution: "eslint-plugin-react-hooks@npm:7.0.0"
|
||||
dependencies:
|
||||
"@babel/core": "npm:^7.24.4"
|
||||
"@babel/parser": "npm:^7.24.4"
|
||||
hermes-parser: "npm:^0.25.1"
|
||||
zod: "npm:^3.22.4 || ^4.0.0"
|
||||
zod-validation-error: "npm:^3.0.3 || ^4.0.0"
|
||||
peerDependencies:
|
||||
eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0
|
||||
checksum: 10c0/911c9efdd9b102ce2eabac247dff8c217ecb8d6972aaf3b7eecfb1cfc293d4d902766355993ff7a37a33c0abde3e76971f43bc1c8ff36d6c123310e5680d0423
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eslint-plugin-react-naming-convention@npm:1.48.1":
|
||||
version: 1.48.1
|
||||
resolution: "eslint-plugin-react-naming-convention@npm:1.48.1"
|
||||
@@ -20005,6 +20115,22 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hermes-estree@npm:0.25.1":
|
||||
version: 0.25.1
|
||||
resolution: "hermes-estree@npm:0.25.1"
|
||||
checksum: 10c0/48be3b2fa37a0cbc77a112a89096fa212f25d06de92781b163d67853d210a8a5c3784fac23d7d48335058f7ed283115c87b4332c2a2abaaccc76d0ead1a282ac
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hermes-parser@npm:^0.25.1":
|
||||
version: 0.25.1
|
||||
resolution: "hermes-parser@npm:0.25.1"
|
||||
dependencies:
|
||||
hermes-estree: "npm:0.25.1"
|
||||
checksum: 10c0/3abaa4c6f1bcc25273f267297a89a4904963ea29af19b8e4f6eabe04f1c2c7e9abd7bfc4730ddb1d58f2ea04b6fee74053d8bddb5656ec6ebf6c79cc8d14202c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hls-video-element@npm:^1.5.6":
|
||||
version: 1.5.7
|
||||
resolution: "hls-video-element@npm:1.5.7"
|
||||
@@ -30251,6 +30377,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"zod-validation-error@npm:^3.0.3 || ^4.0.0":
|
||||
version: 4.0.2
|
||||
resolution: "zod-validation-error@npm:4.0.2"
|
||||
peerDependencies:
|
||||
zod: ^3.25.0 || ^4.0.0
|
||||
checksum: 10c0/0ccfec48c46de1be440b719cd02044d4abb89ed0e14c13e637cd55bf29102f67ccdba373f25def0fc7130e5f15025be4d557a7edcc95d5a3811599aade689e1b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"zod-validation-error@npm:^3.4.0":
|
||||
version: 3.4.0
|
||||
resolution: "zod-validation-error@npm:3.4.0"
|
||||
@@ -30260,6 +30395,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"zod@npm:^3.22.4 || ^4.0.0":
|
||||
version: 4.1.12
|
||||
resolution: "zod@npm:4.1.12"
|
||||
checksum: 10c0/b64c1feb19e99d77075261eaf613e0b2be4dfcd3551eff65ad8b4f2a079b61e379854d066f7d447491fcf193f45babd8095551a9d47973d30b46b6d8e2c46774
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"zod@npm:^3.22.4, zod@npm:^3.23.8, zod@npm:^3.24.1":
|
||||
version: 3.25.56
|
||||
resolution: "zod@npm:3.25.56"
|
||||
|
||||
Reference in New Issue
Block a user