mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 06:30:10 +08:00
refactor: remove heroui
commit7c8bf8b591Author: defi-failure <159208748+defi-failure@users.noreply.github.com> Date: Thu Nov 6 17:59:38 2025 +0800 fix: add token usage to agent session message commitff8e5ddd27Author: defi-failure <159208748+defi-failure@users.noreply.github.com> Date: Thu Nov 6 17:25:54 2025 +0800 fix: close prompt stream when finish or error chunk received commit530e6516fdAuthor: defi-failure <159208748+defi-failure@users.noreply.github.com> Date: Thu Nov 6 17:19:53 2025 +0800 chore: code cleanup commitab21c0d56cAuthor: kangfenmao <kangfenmao@qq.com> Date: Thu Nov 6 16:13:36 2025 +0800 feat(SessionItem): implement auto-rename feature for sessions and improve context menu handling - Added a new context menu option to automatically rename sessions based on topics. - Introduced useDeferredValue for managing target session state. - Updated imports to include necessary thunk actions and components. - Enhanced API service to handle optional assistant model in message summary fetching. - Exported renameAgentSessionIfNeeded function for better accessibility in the store. commit21ea8ccf37Merge:ab7b207d2816a92c60Author: kangfenmao <kangfenmao@qq.com> Date: Thu Nov 6 15:29:09 2025 +0800 Merge branch 'main' of github.com:CherryHQ/cherry-studio into refactor/heroui-antd # Conflicts: # src/renderer/src/pages/home/Tabs/components/AddButton.tsx # src/renderer/src/pages/home/Tabs/components/SessionItem.tsx # src/renderer/src/pages/home/Tabs/components/Sessions.tsx # src/renderer/src/pages/home/Tabs/components/Topics.tsx # src/renderer/src/pages/paintings/NewApiPage.tsx commitab7b207d29Author: kangfenmao <kangfenmao@qq.com> Date: Thu Nov 6 14:50:05 2025 +0800 refactor: streamline event listener management in useAppInit and update ToolPermissionRequestCard styling commit3834c5d402Author: kangfenmao <kangfenmao@qq.com> Date: Thu Nov 6 14:21:25 2025 +0800 refactor: enhance API server state management and remove unused initialization in useAppInit commita64b94a41fAuthor: kangfenmao <kangfenmao@qq.com> Date: Thu Nov 6 13:21:58 2025 +0800 refactor: update OpenAPI documentation paths to include subdirectories for better route coverage commit2e0ff28505Author: kangfenmao <kangfenmao@qq.com> Date: Thu Nov 6 12:26:09 2025 +0800 refactor: center align columns in InstalledPluginsList and set AntTable size to small commit84bf94e2ffAuthor: defi-failure <159208748+defi-failure@users.noreply.github.com> Date: Thu Nov 6 12:06:09 2025 +0800 refactor: align create agent model selection with edit agent commit84f2281506Author: kangfenmao <kangfenmao@qq.com> Date: Thu Nov 6 11:29:32 2025 +0800 refactor: integrate API server functionality into various components and enhance user notifications commit4e01210df4Author: kangfenmao <kangfenmao@qq.com> Date: Thu Nov 6 10:56:38 2025 +0800 refactor: replace ContextMenu with Dropdown in AgentItem and SessionItem components for improved context menu handling commit9df38c7e83Author: kangfenmao <kangfenmao@qq.com> Date: Thu Nov 6 10:27:30 2025 +0800 refactor: update AddButton styling to use CSS variable for border radius and remove unused settings hook commit251c269ab3Author: kangfenmao <kangfenmao@qq.com> Date: Thu Nov 6 10:11:21 2025 +0800 refactor: remove unused error handling alerts from AssistantsTab component commit9b9640d8d1Author: kangfenmao <kangfenmao@qq.com> Date: Thu Nov 6 10:07:26 2025 +0800 refactor: adjust margin styling for UnifiedAddButton component commitedd6b11aa7Author: kangfenmao <kangfenmao@qq.com> Date: Thu Nov 6 10:04:01 2025 +0800 refactor: update AddButton styling based on topic position and clean up CSS for root element commit1c0de625d8Author: kangfenmao <kangfenmao@qq.com> Date: Thu Nov 6 09:56:42 2025 +0800 fix: update assistant addition messages for multiple languages commit0ea4dd4e3aAuthor: dev <verc20.dev@proton.me> Date: Wed Nov 5 21:01:24 2025 +0800 fix: init message api err commitf3bbd4ed44Author: dev <verc20.dev@proton.me> Date: Wed Nov 5 20:42:49 2025 +0800 refactor: remove heroui commitd01609fc36Author: dev <verc20.dev@proton.me> Date: Wed Nov 5 19:08:41 2025 +0800 refactor: migrate heroui/toast to antd message commitf4b14dfc10Author: kangfenmao <kangfenmao@qq.com> Date: Wed Nov 5 18:51:29 2025 +0800 refactor: enhance Sessions component layout with styled Scrollbar and adjust UnifiedAddButton margins commit6ae5f69163Author: kangfenmao <kangfenmao@qq.com> Date: Wed Nov 5 18:44:13 2025 +0800 refactor: update PluginSettings and ToolingSettings for improved layout and functionality commitfcb0020787Author: kangfenmao <kangfenmao@qq.com> Date: Wed Nov 5 18:29:52 2025 +0800 wip commit02265f369eAuthor: dev <verc20.dev@proton.me> Date: Wed Nov 5 17:26:39 2025 +0800 fix: error block related commit5e22d9d36fAuthor: dev <verc20.dev@proton.me> Date: Wed Nov 5 17:14:25 2025 +0800 fix: note head nav related commit3f52b7766aAuthor: dev <verc20.dev@proton.me> Date: Wed Nov 5 16:45:49 2025 +0800 chore: remove dead code commit484622f12bAuthor: dev <verc20.dev@proton.me> Date: Wed Nov 5 16:43:12 2025 +0800 chore: remove dead code commit2bceb302e0Author: dev <verc20.dev@proton.me> Date: Wed Nov 5 15:33:25 2025 +0800 fix: tool setting related commit5c455f25ebAuthor: dev <verc20.dev@proton.me> Date: Wed Nov 5 13:59:33 2025 +0800 chore: remove dead code commitd1d1dbc046Author: dev <verc20.dev@proton.me> Date: Wed Nov 5 13:51:41 2025 +0800 fix: tool permission card related commitbf4ec23ef7Author: dev <verc20.dev@proton.me> Date: Wed Nov 5 12:22:53 2025 +0800 fix: remove button and modal renaming commit47db5baeb1Author: dev <verc20.dev@proton.me> Date: Wed Nov 5 12:20:36 2025 +0800 fix: plugin setting related commit81fecce552Author: kangfenmao <kangfenmao@qq.com> Date: Wed Nov 5 12:16:42 2025 +0800 refactor: enhance ChatNavbarContent structure by replacing Breadcrumbs with custom layout and adding separators commitfc64b6c611Author: kangfenmao <kangfenmao@qq.com> Date: Wed Nov 5 12:10:48 2025 +0800 refactor: simplify MessageAgentTools component structure by removing unnecessary wrapper div commite0f383a050Author: kangfenmao <kangfenmao@qq.com> Date: Wed Nov 5 12:08:32 2025 +0800 fix: update button classes in AddAssistantOrAgentPopup for improved cursor behavior commit720284262fAuthor: kangfenmao <kangfenmao@qq.com> Date: Wed Nov 5 12:06:58 2025 +0800 refactor: update AgentModal to use TopView for improved modal management and enhance form structure commitb334a2c5beAuthor: kangfenmao <kangfenmao@qq.com> Date: Wed Nov 5 11:40:47 2025 +0800 refactor: replace UpdateDialog with UpdateDialogPopup for better modal handling commit468aebd632Author: dev <verc20.dev@proton.me> Date: Wed Nov 5 10:56:40 2025 +0800 fix: plugins related wip commitbd4a979f62Author: dev <verc20.dev@proton.me> Date: Tue Nov 4 17:46:14 2025 +0800 fix: add button related commitb3316a4dc8Author: dev <verc20.dev@proton.me> Date: Tue Nov 4 17:18:31 2025 +0800 fix: agent tool result related components commit6ca7597a98Author: dev <verc20.dev@proton.me> Date: Tue Nov 4 11:12:01 2025 +0800 fix: lint commit7d0f0b38a6Author: kangfenmao <kangfenmao@qq.com> Date: Tue Nov 4 09:56:32 2025 +0800 wip commit96a607a410Author: kangfenmao <kangfenmao@qq.com> Date: Mon Nov 3 20:23:25 2025 +0800 wip commit235ad16252Author: kangfenmao <kangfenmao@qq.com> Date: Mon Nov 3 20:08:45 2025 +0800 wip commitf23fe1b9e9Author: kangfenmao <kangfenmao@qq.com> Date: Mon Nov 3 19:15:01 2025 +0800 wip commit28fac543fcAuthor: kangfenmao <kangfenmao@qq.com> Date: Mon Nov 3 18:39:39 2025 +0800 wip commit3cc7ee01e2Author: kangfenmao <kangfenmao@qq.com> Date: Mon Nov 3 17:33:13 2025 +0800 wip commit37bdf9e508Author: kangfenmao <kangfenmao@qq.com> Date: Sat Nov 1 19:16:58 2025 +0800 wip commit1bf5104f97Author: kangfenmao <kangfenmao@qq.com> Date: Sat Nov 1 12:12:01 2025 +0800 wip
This commit is contained in:
parent
76483d828e
commit
78278ce96d
@ -7,7 +7,6 @@ This file provides guidance to AI coding assistants when working with code in th
|
||||
- **Keep it clear**: Write code that is easy to read, maintain, and explain.
|
||||
- **Match the house style**: Reuse existing patterns, naming, and conventions.
|
||||
- **Search smart**: Prefer `ast-grep` for semantic queries; fall back to `rg`/`grep` when needed.
|
||||
- **Build with HeroUI**: Use HeroUI for every new UI component; never add `antd` or `styled-components`.
|
||||
- **Log centrally**: Route all logging through `loggerService` with the right context—no `console.log`.
|
||||
- **Research via subagent**: Lean on `subagent` for external docs, APIs, news, and references.
|
||||
- **Always propose before executing**: Before making any changes, clearly explain your planned approach and wait for explicit user approval to ensure alignment and prevent unwanted modifications.
|
||||
@ -41,7 +40,6 @@ This file provides guidance to AI coding assistants when working with code in th
|
||||
- **Services** (`src/main/services/`): MCPService, KnowledgeService, WindowService, etc.
|
||||
- **Build System**: Electron-Vite with experimental rolldown-vite, yarn workspaces.
|
||||
- **State Management**: Redux Toolkit (`src/renderer/src/store/`) for predictable state.
|
||||
- **UI Components**: HeroUI (`@heroui/*`) for all new UI elements.
|
||||
|
||||
### Logging
|
||||
```typescript
|
||||
|
||||
@ -147,7 +147,6 @@
|
||||
"@eslint/js": "^9.22.0",
|
||||
"@google/genai": "patch:@google/genai@npm%3A1.0.1#~/.yarn/patches/@google-genai-npm-1.0.1-e26f0f9af7.patch",
|
||||
"@hello-pangea/dnd": "^18.0.1",
|
||||
"@heroui/react": "^2.8.3",
|
||||
"@kangfenmao/keyv-storage": "^0.1.0",
|
||||
"@langchain/community": "^1.0.0",
|
||||
"@langchain/core": "patch:@langchain/core@npm%3A1.0.2#~/.yarn/patches/@langchain-core-npm-1.0.2-183ef83fe4.patch",
|
||||
@ -349,6 +348,7 @@
|
||||
"striptags": "^3.2.0",
|
||||
"styled-components": "^6.1.11",
|
||||
"swr": "^2.3.6",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.13",
|
||||
"tar": "^7.4.3",
|
||||
"tiny-pinyin": "^1.3.2",
|
||||
|
||||
@ -171,7 +171,7 @@ const swaggerOptions: swaggerJSDoc.Options = {
|
||||
}
|
||||
]
|
||||
},
|
||||
apis: ['./src/main/apiServer/routes/*.ts', './src/main/apiServer/app.ts']
|
||||
apis: ['./src/main/apiServer/routes/**/*.ts', './src/main/apiServer/app.ts']
|
||||
}
|
||||
|
||||
export function setupOpenAPIDocumentation(app: Express) {
|
||||
|
||||
@ -365,6 +365,16 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
type: 'chunk',
|
||||
chunk
|
||||
})
|
||||
|
||||
// Close prompt stream when SDK signals completion or error
|
||||
if (chunk.type === 'finish' || chunk.type === 'error') {
|
||||
logger.info('Closing prompt stream as SDK signaled completion', {
|
||||
chunkType: chunk.type,
|
||||
reason: chunk.type === 'finish' ? 'finished' : 'error_occurred'
|
||||
})
|
||||
closePromptStream()
|
||||
logger.info('Prompt stream closed successfully')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -6,11 +6,9 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { Provider } from 'react-redux'
|
||||
import { PersistGate } from 'redux-persist/integration/react'
|
||||
|
||||
import { ToastPortal } from './components/ToastPortal'
|
||||
import TopViewContainer from './components/TopView'
|
||||
import AntdProvider from './context/AntdProvider'
|
||||
import { CodeStyleProvider } from './context/CodeStyleProvider'
|
||||
import { HeroUIProvider } from './context/HeroUIProvider'
|
||||
import { NotificationProvider } from './context/NotificationProvider'
|
||||
import StyleSheetManager from './context/StyleSheetManager'
|
||||
import { ThemeProvider } from './context/ThemeProvider'
|
||||
@ -34,24 +32,21 @@ function App(): React.ReactElement {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<HeroUIProvider>
|
||||
<StyleSheetManager>
|
||||
<ThemeProvider>
|
||||
<AntdProvider>
|
||||
<NotificationProvider>
|
||||
<CodeStyleProvider>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<TopViewContainer>
|
||||
<Router />
|
||||
</TopViewContainer>
|
||||
</PersistGate>
|
||||
</CodeStyleProvider>
|
||||
</NotificationProvider>
|
||||
</AntdProvider>
|
||||
</ThemeProvider>
|
||||
</StyleSheetManager>
|
||||
<ToastPortal />
|
||||
</HeroUIProvider>
|
||||
<StyleSheetManager>
|
||||
<ThemeProvider>
|
||||
<AntdProvider>
|
||||
<NotificationProvider>
|
||||
<CodeStyleProvider>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<TopViewContainer>
|
||||
<Router />
|
||||
</TopViewContainer>
|
||||
</PersistGate>
|
||||
</CodeStyleProvider>
|
||||
</NotificationProvider>
|
||||
</AntdProvider>
|
||||
</ThemeProvider>
|
||||
</StyleSheetManager>
|
||||
</QueryClientProvider>
|
||||
</Provider>
|
||||
)
|
||||
|
||||
@ -41,11 +41,11 @@ body,
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* #root {
|
||||
#root {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
} */
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
|
||||
@ -1,10 +1,6 @@
|
||||
@import 'tailwindcss' source('../../../../renderer');
|
||||
@import 'tw-animate-css';
|
||||
|
||||
/* heroui */
|
||||
@plugin '../../hero.ts';
|
||||
@source '../../../../../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
/* 如需自定义:
|
||||
@ -156,11 +152,6 @@
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
|
||||
/* To disable drag title bar on toast. tailwind css doesn't provide such class name. */
|
||||
.hero-toast {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
|
||||
@ -1,31 +0,0 @@
|
||||
import { Avatar, cn } from '@heroui/react'
|
||||
import { getModelLogoById } from '@renderer/config/models'
|
||||
import type { ApiModel } from '@renderer/types'
|
||||
import React from 'react'
|
||||
|
||||
import Ellipsis from './Ellipsis'
|
||||
|
||||
export interface ModelLabelProps extends Omit<React.ComponentPropsWithRef<'div'>, 'children'> {
|
||||
model?: ApiModel
|
||||
classNames?: {
|
||||
container?: string
|
||||
avatar?: string
|
||||
modelName?: string
|
||||
divider?: string
|
||||
providerName?: string
|
||||
}
|
||||
}
|
||||
|
||||
export const ApiModelLabel: React.FC<ModelLabelProps> = ({ model, className, classNames, ...props }) => {
|
||||
return (
|
||||
<div className={cn('flex items-center gap-1', className, classNames?.container)} {...props}>
|
||||
<Avatar
|
||||
src={model ? (getModelLogoById(model.id) ?? getModelLogoById(model.name)) : undefined}
|
||||
className={cn('h-4 w-4', classNames?.avatar)}
|
||||
/>
|
||||
<Ellipsis className={classNames?.modelName}>{model?.name}</Ellipsis>
|
||||
<span className={classNames?.divider}> | </span>
|
||||
<Ellipsis className={classNames?.providerName}>{model?.provider_name}</Ellipsis>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { Button, Popover, PopoverContent, PopoverTrigger } from '@heroui/react'
|
||||
import { Button, Popover } from 'antd'
|
||||
import React from 'react'
|
||||
|
||||
import EmojiPicker from '../EmojiPicker'
|
||||
@ -10,13 +10,10 @@ type Props = {
|
||||
|
||||
export const EmojiAvatarWithPicker: React.FC<Props> = ({ emoji, onPick }) => {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<Button size="sm" startContent={<span className="text-lg">{emoji}</span>} isIconOnly />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<EmojiPicker onEmojiClick={onPick}></EmojiPicker>
|
||||
</PopoverContent>
|
||||
<Popover content={<EmojiPicker onEmojiClick={onPick} />} trigger="click">
|
||||
<Button type="text" style={{ width: 32, height: 32, fontSize: 18 }}>
|
||||
{emoji}
|
||||
</Button>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { cn } from '@heroui/react'
|
||||
import { cn } from '@renderer/utils'
|
||||
import type { ButtonProps } from 'antd'
|
||||
import { Button } from 'antd'
|
||||
import React, { memo } from 'react'
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Button } from '@heroui/react'
|
||||
import { CheckIcon, XIcon } from 'lucide-react'
|
||||
import { CheckOutlined, CloseOutlined } from '@ant-design/icons'
|
||||
import { Button } from 'antd'
|
||||
import type { FC } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
@ -28,12 +28,22 @@ const ConfirmDialog: FC<Props> = ({ x, y, message, onConfirm, onCancel }) => {
|
||||
<div className="flex min-w-[160px] items-center rounded-lg border border-[var(--color-border)] bg-[var(--color-background)] p-3 shadow-[0_4px_12px_rgba(0,0,0,0.15)]">
|
||||
<div className="mr-2 text-sm leading-[1.4]">{message}</div>
|
||||
<div className="flex justify-center gap-2">
|
||||
<Button onPress={onCancel} radius="full" className="h-6 w-6 min-w-0 p-1" color="danger">
|
||||
<XIcon className="text-danger-foreground" size={16} />
|
||||
</Button>
|
||||
<Button onPress={onConfirm} radius="full" className="h-6 w-6 min-w-0 p-1" color="success">
|
||||
<CheckIcon className="text-success-foreground" size={16} />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onCancel}
|
||||
shape="circle"
|
||||
size="small"
|
||||
danger
|
||||
icon={<CloseOutlined />}
|
||||
style={{ width: 24, height: 24, minWidth: 24 }}
|
||||
/>
|
||||
<Button
|
||||
onClick={onConfirm}
|
||||
shape="circle"
|
||||
size="small"
|
||||
type="primary"
|
||||
icon={<CheckOutlined />}
|
||||
style={{ width: 24, height: 24, minWidth: 24, backgroundColor: '#52c41a' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Button } from '@heroui/button'
|
||||
import { formatErrorMessage } from '@renderer/utils/error'
|
||||
import { Button } from 'antd'
|
||||
import { Alert, Space } from 'antd'
|
||||
import type { ComponentType, ReactNode } from 'react'
|
||||
import type { FallbackProps } from 'react-error-boundary'
|
||||
@ -24,10 +24,10 @@ const DefaultFallback: ComponentType<FallbackProps> = (props: FallbackProps): Re
|
||||
type="error"
|
||||
action={
|
||||
<Space>
|
||||
<Button size="sm" onPress={debug}>
|
||||
<Button size="small" onClick={debug}>
|
||||
{t('error.boundary.default.devtools')}
|
||||
</Button>
|
||||
<Button size="sm" onPress={reload}>
|
||||
<Button size="small" onClick={reload}>
|
||||
{t('error.boundary.default.reload')}
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { cn } from '@heroui/react'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { cn } from '@renderer/utils'
|
||||
import { ChevronRight } from 'lucide-react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { cn } from '@heroui/react'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { cn } from '@renderer/utils'
|
||||
import { Modal } from 'antd'
|
||||
import { Bot, MessageSquare } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
@ -51,7 +51,7 @@ const PopupContainer: React.FC<Props> = ({ onSelect, resolve }) => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSelect('assistant')}
|
||||
className="group flex flex-col items-center gap-3 rounded-lg bg-[var(--color-background-soft)] p-6 transition-all hover:bg-[var(--color-hover)]"
|
||||
className="group flex cursor-pointer flex-col items-center gap-3 rounded-lg bg-[var(--color-background-soft)] p-6 transition-all hover:bg-[var(--color-hover)]"
|
||||
onMouseEnter={() => setHoveredOption('assistant')}
|
||||
onMouseLeave={() => setHoveredOption(null)}>
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-[var(--color-list-item)] transition-colors">
|
||||
@ -73,7 +73,7 @@ const PopupContainer: React.FC<Props> = ({ onSelect, resolve }) => {
|
||||
<button
|
||||
onClick={() => handleSelect('agent')}
|
||||
type="button"
|
||||
className="group flex flex-col items-center gap-3 rounded-lg bg-[var(--color-background-soft)] p-6 transition-all hover:bg-[var(--color-hover)]"
|
||||
className="group flex cursor-pointer flex-col items-center gap-3 rounded-lg bg-[var(--color-background-soft)] p-6 transition-all hover:bg-[var(--color-hover)]"
|
||||
onMouseEnter={() => setHoveredOption('agent')}
|
||||
onMouseLeave={() => setHoveredOption(null)}>
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-[var(--color-list-item)] transition-colors">
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
import { Button } from '@heroui/button'
|
||||
import { Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@heroui/modal'
|
||||
import { Progress } from '@heroui/progress'
|
||||
import { Spinner } from '@heroui/spinner'
|
||||
import { loggerService } from '@logger'
|
||||
import { AppLogo } from '@renderer/config/env'
|
||||
import { SettingHelpText, SettingRow } from '@renderer/pages/settings'
|
||||
import type { WebSocketCandidatesResponse } from '@shared/config/types'
|
||||
import { Alert, Button, Modal, Progress, Spin } from 'antd'
|
||||
import { QRCodeSVG } from 'qrcode.react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -25,7 +22,7 @@ const LoadingQRCode: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px' }}>
|
||||
<Spinner />
|
||||
<Spin />
|
||||
<span style={{ fontSize: '14px', color: 'var(--color-text-2)' }}>
|
||||
{t('settings.data.export_to_phone.lan.generating_qr')}
|
||||
</span>
|
||||
@ -44,8 +41,8 @@ const ScanQRCode: React.FC<{ qrCodeValue: string }> = ({ qrCodeValue }) => {
|
||||
size={200}
|
||||
imageSettings={{
|
||||
src: AppLogo,
|
||||
width: 60,
|
||||
height: 60,
|
||||
width: 40,
|
||||
height: 40,
|
||||
excavate: true
|
||||
}}
|
||||
/>
|
||||
@ -72,7 +69,7 @@ const ConnectingAnimation: React.FC = () => {
|
||||
borderRadius: '12px',
|
||||
backgroundColor: 'var(--color-status-warning)'
|
||||
}}>
|
||||
<Spinner size="lg" color="warning" />
|
||||
<Spin size="large" />
|
||||
<span style={{ fontSize: '14px', color: 'var(--color-text)', marginTop: '12px' }}>
|
||||
{t('settings.data.export_to_phone.lan.status.connecting')}
|
||||
</span>
|
||||
@ -137,7 +134,6 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
const [selectedFolderPath, setSelectedFolderPath] = useState<string | null>(null)
|
||||
const [sendProgress, setSendProgress] = useState(0)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [showCloseConfirm, setShowCloseConfirm] = useState(false)
|
||||
const [autoCloseCountdown, setAutoCloseCountdown] = useState<number | null>(null)
|
||||
|
||||
const { t } = useTranslation()
|
||||
@ -299,22 +295,20 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
// 尝试关闭弹窗 - 如果正在传输则显示确认
|
||||
const handleCancel = useCallback(() => {
|
||||
if (isSending) {
|
||||
setShowCloseConfirm(true)
|
||||
window.modal.confirm({
|
||||
title: t('settings.data.export_to_phone.lan.confirm_close_title'),
|
||||
content: t('settings.data.export_to_phone.lan.confirm_close_message'),
|
||||
centered: true,
|
||||
okButtonProps: {
|
||||
danger: true
|
||||
},
|
||||
okText: t('settings.data.export_to_phone.lan.force_close'),
|
||||
onOk: () => setIsOpen(false)
|
||||
})
|
||||
} else {
|
||||
setIsOpen(false)
|
||||
}
|
||||
}, [isSending])
|
||||
|
||||
// 确认强制关闭
|
||||
const handleForceClose = useCallback(() => {
|
||||
logger.info('Force closing popup during transfer')
|
||||
setIsOpen(false)
|
||||
}, [])
|
||||
|
||||
// 取消关闭确认
|
||||
const handleCancelClose = useCallback(() => {
|
||||
setShowCloseConfirm(false)
|
||||
}, [])
|
||||
}, [isSending, t])
|
||||
|
||||
// 清理并关闭
|
||||
const handleClose = useCallback(async () => {
|
||||
@ -376,11 +370,13 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '8px',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '8px',
|
||||
padding: '5px 12px',
|
||||
width: '100%',
|
||||
backgroundColor: connectionStatusStyles.bg,
|
||||
border: `1px solid ${connectionStatusStyles.border}`
|
||||
border: `1px solid ${connectionStatusStyles.border}`,
|
||||
marginBottom: 10
|
||||
}}>
|
||||
<span style={{ fontSize: '14px', fontWeight: '500', color: 'var(--color-text)' }}>{connectionStatusText}</span>
|
||||
</div>
|
||||
@ -412,7 +408,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
if (!isSending && transferPhase !== 'completed') return null
|
||||
|
||||
return (
|
||||
<div style={{ paddingTop: '8px' }}>
|
||||
<div style={{ paddingTop: '20px' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
@ -441,11 +437,9 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
</div>
|
||||
|
||||
<Progress
|
||||
value={Math.round(sendProgress)}
|
||||
size="md"
|
||||
color={transferPhase === 'completed' ? 'success' : 'primary'}
|
||||
showValueLabel={false}
|
||||
aria-label="Send progress"
|
||||
percent={Math.round(sendProgress)}
|
||||
status={transferPhase === 'completed' ? 'success' : 'active'}
|
||||
showInfo={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -488,95 +482,50 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
handleCancel()
|
||||
}
|
||||
}}
|
||||
isDismissable={false}
|
||||
isKeyboardDismissDisabled={false}
|
||||
placement="center"
|
||||
onClose={handleClose}>
|
||||
<ModalContent>
|
||||
{() => (
|
||||
<>
|
||||
<ModalHeader>{t('settings.data.export_to_phone.lan.title')}</ModalHeader>
|
||||
<ModalBody>
|
||||
<SettingRow>
|
||||
<StatusIndicator />
|
||||
</SettingRow>
|
||||
open={isOpen}
|
||||
onCancel={handleCancel}
|
||||
afterClose={handleClose}
|
||||
title={t('settings.data.export_to_phone.lan.title')}
|
||||
centered
|
||||
closable={!isSending}
|
||||
maskClosable={false}
|
||||
keyboard={true}
|
||||
footer={null}
|
||||
styles={{ body: { paddingBottom: 10 } }}>
|
||||
<SettingRow>
|
||||
<StatusIndicator />
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow>
|
||||
<div>{t('settings.data.export_to_phone.lan.content')}</div>
|
||||
</SettingRow>
|
||||
<Alert message={t('settings.data.export_to_phone.lan.content')} type="info" style={{ borderRadius: 0 }} />
|
||||
|
||||
<SettingRow style={{ display: 'flex', justifyContent: 'center', minHeight: '180px' }}>
|
||||
<QRCodeDisplay />
|
||||
</SettingRow>
|
||||
<SettingRow style={{ display: 'flex', justifyContent: 'center', minHeight: '180px', marginBlock: 25 }}>
|
||||
<QRCodeDisplay />
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<div style={{ display: 'flex', gap: 10, justifyContent: 'center', width: '100%' }}>
|
||||
<Button color="default" variant="flat" onPress={handleSelectZip} isDisabled={isSending}>
|
||||
{t('settings.data.export_to_phone.lan.selectZip')}
|
||||
</Button>
|
||||
<Button color="primary" onPress={handleSendZip} isDisabled={!canSend} isLoading={isSending}>
|
||||
{transferStatusText || t('settings.data.export_to_phone.lan.sendZip')}
|
||||
</Button>
|
||||
</div>
|
||||
</SettingRow>
|
||||
<SettingRow style={{ display: 'flex', alignItems: 'center', marginBlock: 10 }}>
|
||||
<div style={{ display: 'flex', gap: 10, justifyContent: 'center', width: '100%' }}>
|
||||
<Button onClick={handleSelectZip} disabled={isSending}>
|
||||
{t('settings.data.export_to_phone.lan.selectZip')}
|
||||
</Button>
|
||||
<Button type="primary" onClick={handleSendZip} disabled={!canSend} loading={isSending}>
|
||||
{transferStatusText || t('settings.data.export_to_phone.lan.sendZip')}
|
||||
</Button>
|
||||
</div>
|
||||
</SettingRow>
|
||||
|
||||
<SettingHelpText
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
{selectedFolderPath || t('settings.data.export_to_phone.lan.noZipSelected')}
|
||||
</SettingHelpText>
|
||||
<SettingHelpText
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
{selectedFolderPath || t('settings.data.export_to_phone.lan.noZipSelected')}
|
||||
</SettingHelpText>
|
||||
|
||||
<TransferProgress />
|
||||
<AutoCloseCountdown />
|
||||
<ErrorDisplay />
|
||||
</ModalBody>
|
||||
|
||||
{showCloseConfirm && (
|
||||
<ModalFooter>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
gap: '12px',
|
||||
padding: '8px',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: 'var(--color-status-warning)',
|
||||
border: '1px solid var(--color-status-warning)'
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<span style={{ fontSize: '20px' }}>⚠️</span>
|
||||
<span style={{ fontSize: '14px', color: 'var(--color-text)', fontWeight: '500' }}>
|
||||
{t('settings.data.export_to_phone.lan.confirm_close_title')}
|
||||
</span>
|
||||
</div>
|
||||
<span style={{ fontSize: '13px', color: 'var(--color-text-2)', marginLeft: '28px' }}>
|
||||
{t('settings.data.export_to_phone.lan.confirm_close_message')}
|
||||
</span>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '8px', marginTop: '4px' }}>
|
||||
<Button size="sm" color="default" variant="flat" onPress={handleCancelClose}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button size="sm" color="danger" onPress={handleForceClose}>
|
||||
{t('settings.data.export_to_phone.lan.force_close')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
<TransferProgress />
|
||||
<AutoCloseCountdown />
|
||||
<ErrorDisplay />
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
205
src/renderer/src/components/Popups/UpdateDialogPopup.tsx
Normal file
205
src/renderer/src/components/Popups/UpdateDialogPopup.tsx
Normal file
@ -0,0 +1,205 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { handleSaveData } from '@renderer/store'
|
||||
import { Button, Modal } from 'antd'
|
||||
import type { ReleaseNoteInfo, UpdateInfo } from 'builder-util-runtime'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Markdown from 'react-markdown'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const logger = loggerService.withContext('UpdateDialog')
|
||||
|
||||
interface ShowParams {
|
||||
releaseInfo: UpdateInfo | null
|
||||
}
|
||||
|
||||
interface Props extends ShowParams {
|
||||
resolve: (data: any) => void
|
||||
}
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ releaseInfo, resolve }) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(true)
|
||||
const [isInstalling, setIsInstalling] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (releaseInfo) {
|
||||
logger.info('Update dialog opened', { version: releaseInfo.version })
|
||||
}
|
||||
}, [releaseInfo])
|
||||
|
||||
const handleInstall = async () => {
|
||||
setIsInstalling(true)
|
||||
try {
|
||||
await handleSaveData()
|
||||
await window.api.quitAndInstall()
|
||||
setOpen(false)
|
||||
} catch (error) {
|
||||
logger.error('Failed to save data before update', error as Error)
|
||||
setIsInstalling(false)
|
||||
window.toast.error(t('update.saveDataError'))
|
||||
}
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
resolve({})
|
||||
}
|
||||
|
||||
UpdateDialogPopup.hide = onCancel
|
||||
|
||||
const releaseNotes = releaseInfo?.releaseNotes
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<ModalHeaderWrapper>
|
||||
<h3>{t('update.title')}</h3>
|
||||
<p>{t('update.message').replace('{{version}}', releaseInfo?.version || '')}</p>
|
||||
</ModalHeaderWrapper>
|
||||
}
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
afterClose={onClose}
|
||||
transitionName="animation-move-down"
|
||||
centered
|
||||
width={720}
|
||||
footer={[
|
||||
<Button key="later" onClick={onCancel} disabled={isInstalling}>
|
||||
{t('update.later')}
|
||||
</Button>,
|
||||
<Button key="install" type="primary" onClick={handleInstall} loading={isInstalling}>
|
||||
{t('update.install')}
|
||||
</Button>
|
||||
]}>
|
||||
<ModalBodyWrapper>
|
||||
<ReleaseNotesWrapper className="markdown">
|
||||
<Markdown>
|
||||
{typeof releaseNotes === 'string'
|
||||
? releaseNotes
|
||||
: Array.isArray(releaseNotes)
|
||||
? releaseNotes
|
||||
.map((note: ReleaseNoteInfo) => note.note)
|
||||
.filter(Boolean)
|
||||
.join('\n\n')
|
||||
: t('update.noReleaseNotes')}
|
||||
</Markdown>
|
||||
</ReleaseNotesWrapper>
|
||||
</ModalBodyWrapper>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const TopViewKey = 'UpdateDialogPopup'
|
||||
|
||||
export default class UpdateDialogPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide(TopViewKey)
|
||||
}
|
||||
static show(props: ShowParams) {
|
||||
return new Promise<any>((resolve) => {
|
||||
TopView.show(
|
||||
<PopupContainer
|
||||
{...props}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
TopView.hide(TopViewKey)
|
||||
}}
|
||||
/>,
|
||||
TopViewKey
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const ModalHeaderWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
`
|
||||
|
||||
const ModalBodyWrapper = styled.div`
|
||||
max-height: 450px;
|
||||
overflow-y: auto;
|
||||
padding: 12px 0;
|
||||
`
|
||||
|
||||
const ReleaseNotesWrapper = styled.div`
|
||||
background-color: var(--color-bg-2);
|
||||
border-radius: 8px;
|
||||
|
||||
p {
|
||||
margin: 0 0 12px 0;
|
||||
color: var(--color-text-2);
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 16px 0 8px 0;
|
||||
color: var(--color-text-1);
|
||||
font-weight: 600;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin: 8px 0;
|
||||
padding-left: 24px;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 2px 6px;
|
||||
background-color: var(--color-bg-3);
|
||||
border-radius: 4px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 12px;
|
||||
background-color: var(--color-bg-3);
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
|
||||
code {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -1,44 +1,32 @@
|
||||
import type { SelectedItemProps } from '@heroui/react'
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
Select,
|
||||
SelectItem,
|
||||
Textarea,
|
||||
useDisclosure
|
||||
} from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import type { Selection } from '@react-types/shared'
|
||||
import ClaudeIcon from '@renderer/assets/images/models/claude.png'
|
||||
import { ErrorBoundary } from '@renderer/components/ErrorBoundary'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { permissionModeCards } from '@renderer/config/agent'
|
||||
import { agentModelFilter, getModelLogoById } from '@renderer/config/models'
|
||||
import { useAgents } from '@renderer/hooks/agents/useAgents'
|
||||
import { useApiModels } from '@renderer/hooks/agents/useModels'
|
||||
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
|
||||
import SelectAgentBaseModelButton from '@renderer/pages/home/components/SelectAgentBaseModelButton'
|
||||
import type {
|
||||
AddAgentForm,
|
||||
AgentEntity,
|
||||
AgentType,
|
||||
ApiModel,
|
||||
BaseAgentForm,
|
||||
PermissionMode,
|
||||
Tool,
|
||||
UpdateAgentForm
|
||||
} from '@renderer/types'
|
||||
import { AgentConfigurationSchema, isAgentType } from '@renderer/types'
|
||||
import { Avatar, Button, Input, Modal, Select } from 'antd'
|
||||
import { AlertTriangleIcon } from 'lucide-react'
|
||||
import type { ChangeEvent, FormEvent } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { ErrorBoundary } from '../../ErrorBoundary'
|
||||
import type { BaseOption, ModelOption } from './shared'
|
||||
import { Option, renderOption } from './shared'
|
||||
import type { BaseOption } from './shared'
|
||||
|
||||
const { TextArea } = Input
|
||||
|
||||
const logger = loggerService.withContext('AddAgentPopup')
|
||||
|
||||
@ -48,8 +36,6 @@ interface AgentTypeOption extends BaseOption {
|
||||
name: AgentEntity['name']
|
||||
}
|
||||
|
||||
type Option = AgentTypeOption | ModelOption
|
||||
|
||||
type AgentWithTools = AgentEntity & { tools?: Tool[] }
|
||||
|
||||
const buildAgentForm = (existing?: AgentWithTools): BaseAgentForm => ({
|
||||
@ -64,58 +50,37 @@ const buildAgentForm = (existing?: AgentWithTools): BaseAgentForm => ({
|
||||
configuration: AgentConfigurationSchema.parse(existing?.configuration ?? {})
|
||||
})
|
||||
|
||||
type Props = {
|
||||
interface ShowParams {
|
||||
agent?: AgentWithTools
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
afterSubmit?: (a: AgentEntity) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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, isOpen: _isOpen, onClose: _onClose, afterSubmit }) => {
|
||||
const { isOpen, onClose } = useDisclosure({ isOpen: _isOpen, onClose: _onClose })
|
||||
interface Props extends ShowParams {
|
||||
resolve: (data: any) => void
|
||||
}
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ agent, afterSubmit, resolve }) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(true)
|
||||
const loadingRef = useRef(false)
|
||||
// const { setTimeoutTimer } = useTimer()
|
||||
const { addAgent } = useAgents()
|
||||
const { updateAgent } = useUpdateAgent()
|
||||
// hard-coded. We only support anthropic for now.
|
||||
const { models } = useApiModels({ providerType: 'anthropic' })
|
||||
const isEditing = (agent?: AgentWithTools) => agent !== undefined
|
||||
|
||||
const [form, setForm] = useState<BaseAgentForm>(() => buildAgentForm(agent))
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
if (open) {
|
||||
setForm(buildAgentForm(agent))
|
||||
}
|
||||
}, [agent, isOpen])
|
||||
}, [agent, open])
|
||||
|
||||
const selectedPermissionMode = form.configuration?.permission_mode ?? 'default'
|
||||
|
||||
const onPermissionModeChange = useCallback((keys: Selection) => {
|
||||
if (keys === 'all') {
|
||||
return
|
||||
}
|
||||
|
||||
const [first] = Array.from(keys)
|
||||
if (!first) {
|
||||
return
|
||||
}
|
||||
|
||||
const onPermissionModeChange = useCallback((value: PermissionMode) => {
|
||||
setForm((prev) => {
|
||||
const parsedConfiguration = AgentConfigurationSchema.parse(prev.configuration ?? {})
|
||||
const nextMode = first as PermissionMode
|
||||
|
||||
if (parsedConfiguration.permission_mode === nextMode) {
|
||||
if (parsedConfiguration.permission_mode === value) {
|
||||
if (!prev.configuration) {
|
||||
return {
|
||||
...prev,
|
||||
@ -129,7 +94,7 @@ export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _
|
||||
...prev,
|
||||
configuration: {
|
||||
...parsedConfiguration,
|
||||
permission_mode: nextMode
|
||||
permission_mode: value
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -150,55 +115,57 @@ export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _
|
||||
[]
|
||||
)
|
||||
|
||||
const agentOptions: AgentTypeOption[] = useMemo(
|
||||
const agentOptions = useMemo(
|
||||
() =>
|
||||
agentConfig.map(
|
||||
(option) =>
|
||||
({
|
||||
...option,
|
||||
rendered: <Option option={option} />
|
||||
}) as const satisfies SelectedItemProps
|
||||
),
|
||||
agentConfig.map((option) => ({
|
||||
value: option.key,
|
||||
label: (
|
||||
<OptionWrapper>
|
||||
<Avatar src={option.avatar} size={24} />
|
||||
<span>{option.label}</span>
|
||||
</OptionWrapper>
|
||||
)
|
||||
})),
|
||||
[agentConfig]
|
||||
)
|
||||
|
||||
const onAgentTypeChange = useCallback(
|
||||
(e: ChangeEvent<HTMLSelectElement>) => {
|
||||
(value: AgentType) => {
|
||||
const prevConfig = agentConfig.find((config) => config.key === form.type)
|
||||
let newName: string | undefined = form.name
|
||||
if (prevConfig && prevConfig.name === form.name) {
|
||||
const newConfig = agentConfig.find((config) => config.key === e.target.value)
|
||||
const newConfig = agentConfig.find((config) => config.key === value)
|
||||
if (newConfig) {
|
||||
newName = newConfig.name
|
||||
}
|
||||
}
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
type: e.target.value as AgentType,
|
||||
type: value,
|
||||
name: newName
|
||||
}))
|
||||
},
|
||||
[agentConfig, form.name, form.type]
|
||||
)
|
||||
|
||||
const onNameChange = useCallback((name: string) => {
|
||||
const onNameChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
name
|
||||
name: e.target.value
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const onDescChange = useCallback((description: string) => {
|
||||
const onDescChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
description
|
||||
description: e.target.value
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const onInstChange = useCallback((instructions: string) => {
|
||||
const onInstChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
instructions
|
||||
instructions: e.target.value
|
||||
}))
|
||||
}, [])
|
||||
|
||||
@ -231,34 +198,36 @@ export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const modelOptions = useMemo(() => {
|
||||
// mocked data. not final version
|
||||
return (models ?? [])
|
||||
.filter((m) =>
|
||||
agentModelFilter({
|
||||
id: m.id,
|
||||
provider: m.provider || '',
|
||||
name: m.name,
|
||||
group: ''
|
||||
})
|
||||
)
|
||||
.map((model) => ({
|
||||
type: 'model',
|
||||
key: model.id,
|
||||
label: model.name,
|
||||
avatar: getModelLogoById(model.id),
|
||||
providerId: model.provider,
|
||||
providerName: model.provider_name
|
||||
})) satisfies ModelOption[]
|
||||
}, [models])
|
||||
// Create a temporary agentBase object for SelectAgentBaseModelButton
|
||||
const tempAgentBase: AgentEntity = useMemo(
|
||||
() => ({
|
||||
id: agent?.id ?? 'temp-creating',
|
||||
type: form.type,
|
||||
name: form.name,
|
||||
model: form.model,
|
||||
accessible_paths: form.accessible_paths.length > 0 ? form.accessible_paths : ['/'],
|
||||
allowed_tools: form.allowed_tools ?? [],
|
||||
description: form.description,
|
||||
instructions: form.instructions,
|
||||
configuration: form.configuration,
|
||||
created_at: agent?.created_at ?? new Date().toISOString(),
|
||||
updated_at: agent?.updated_at ?? new Date().toISOString()
|
||||
}),
|
||||
[form, agent?.id, agent?.created_at, agent?.updated_at]
|
||||
)
|
||||
|
||||
const onModelChange = useCallback((e: ChangeEvent<HTMLSelectElement>) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
model: e.target.value
|
||||
}))
|
||||
const handleModelSelect = useCallback(async (model: ApiModel) => {
|
||||
setForm((prev) => ({ ...prev, model: model.id }))
|
||||
}, [])
|
||||
|
||||
const onCancel = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
resolve({})
|
||||
}
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
@ -330,9 +299,7 @@ export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _
|
||||
afterSubmit?.(result.data)
|
||||
}
|
||||
loadingRef.current = false
|
||||
|
||||
// setTimeoutTimer('onCreateAgent', () => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0)
|
||||
onClose()
|
||||
setOpen(false)
|
||||
},
|
||||
[
|
||||
form.type,
|
||||
@ -344,7 +311,6 @@ export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _
|
||||
form.allowed_tools,
|
||||
form.configuration,
|
||||
agent,
|
||||
onClose,
|
||||
t,
|
||||
updateAgent,
|
||||
afterSubmit,
|
||||
@ -352,138 +318,312 @@ export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _
|
||||
]
|
||||
)
|
||||
|
||||
AgentModalPopup.hide = onCancel
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
classNames={{
|
||||
base: 'max-h-[90vh]',
|
||||
wrapper: 'overflow-hidden'
|
||||
}}>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader>{isEditing(agent) ? t('agent.edit.title') : t('agent.add.title')}</ModalHeader>
|
||||
<Form onSubmit={onSubmit} className="min-h-0 w-full shrink overflow-auto">
|
||||
<ModalBody className="min-h-0 w-full flex-1 shrink overflow-auto">
|
||||
<div className="flex gap-2">
|
||||
<Select
|
||||
isRequired
|
||||
isDisabled={isEditing(agent)}
|
||||
selectionMode="single"
|
||||
selectedKeys={[form.type]}
|
||||
disallowEmptySelection
|
||||
onChange={onAgentTypeChange}
|
||||
items={agentOptions}
|
||||
label={t('agent.type.label')}
|
||||
placeholder={t('agent.add.type.placeholder')}
|
||||
renderValue={renderOption}>
|
||||
{(option) => (
|
||||
<SelectItem key={option.key} textValue={option.label}>
|
||||
<Option option={option} />
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
<Input isRequired value={form.name} onValueChange={onNameChange} label={t('common.name')} />
|
||||
</div>
|
||||
<Select
|
||||
isRequired
|
||||
selectionMode="single"
|
||||
selectedKeys={form.model ? [form.model] : []}
|
||||
disallowEmptySelection
|
||||
onChange={onModelChange}
|
||||
items={modelOptions}
|
||||
label={t('common.model')}
|
||||
placeholder={t('common.placeholders.select.model')}
|
||||
renderValue={renderOption}>
|
||||
{(option) => (
|
||||
<SelectItem key={option.key} textValue={option.label}>
|
||||
<Option option={option} />
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
<Select
|
||||
isRequired
|
||||
selectionMode="single"
|
||||
selectedKeys={[selectedPermissionMode]}
|
||||
onSelectionChange={onPermissionModeChange}
|
||||
label={t('agent.settings.tooling.permissionMode.title', 'Permission mode')}
|
||||
placeholder={t('agent.settings.tooling.permissionMode.placeholder', 'Select permission mode')}
|
||||
description={t(
|
||||
'agent.settings.tooling.permissionMode.helper',
|
||||
'Choose how the agent handles tool approvals.'
|
||||
)}
|
||||
items={permissionModeCards}>
|
||||
{(item) => (
|
||||
<SelectItem key={item.mode} textValue={t(item.titleKey, item.titleFallback)}>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-medium text-sm">{t(item.titleKey, item.titleFallback)}</span>
|
||||
<span className="text-foreground-500 text-xs">
|
||||
{t(item.descriptionKey, item.descriptionFallback)}
|
||||
</span>
|
||||
<span className="text-foreground-400 text-xs">
|
||||
{t(item.behaviorKey, item.behaviorFallback)}
|
||||
</span>
|
||||
{item.caution ? (
|
||||
<span className="flex items-center gap-1 text-danger-500 text-xs">
|
||||
<AlertTriangleIcon size={12} className="text-danger" />
|
||||
{t(
|
||||
'agent.settings.tooling.permissionMode.bypassPermissions.warning',
|
||||
'Use with caution — all tools will run without asking for approval.'
|
||||
)}
|
||||
</span>
|
||||
) : null}
|
||||
title={isEditing(agent) ? t('agent.edit.title') : t('agent.add.title')}
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
afterClose={onClose}
|
||||
transitionName="animation-move-down"
|
||||
centered
|
||||
width={500}
|
||||
footer={null}>
|
||||
<StyledForm onSubmit={onSubmit}>
|
||||
<FormContent>
|
||||
<FormRow>
|
||||
<FormItem style={{ flex: 1 }}>
|
||||
<Label>{t('agent.type.label')}</Label>
|
||||
<Select
|
||||
value={form.type}
|
||||
onChange={onAgentTypeChange}
|
||||
options={agentOptions}
|
||||
disabled={isEditing(agent)}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem style={{ flex: 1 }}>
|
||||
<Label>
|
||||
{t('common.name')} <RequiredMark>*</RequiredMark>
|
||||
</Label>
|
||||
<Input value={form.name} onChange={onNameChange} required />
|
||||
</FormItem>
|
||||
</FormRow>
|
||||
|
||||
<FormItem>
|
||||
<Label>
|
||||
{t('common.model')} <RequiredMark>*</RequiredMark>
|
||||
</Label>
|
||||
<SelectAgentBaseModelButton
|
||||
agentBase={tempAgentBase}
|
||||
onSelect={handleModelSelect}
|
||||
fontSize={14}
|
||||
avatarSize={24}
|
||||
iconSize={16}
|
||||
buttonStyle={{
|
||||
padding: '8px 12px',
|
||||
width: '100%',
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: 6,
|
||||
height: 'auto'
|
||||
}}
|
||||
containerClassName="flex items-center justify-between w-full"
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem>
|
||||
<Label>
|
||||
{t('agent.settings.tooling.permissionMode.title', 'Permission mode')} <RequiredMark>*</RequiredMark>
|
||||
</Label>
|
||||
<Select
|
||||
value={selectedPermissionMode}
|
||||
onChange={onPermissionModeChange}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('agent.settings.tooling.permissionMode.placeholder', 'Select permission mode')}
|
||||
dropdownStyle={{ minWidth: '500px' }}
|
||||
optionLabelProp="label">
|
||||
{permissionModeCards.map((item) => (
|
||||
<Select.Option key={item.mode} value={item.mode} label={t(item.titleKey, item.titleFallback)}>
|
||||
<PermissionOptionWrapper>
|
||||
<div className="title">{t(item.titleKey, item.titleFallback)}</div>
|
||||
<div className="description">{t(item.descriptionKey, item.descriptionFallback)}</div>
|
||||
<div className="behavior">{t(item.behaviorKey, item.behaviorFallback)}</div>
|
||||
{item.caution && (
|
||||
<div className="caution">
|
||||
<AlertTriangleIcon size={12} />
|
||||
{t(
|
||||
'agent.settings.tooling.permissionMode.bypassPermissions.warning',
|
||||
'Use with caution — all tools will run without asking for approval.'
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium text-foreground text-sm">
|
||||
{t('agent.session.accessible_paths.label')}
|
||||
</span>
|
||||
<Button size="sm" variant="flat" onPress={addAccessiblePath}>
|
||||
{t('agent.session.accessible_paths.add')}
|
||||
)}
|
||||
</PermissionOptionWrapper>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<HelpText>
|
||||
{t('agent.settings.tooling.permissionMode.helper', 'Choose how the agent handles tool approvals.')}
|
||||
</HelpText>
|
||||
</FormItem>
|
||||
|
||||
<FormItem>
|
||||
<LabelWithButton>
|
||||
<Label>
|
||||
{t('agent.session.accessible_paths.label')} <RequiredMark>*</RequiredMark>
|
||||
</Label>
|
||||
<Button size="small" onClick={addAccessiblePath}>
|
||||
{t('agent.session.accessible_paths.add')}
|
||||
</Button>
|
||||
</LabelWithButton>
|
||||
{form.accessible_paths.length > 0 ? (
|
||||
<PathList>
|
||||
{form.accessible_paths.map((path) => (
|
||||
<PathItem key={path}>
|
||||
<PathText title={path}>{path}</PathText>
|
||||
<Button size="small" danger onClick={() => removeAccessiblePath(path)}>
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
</div>
|
||||
{form.accessible_paths.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{form.accessible_paths.map((path) => (
|
||||
<div
|
||||
key={path}
|
||||
className="flex items-center justify-between gap-2 rounded-medium border border-default-200 px-3 py-2">
|
||||
<span className="truncate text-sm" title={path}>
|
||||
{path}
|
||||
</span>
|
||||
<Button size="sm" variant="light" color="danger" onPress={() => removeAccessiblePath(path)}>
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-foreground-400 text-sm">{t('agent.session.accessible_paths.empty')}</p>
|
||||
)}
|
||||
</div>
|
||||
<Textarea label={t('common.prompt')} value={form.instructions ?? ''} onValueChange={onInstChange} />
|
||||
<Textarea
|
||||
label={t('common.description')}
|
||||
value={form.description ?? ''}
|
||||
onValueChange={onDescChange}
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter className="w-full">
|
||||
<Button onPress={onClose}>{t('common.close')}</Button>
|
||||
<Button color="primary" type="submit" isLoading={loadingRef.current}>
|
||||
{isEditing(agent) ? t('common.confirm') : t('common.add')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Form>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</PathItem>
|
||||
))}
|
||||
</PathList>
|
||||
) : (
|
||||
<EmptyText>{t('agent.session.accessible_paths.empty')}</EmptyText>
|
||||
)}
|
||||
</FormItem>
|
||||
|
||||
<FormItem>
|
||||
<Label>{t('common.prompt')}</Label>
|
||||
<TextArea rows={3} value={form.instructions ?? ''} onChange={onInstChange} />
|
||||
</FormItem>
|
||||
|
||||
<FormItem>
|
||||
<Label>{t('common.description')}</Label>
|
||||
<TextArea rows={2} value={form.description ?? ''} onChange={onDescChange} />
|
||||
</FormItem>
|
||||
</FormContent>
|
||||
|
||||
<FormFooter>
|
||||
<Button onClick={onCancel}>{t('common.close')}</Button>
|
||||
<Button type="primary" htmlType="submit" loading={loadingRef.current}>
|
||||
{isEditing(agent) ? t('common.confirm') : t('common.add')}
|
||||
</Button>
|
||||
</FormFooter>
|
||||
</StyledForm>
|
||||
</Modal>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
const TopViewKey = 'AgentModalPopup'
|
||||
|
||||
export default class AgentModalPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide(TopViewKey)
|
||||
}
|
||||
static show(props: ShowParams) {
|
||||
return new Promise<any>((resolve) => {
|
||||
TopView.show(
|
||||
<PopupContainer
|
||||
{...props}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
TopView.hide(TopViewKey)
|
||||
}}
|
||||
/>,
|
||||
TopViewKey
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the old export for backward compatibility during migration
|
||||
export const AgentModal = AgentModalPopup
|
||||
|
||||
const StyledForm = styled.form`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
`
|
||||
|
||||
const FormContent = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
padding-right: 8px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--color-border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
`
|
||||
|
||||
const FormRow = styled.div`
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
`
|
||||
|
||||
const FormItem = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
const Label = styled.label`
|
||||
font-size: 14px;
|
||||
color: var(--color-text-1);
|
||||
font-weight: 500;
|
||||
`
|
||||
|
||||
const RequiredMark = styled.span`
|
||||
color: #ff4d4f;
|
||||
margin-left: 4px;
|
||||
`
|
||||
|
||||
const HelpText = styled.div`
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
`
|
||||
|
||||
const LabelWithButton = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const PathList = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
const PathItem = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
background-color: var(--color-bg-1);
|
||||
`
|
||||
|
||||
const PathText = styled.span`
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-2);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`
|
||||
|
||||
const EmptyText = styled.p`
|
||||
font-size: 13px;
|
||||
color: var(--color-text-3);
|
||||
margin: 0;
|
||||
`
|
||||
|
||||
const FormFooter = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
`
|
||||
|
||||
const OptionWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
const PermissionOptionWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 8px 0;
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-2);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.behavior {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.caution {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #ff4d4f;
|
||||
margin-top: 4px;
|
||||
padding: 6px 8px;
|
||||
background-color: rgba(255, 77, 79, 0.1);
|
||||
border-radius: 4px;
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -1,320 +0,0 @@
|
||||
import {
|
||||
Button,
|
||||
cn,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
Textarea,
|
||||
useDisclosure
|
||||
} from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import type { Selection } from '@react-types/shared'
|
||||
import { AllowedToolsSelect } from '@renderer/components/agent'
|
||||
import { useAgent } from '@renderer/hooks/agents/useAgent'
|
||||
import { useSessions } from '@renderer/hooks/agents/useSessions'
|
||||
import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
|
||||
import type {
|
||||
AgentEntity,
|
||||
AgentSessionEntity,
|
||||
BaseSessionForm,
|
||||
CreateSessionForm,
|
||||
Tool,
|
||||
UpdateSessionForm
|
||||
} from '@renderer/types'
|
||||
import type { FormEvent, ReactNode } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { ErrorBoundary } from '../../ErrorBoundary'
|
||||
|
||||
const logger = loggerService.withContext('SessionAgentPopup')
|
||||
|
||||
type AgentWithTools = AgentEntity & { tools?: Tool[] }
|
||||
type SessionWithTools = AgentSessionEntity & { tools?: Tool[] }
|
||||
|
||||
const buildSessionForm = (existing?: SessionWithTools, agent?: AgentWithTools): BaseSessionForm => ({
|
||||
name: existing?.name ?? agent?.name ?? 'Claude Code',
|
||||
description: existing?.description ?? agent?.description,
|
||||
instructions: existing?.instructions ?? agent?.instructions,
|
||||
model: existing?.model ?? agent?.model ?? '',
|
||||
accessible_paths: existing?.accessible_paths
|
||||
? [...existing.accessible_paths]
|
||||
: agent?.accessible_paths
|
||||
? [...agent.accessible_paths]
|
||||
: [],
|
||||
allowed_tools: existing?.allowed_tools
|
||||
? [...existing.allowed_tools]
|
||||
: agent?.allowed_tools
|
||||
? [...agent.allowed_tools]
|
||||
: [],
|
||||
mcps: existing?.mcps ? [...existing.mcps] : agent?.mcps ? [...agent.mcps] : []
|
||||
})
|
||||
|
||||
interface BaseProps {
|
||||
agentId: string
|
||||
session?: SessionWithTools
|
||||
onSessionCreated?: (session: AgentSessionEntity) => void
|
||||
}
|
||||
|
||||
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 a Session.
|
||||
* @deprecated may as a reference when migrating to v2
|
||||
*
|
||||
* Either trigger or isOpen and onClose is given.
|
||||
* @param agentId - The ID of agent which the session is related.
|
||||
* @param session - Optional session 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 SessionModal: React.FC<Props> = ({
|
||||
agentId,
|
||||
session,
|
||||
trigger,
|
||||
isOpen: _isOpen,
|
||||
onClose: _onClose,
|
||||
onSessionCreated
|
||||
}) => {
|
||||
const { isOpen, onClose, onOpen } = useDisclosure({ isOpen: _isOpen, onClose: _onClose })
|
||||
const { t } = useTranslation()
|
||||
const loadingRef = useRef(false)
|
||||
// const { setTimeoutTimer } = useTimer()
|
||||
const { createSession } = useSessions(agentId)
|
||||
const { updateSession } = useUpdateSession(agentId)
|
||||
const { agent } = useAgent(agentId)
|
||||
const isEditing = (session?: AgentSessionEntity) => session !== undefined
|
||||
|
||||
const [form, setForm] = useState<BaseSessionForm>(() => buildSessionForm(session, agent ?? undefined))
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setForm(buildSessionForm(session, agent ?? undefined))
|
||||
}
|
||||
}, [session, agent, isOpen])
|
||||
|
||||
const availableTools = useMemo(() => session?.tools ?? agent?.tools ?? [], [agent?.tools, session?.tools])
|
||||
const selectedToolKeys = useMemo(() => new Set(form.allowed_tools ?? []), [form.allowed_tools])
|
||||
|
||||
useEffect(() => {
|
||||
if (!availableTools.length) {
|
||||
return
|
||||
}
|
||||
|
||||
setForm((prev) => {
|
||||
const allowed = prev.allowed_tools ?? []
|
||||
const validTools = allowed.filter((id) => availableTools.some((tool) => tool.id === id))
|
||||
if (validTools.length === allowed.length) {
|
||||
return prev
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
allowed_tools: validTools
|
||||
}
|
||||
})
|
||||
}, [availableTools])
|
||||
|
||||
const onNameChange = useCallback((name: string) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
name
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const onDescChange = useCallback((description: string) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
description
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const onInstChange = useCallback((instructions: string) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
instructions
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const onAllowedToolsChange = useCallback(
|
||||
(keys: Selection) => {
|
||||
setForm((prev) => {
|
||||
const existing = prev.allowed_tools ?? []
|
||||
if (keys === 'all') {
|
||||
return {
|
||||
...prev,
|
||||
allowed_tools: availableTools.map((tool) => tool.id)
|
||||
}
|
||||
}
|
||||
|
||||
const next = Array.from(keys).map(String)
|
||||
const filtered = availableTools.length
|
||||
? next.filter((id) => availableTools.some((tool) => tool.id === id))
|
||||
: next
|
||||
|
||||
if (existing.length === filtered.length && existing.every((id) => filtered.includes(id))) {
|
||||
return prev
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
allowed_tools: filtered
|
||||
}
|
||||
})
|
||||
},
|
||||
[availableTools]
|
||||
)
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
if (loadingRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
loadingRef.current = true
|
||||
|
||||
// Additional validation check besides native HTML validation to ensure security
|
||||
if (!form.model) {
|
||||
window.toast.error(t('error.model.not_exists'))
|
||||
loadingRef.current = false
|
||||
return
|
||||
}
|
||||
|
||||
if (form.accessible_paths.length === 0) {
|
||||
window.toast.error(t('agent.session.accessible_paths.error.at_least_one'))
|
||||
loadingRef.current = false
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (isEditing(session)) {
|
||||
if (!session) {
|
||||
throw new Error('Agent is required for editing mode')
|
||||
}
|
||||
|
||||
const updatePayload = {
|
||||
id: session.id,
|
||||
name: form.name,
|
||||
description: form.description,
|
||||
instructions: form.instructions,
|
||||
model: form.model,
|
||||
accessible_paths: [...form.accessible_paths],
|
||||
allowed_tools: [...(form.allowed_tools ?? [])],
|
||||
mcps: [...(form.mcps ?? [])]
|
||||
} satisfies UpdateSessionForm
|
||||
|
||||
updateSession(updatePayload)
|
||||
logger.debug('Updated agent', updatePayload)
|
||||
} else {
|
||||
const newSession = {
|
||||
name: form.name,
|
||||
description: form.description,
|
||||
instructions: form.instructions,
|
||||
model: form.model,
|
||||
accessible_paths: [...form.accessible_paths],
|
||||
allowed_tools: [...(form.allowed_tools ?? [])],
|
||||
mcps: [...(form.mcps ?? [])]
|
||||
} satisfies CreateSessionForm
|
||||
const createdSession = await createSession(newSession)
|
||||
if (createdSession) {
|
||||
onSessionCreated?.(createdSession)
|
||||
}
|
||||
logger.debug('Added agent', newSession)
|
||||
}
|
||||
|
||||
// setTimeoutTimer('onCreateAgent', () => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0)
|
||||
onClose()
|
||||
} finally {
|
||||
loadingRef.current = false
|
||||
}
|
||||
},
|
||||
[
|
||||
form.model,
|
||||
form.name,
|
||||
form.description,
|
||||
form.instructions,
|
||||
form.accessible_paths,
|
||||
form.allowed_tools,
|
||||
form.mcps,
|
||||
session,
|
||||
onClose,
|
||||
onSessionCreated,
|
||||
t,
|
||||
updateSession,
|
||||
createSession
|
||||
]
|
||||
)
|
||||
|
||||
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}>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader>
|
||||
{isEditing(session) ? t('agent.session.edit.title') : t('agent.session.add.title')}
|
||||
</ModalHeader>
|
||||
<Form onSubmit={onSubmit} className="w-full">
|
||||
<ModalBody className="w-full">
|
||||
<Input isRequired value={form.name} onValueChange={onNameChange} label={t('common.name')} />
|
||||
<Textarea
|
||||
label={t('common.description')}
|
||||
value={form.description ?? ''}
|
||||
onValueChange={onDescChange}
|
||||
/>
|
||||
<AllowedToolsSelect
|
||||
items={availableTools}
|
||||
selectedKeys={selectedToolKeys}
|
||||
onSelectionChange={onAllowedToolsChange}
|
||||
/>
|
||||
<Textarea label={t('common.prompt')} value={form.instructions ?? ''} onValueChange={onInstChange} />
|
||||
</ModalBody>
|
||||
<ModalFooter className="w-full">
|
||||
<Button onPress={onClose}>{t('common.close')}</Button>
|
||||
<Button color="primary" type="submit" isLoading={loadingRef.current}>
|
||||
{isEditing(session) ? t('common.confirm') : t('common.add')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Form>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
@ -1,8 +1,3 @@
|
||||
import type { SelectedItemProps, SelectedItems } from '@heroui/react'
|
||||
import { Avatar } from '@heroui/react'
|
||||
import { getProviderLabel } from '@renderer/i18n/label'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export interface BaseOption {
|
||||
type: 'type' | 'model'
|
||||
key: string
|
||||
@ -10,43 +5,3 @@ export interface BaseOption {
|
||||
// img src
|
||||
avatar?: string
|
||||
}
|
||||
|
||||
export interface ModelOption extends BaseOption {
|
||||
providerId?: string
|
||||
providerName?: string
|
||||
}
|
||||
|
||||
export function isModelOption(option: BaseOption): option is ModelOption {
|
||||
return option.type === 'model'
|
||||
}
|
||||
|
||||
export const Item = ({ item }: { item: SelectedItemProps<BaseOption> }) => <Option option={item.data} />
|
||||
|
||||
export const renderOption = (items: SelectedItems<BaseOption>) =>
|
||||
items.map((item) => <Item key={item.key} item={item} />)
|
||||
|
||||
export const Option = ({ option }: { option?: BaseOption | null }) => {
|
||||
const { t } = useTranslation()
|
||||
if (!option) {
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<Avatar name="?" className="h-5 w-5" />
|
||||
{t('common.invalid_value')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const providerLabel = (() => {
|
||||
if (!isModelOption(option)) return null
|
||||
if (option.providerName) return option.providerName
|
||||
if (option.providerId) return getProviderLabel(option.providerId)
|
||||
return null
|
||||
})()
|
||||
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<Avatar src={option.avatar} className="h-5 w-5" />
|
||||
<span className="truncate">{option.label}</span>
|
||||
{providerLabel ? <span className="truncate text-foreground-500">| {providerLabel}</span> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
import { ToastProvider } from '@heroui/toast'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
export const ToastPortal = () => {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
return () => setMounted(false)
|
||||
}, [])
|
||||
|
||||
if (!mounted) return null
|
||||
|
||||
return createPortal(
|
||||
<ToastProvider
|
||||
placement="top-center"
|
||||
regionProps={{
|
||||
className: 'z-[1001]'
|
||||
}}
|
||||
toastOffset={20}
|
||||
toastProps={{
|
||||
timeout: 3000,
|
||||
classNames: {
|
||||
// This setting causes the 'hero-toast' class to be applied twice to the toast element. This is weird and I don't know why, but it works.
|
||||
base: 'hero-toast'
|
||||
}
|
||||
}}
|
||||
/>,
|
||||
document.body
|
||||
)
|
||||
}
|
||||
@ -2,12 +2,12 @@
|
||||
import TopViewMinappContainer from '@renderer/components/MinApp/TopViewMinappContainer'
|
||||
import { useAppInit } from '@renderer/hooks/useAppInit'
|
||||
import { useShortcuts } from '@renderer/hooks/useShortcuts'
|
||||
import { Modal } from 'antd'
|
||||
import { message, Modal } from 'antd'
|
||||
import type { PropsWithChildren } from 'react'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { Box } from '../Layout'
|
||||
import { getToastUtilities } from './toast'
|
||||
import { getToastUtilities, initMessageApi } from './toast'
|
||||
|
||||
let onPop = () => {}
|
||||
let onShow = ({ element, id }: { element: React.FC | React.ReactNode; id: string }) => {
|
||||
@ -36,6 +36,7 @@ const TopViewContainer: React.FC<Props> = ({ children }) => {
|
||||
elementsRef.current = elements
|
||||
|
||||
const [modal, modalContextHolder] = Modal.useModal()
|
||||
const [messageApi, messageContextHolder] = message.useMessage()
|
||||
const { shortcuts } = useShortcuts()
|
||||
const enableQuitFullScreen = shortcuts.find((item) => item.key === 'exit_fullscreen')?.enabled
|
||||
|
||||
@ -43,8 +44,9 @@ const TopViewContainer: React.FC<Props> = ({ children }) => {
|
||||
|
||||
useEffect(() => {
|
||||
window.modal = modal
|
||||
initMessageApi(messageApi)
|
||||
window.toast = getToastUtilities()
|
||||
}, [modal])
|
||||
}, [messageApi, modal])
|
||||
|
||||
onPop = () => {
|
||||
const views = [...elementsRef.current]
|
||||
@ -97,6 +99,7 @@ const TopViewContainer: React.FC<Props> = ({ children }) => {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
{messageContextHolder}
|
||||
{modalContextHolder}
|
||||
<TopViewMinappContainer />
|
||||
{elements.map(({ element: Element, id }) => (
|
||||
|
||||
@ -1,72 +0,0 @@
|
||||
import { addToast, closeAll, closeToast, getToastQueue, isToastClosing } from '@heroui/toast'
|
||||
import type { RequireSome } from '@renderer/types'
|
||||
|
||||
type AddToastProps = Parameters<typeof addToast>[0]
|
||||
type ToastPropsColored = Omit<AddToastProps, 'color'>
|
||||
|
||||
const createToast = (color: 'danger' | 'success' | 'warning' | 'default') => {
|
||||
return (arg: ToastPropsColored | string): string | null => {
|
||||
if (typeof arg === 'string') {
|
||||
return addToast({ color, title: arg })
|
||||
} else {
|
||||
return addToast({ color, ...arg })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// syntatic sugar, oh yeah
|
||||
|
||||
/**
|
||||
* Display an error toast notification with red color
|
||||
* @param arg - Toast content (string) or toast options object
|
||||
* @returns Toast ID or null
|
||||
*/
|
||||
export const error = createToast('danger')
|
||||
|
||||
/**
|
||||
* Display a success toast notification with green color
|
||||
* @param arg - Toast content (string) or toast options object
|
||||
* @returns Toast ID or null
|
||||
*/
|
||||
export const success = createToast('success')
|
||||
|
||||
/**
|
||||
* Display a warning toast notification with yellow color
|
||||
* @param arg - Toast content (string) or toast options object
|
||||
* @returns Toast ID or null
|
||||
*/
|
||||
export const warning = createToast('warning')
|
||||
|
||||
/**
|
||||
* Display an info toast notification with default color
|
||||
* @param arg - Toast content (string) or toast options object
|
||||
* @returns Toast ID or null
|
||||
*/
|
||||
export const info = createToast('default')
|
||||
|
||||
/**
|
||||
* Display a loading toast notification that resolves with a promise
|
||||
* @param args - Toast options object containing a promise to resolve
|
||||
* @returns Toast ID or null
|
||||
*/
|
||||
export const loading = (args: RequireSome<AddToastProps, 'promise'>) => {
|
||||
// Disappear immediately by default
|
||||
if (args.timeout === undefined) {
|
||||
args.timeout = 1
|
||||
}
|
||||
return addToast(args)
|
||||
}
|
||||
|
||||
export const getToastUtilities = () =>
|
||||
({
|
||||
getToastQueue,
|
||||
addToast,
|
||||
closeToast,
|
||||
closeAll,
|
||||
isToastClosing,
|
||||
error,
|
||||
success,
|
||||
warning,
|
||||
info,
|
||||
loading
|
||||
}) as const
|
||||
231
src/renderer/src/components/TopView/toast.tsx
Normal file
231
src/renderer/src/components/TopView/toast.tsx
Normal file
@ -0,0 +1,231 @@
|
||||
import type { RequireSome } from '@renderer/types'
|
||||
import { message as antdMessage } from 'antd'
|
||||
import type { MessageInstance } from 'antd/es/message/interface'
|
||||
import type React from 'react'
|
||||
|
||||
// Global message instance for static usage
|
||||
let messageApi: MessageInstance | null = null
|
||||
|
||||
// Initialize message API - should be called once the App component is mounted
|
||||
export const initMessageApi = (api: MessageInstance) => {
|
||||
messageApi = api
|
||||
}
|
||||
|
||||
// Get message API instance
|
||||
const getMessageApi = (): MessageInstance => {
|
||||
if (!messageApi) {
|
||||
// Fallback to static method if hook API is not available
|
||||
return antdMessage
|
||||
}
|
||||
return messageApi
|
||||
}
|
||||
|
||||
type ToastColor = 'danger' | 'success' | 'warning' | 'default'
|
||||
type MessageType = 'error' | 'success' | 'warning' | 'info'
|
||||
|
||||
interface ToastConfig {
|
||||
title?: React.ReactNode
|
||||
icon?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
timeout?: number
|
||||
key?: string | number
|
||||
className?: string
|
||||
style?: React.CSSProperties
|
||||
onClick?: () => void
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
interface LoadingToastConfig extends ToastConfig {
|
||||
promise: Promise<any>
|
||||
}
|
||||
|
||||
const colorToType = (color: ToastColor): MessageType => {
|
||||
switch (color) {
|
||||
case 'danger':
|
||||
return 'error'
|
||||
case 'success':
|
||||
return 'success'
|
||||
case 'warning':
|
||||
return 'warning'
|
||||
case 'default':
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// Toast content component
|
||||
const ToastContent: React.FC<{ title?: React.ReactNode; description?: React.ReactNode; icon?: React.ReactNode }> = ({
|
||||
title,
|
||||
description,
|
||||
icon
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
{(icon || title) && (
|
||||
<div className="flex items-center gap-2 font-semibold">
|
||||
{icon}
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
{description && <div className="text-sm">{description}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const createToast = (color: ToastColor) => {
|
||||
return (arg: ToastConfig | string): string | null => {
|
||||
const api = getMessageApi()
|
||||
const type = colorToType(color) as 'error' | 'success' | 'warning' | 'info'
|
||||
|
||||
if (typeof arg === 'string') {
|
||||
// antd message methods return a function to close the message
|
||||
api[type](arg)
|
||||
return null
|
||||
}
|
||||
|
||||
const { title, description, icon, timeout, ...restConfig } = arg
|
||||
|
||||
// Convert timeout from milliseconds to seconds (antd uses seconds)
|
||||
const duration = timeout !== undefined ? timeout / 1000 : 3
|
||||
|
||||
return (
|
||||
(api.open({
|
||||
type: type as 'error' | 'success' | 'warning' | 'info',
|
||||
content: <ToastContent title={title} description={description} icon={icon} />,
|
||||
duration,
|
||||
...restConfig
|
||||
}) as any) || null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display an error toast notification with red color
|
||||
* @param arg - Toast content (string) or toast options object
|
||||
* @returns Toast ID or null
|
||||
*/
|
||||
export const error = createToast('danger')
|
||||
|
||||
/**
|
||||
* Display a success toast notification with green color
|
||||
* @param arg - Toast content (string) or toast options object
|
||||
* @returns Toast ID or null
|
||||
*/
|
||||
export const success = createToast('success')
|
||||
|
||||
/**
|
||||
* Display a warning toast notification with yellow color
|
||||
* @param arg - Toast content (string) or toast options object
|
||||
* @returns Toast ID or null
|
||||
*/
|
||||
export const warning = createToast('warning')
|
||||
|
||||
/**
|
||||
* Display an info toast notification with default color
|
||||
* @param arg - Toast content (string) or toast options object
|
||||
* @returns Toast ID or null
|
||||
*/
|
||||
export const info = createToast('default')
|
||||
|
||||
/**
|
||||
* Display a loading toast notification that resolves with a promise
|
||||
* @param args - Toast options object containing a promise to resolve
|
||||
*/
|
||||
export const loading = (args: RequireSome<LoadingToastConfig, 'promise'>): string | null => {
|
||||
const api = getMessageApi()
|
||||
const { title, description, icon, promise, timeout, ...restConfig } = args
|
||||
|
||||
// Generate unique key for this loading message
|
||||
const key = args.key || `loading-${Date.now()}-${Math.random()}`
|
||||
|
||||
// Show loading message
|
||||
api.loading({
|
||||
content: <ToastContent title={title || 'Loading...'} description={description} icon={icon} />,
|
||||
duration: 0, // Don't auto-close
|
||||
key,
|
||||
...restConfig
|
||||
})
|
||||
|
||||
// Handle promise resolution
|
||||
promise
|
||||
.then((result) => {
|
||||
api.success({
|
||||
content: <ToastContent title={title || 'Success'} description={description} />,
|
||||
duration: timeout !== undefined ? timeout / 1000 : 2,
|
||||
key,
|
||||
...restConfig
|
||||
})
|
||||
return result
|
||||
})
|
||||
.catch((err) => {
|
||||
api.error({
|
||||
content: (
|
||||
<ToastContent title={title || 'Error'} description={err?.message || description || 'An error occurred'} />
|
||||
),
|
||||
duration: timeout !== undefined ? timeout / 1000 : 3,
|
||||
key,
|
||||
...restConfig
|
||||
})
|
||||
throw err
|
||||
})
|
||||
|
||||
return key as string
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a toast notification
|
||||
* @param config - Toast configuration object
|
||||
* @returns Toast ID or null
|
||||
*/
|
||||
export const addToast = (config: ToastConfig) => info(config)
|
||||
|
||||
/**
|
||||
* Close a specific toast notification by its key
|
||||
* @param key - Toast key (string)
|
||||
*/
|
||||
export const closeToast = (key: string) => {
|
||||
getMessageApi().destroy(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Close all toast notifications
|
||||
*/
|
||||
export const closeAll = () => {
|
||||
getMessageApi().destroy()
|
||||
}
|
||||
|
||||
/**
|
||||
* Stub functions for compatibility with previous toast API
|
||||
* These are no-ops since antd message doesn't expose a queue
|
||||
*/
|
||||
|
||||
/**
|
||||
* @deprecated This function is a no-op stub for backward compatibility only.
|
||||
* Antd message doesn't expose a queue. Do not rely on this function.
|
||||
* @returns Empty toast queue stub
|
||||
*/
|
||||
export const getToastQueue = (): any => ({ toasts: [] })
|
||||
|
||||
/**
|
||||
* @deprecated This function is a no-op stub for backward compatibility only.
|
||||
* Antd message doesn't track closing state. Do not rely on this function.
|
||||
* @param key - Toast key (unused)
|
||||
* @returns Always returns false
|
||||
*/
|
||||
export const isToastClosing = (key?: string): boolean => {
|
||||
key // unused
|
||||
return false
|
||||
}
|
||||
|
||||
export const getToastUtilities = () =>
|
||||
({
|
||||
getToastQueue,
|
||||
addToast,
|
||||
closeToast,
|
||||
closeAll,
|
||||
isToastClosing,
|
||||
error,
|
||||
success,
|
||||
warning,
|
||||
info,
|
||||
loading
|
||||
}) as const
|
||||
@ -1,101 +0,0 @@
|
||||
import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, ScrollShadow } from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import { handleSaveData } from '@renderer/store'
|
||||
import type { ReleaseNoteInfo, UpdateInfo } from 'builder-util-runtime'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Markdown from 'react-markdown'
|
||||
|
||||
const logger = loggerService.withContext('UpdateDialog')
|
||||
|
||||
interface UpdateDialogProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
releaseInfo: UpdateInfo | null
|
||||
}
|
||||
|
||||
const UpdateDialog: React.FC<UpdateDialogProps> = ({ isOpen, onClose, releaseInfo }) => {
|
||||
const { t } = useTranslation()
|
||||
const [isInstalling, setIsInstalling] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && releaseInfo) {
|
||||
logger.info('Update dialog opened', { version: releaseInfo.version })
|
||||
}
|
||||
}, [isOpen, releaseInfo])
|
||||
|
||||
const handleInstall = async () => {
|
||||
setIsInstalling(true)
|
||||
try {
|
||||
await handleSaveData()
|
||||
await window.api.quitAndInstall()
|
||||
} catch (error) {
|
||||
logger.error('Failed to save data before update', error as Error)
|
||||
setIsInstalling(false)
|
||||
window.toast.error(t('update.saveDataError'))
|
||||
}
|
||||
}
|
||||
|
||||
const releaseNotes = releaseInfo?.releaseNotes
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
size="2xl"
|
||||
scrollBehavior="inside"
|
||||
classNames={{
|
||||
base: 'max-h-[85vh]',
|
||||
header: 'border-b border-divider',
|
||||
footer: 'border-t border-divider'
|
||||
}}>
|
||||
<ModalContent>
|
||||
{(onModalClose) => (
|
||||
<>
|
||||
<ModalHeader className="flex flex-col gap-1">
|
||||
<h3 className="font-semibold text-lg">{t('update.title')}</h3>
|
||||
<p className="text-default-500 text-small">
|
||||
{t('update.message').replace('{{version}}', releaseInfo?.version || '')}
|
||||
</p>
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<ScrollShadow className="max-h-[450px]" hideScrollBar>
|
||||
<div className="markdown rounded-lg bg-default-50 p-4">
|
||||
<Markdown>
|
||||
{typeof releaseNotes === 'string'
|
||||
? releaseNotes
|
||||
: Array.isArray(releaseNotes)
|
||||
? releaseNotes
|
||||
.map((note: ReleaseNoteInfo) => note.note)
|
||||
.filter(Boolean)
|
||||
.join('\n\n')
|
||||
: t('update.noReleaseNotes')}
|
||||
</Markdown>
|
||||
</div>
|
||||
</ScrollShadow>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button variant="light" onPress={onModalClose} isDisabled={isInstalling}>
|
||||
{t('update.later')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color="primary"
|
||||
onPress={async () => {
|
||||
await handleInstall()
|
||||
onModalClose()
|
||||
}}
|
||||
isLoading={isInstalling}>
|
||||
{t('update.install')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default UpdateDialog
|
||||
@ -1,55 +0,0 @@
|
||||
import type { SelectedItems, SelectProps } from '@heroui/react'
|
||||
import { Chip, cn, Select, SelectItem } from '@heroui/react'
|
||||
import type { Tool } from '@renderer/types'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export interface AllowedToolsSelectProps extends Omit<SelectProps, 'children'> {
|
||||
items: Tool[]
|
||||
}
|
||||
|
||||
export const AllowedToolsSelect: React.FC<AllowedToolsSelectProps> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { items: availableTools, className, ...rest } = props
|
||||
|
||||
const renderSelectedTools = useCallback((items: SelectedItems<Tool>) => {
|
||||
if (!items.length) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{items.map((item) => (
|
||||
<Chip key={item.key} size="sm" variant="flat" className="max-w-[160px] truncate">
|
||||
{item.data?.name ?? item.textValue ?? item.key}
|
||||
</Chip>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Select
|
||||
aria-label={t('agent.session.allowed_tools.label')}
|
||||
selectionMode="multiple"
|
||||
isMultiline
|
||||
label={t('agent.session.allowed_tools.label')}
|
||||
placeholder={t('agent.session.allowed_tools.placeholder')}
|
||||
description={
|
||||
availableTools.length ? t('agent.session.allowed_tools.helper') : t('agent.session.allowed_tools.empty')
|
||||
}
|
||||
isDisabled={!availableTools.length}
|
||||
items={availableTools}
|
||||
renderValue={renderSelectedTools}
|
||||
className={cn('max-w-xl', className)}
|
||||
{...rest}>
|
||||
{(tool) => (
|
||||
<SelectItem key={tool.id} textValue={tool.name}>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium text-sm">{tool.name}</span>
|
||||
{tool.description ? <span className="text-foreground-500 text-xs">{tool.description}</span> : null}
|
||||
</div>
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
export { AllowedToolsSelect } from './AllowedToolsSelect'
|
||||
@ -1,13 +0,0 @@
|
||||
import { HeroUIProvider } from '@heroui/react'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
|
||||
const AppHeroUIProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const { language } = useSettings()
|
||||
return (
|
||||
<HeroUIProvider className="flex h-full w-full flex-1" locale={language}>
|
||||
{children}
|
||||
</HeroUIProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export { AppHeroUIProvider as HeroUIProvider }
|
||||
14
src/renderer/src/env.d.ts
vendored
14
src/renderer/src/env.d.ts
vendored
@ -1,12 +1,22 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
import type { PermissionUpdate } from '@anthropic-ai/claude-agent-sdk'
|
||||
import type { addToast, closeAll, closeToast, getToastQueue, isToastClosing } from '@heroui/toast'
|
||||
import type KeyvStorage from '@kangfenmao/keyv-storage'
|
||||
import type { HookAPI } from 'antd/es/modal/useModal'
|
||||
import type { NavigateFunction } from 'react-router-dom'
|
||||
|
||||
import type { error, info, loading, success, warning } from './components/TopView/toast'
|
||||
import type {
|
||||
addToast,
|
||||
closeAll,
|
||||
closeToast,
|
||||
error,
|
||||
getToastQueue,
|
||||
info,
|
||||
isToastClosing,
|
||||
loading,
|
||||
success,
|
||||
warning
|
||||
} from './components/TopView/toast'
|
||||
|
||||
interface ImportMetaEnv {
|
||||
VITE_RENDERER_INTEGRATED_MODEL: string
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
import { heroui } from '@heroui/react'
|
||||
export default heroui()
|
||||
@ -41,6 +41,7 @@ export const useAgents = () => {
|
||||
// NOTE: We only use the array for now. useUpdateAgent depends on this behavior.
|
||||
return result.data
|
||||
}, [apiServerConfig.enabled, apiServerRunning, client, t])
|
||||
|
||||
const { data, error, isLoading, mutate } = useSWR(swrKey, fetcher)
|
||||
const { chat } = useRuntime()
|
||||
const { activeAgentId } = chat
|
||||
|
||||
@ -31,21 +31,24 @@ export const useApiServer = () => {
|
||||
try {
|
||||
const status = await window.api.apiServer.getStatus()
|
||||
setApiServerRunning(status.running)
|
||||
if (status.running && !apiServerConfig.enabled) {
|
||||
setApiServerEnabled(true)
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error('Failed to check API server status:', error)
|
||||
} finally {
|
||||
setApiServerLoading(false)
|
||||
}
|
||||
}, [])
|
||||
}, [apiServerConfig.enabled, setApiServerEnabled])
|
||||
|
||||
const startApiServer = useCallback(async () => {
|
||||
if (apiServerLoading) return
|
||||
|
||||
setApiServerLoading(true)
|
||||
try {
|
||||
const result = await window.api.apiServer.start()
|
||||
if (result.success) {
|
||||
setApiServerRunning(true)
|
||||
setApiServerEnabled(true)
|
||||
window.toast.success(t('apiServer.messages.startSuccess'))
|
||||
} else {
|
||||
window.toast.error(t('apiServer.messages.startError') + result.error)
|
||||
@ -55,16 +58,16 @@ export const useApiServer = () => {
|
||||
} finally {
|
||||
setApiServerLoading(false)
|
||||
}
|
||||
}, [apiServerLoading, t])
|
||||
}, [apiServerLoading, setApiServerEnabled, t])
|
||||
|
||||
const stopApiServer = useCallback(async () => {
|
||||
if (apiServerLoading) return
|
||||
|
||||
setApiServerLoading(true)
|
||||
try {
|
||||
const result = await window.api.apiServer.stop()
|
||||
if (result.success) {
|
||||
setApiServerRunning(false)
|
||||
setApiServerEnabled(false)
|
||||
window.toast.success(t('apiServer.messages.stopSuccess'))
|
||||
} else {
|
||||
window.toast.error(t('apiServer.messages.stopError') + result.error)
|
||||
@ -74,14 +77,14 @@ export const useApiServer = () => {
|
||||
} finally {
|
||||
setApiServerLoading(false)
|
||||
}
|
||||
}, [apiServerLoading, t])
|
||||
}, [apiServerLoading, setApiServerEnabled, t])
|
||||
|
||||
const restartApiServer = useCallback(async () => {
|
||||
if (apiServerLoading) return
|
||||
|
||||
setApiServerLoading(true)
|
||||
try {
|
||||
const result = await window.api.apiServer.restart()
|
||||
setApiServerEnabled(result.success)
|
||||
if (result.success) {
|
||||
await checkApiServerStatus()
|
||||
window.toast.success(t('apiServer.messages.restartSuccess'))
|
||||
@ -93,7 +96,7 @@ export const useApiServer = () => {
|
||||
} finally {
|
||||
setApiServerLoading(false)
|
||||
}
|
||||
}, [apiServerLoading, checkApiServerStatus, t])
|
||||
}, [apiServerLoading, checkApiServerStatus, setApiServerEnabled, t])
|
||||
|
||||
useEffect(() => {
|
||||
checkApiServerStatus()
|
||||
|
||||
@ -221,13 +221,12 @@ export function useAppInit() {
|
||||
}
|
||||
}
|
||||
|
||||
window.electron.ipcRenderer.on(IpcChannel.AgentToolPermission_Request, requestListener)
|
||||
window.electron.ipcRenderer.on(IpcChannel.AgentToolPermission_Result, resultListener)
|
||||
const removeListeners = [
|
||||
window.electron.ipcRenderer.on(IpcChannel.AgentToolPermission_Request, requestListener),
|
||||
window.electron.ipcRenderer.on(IpcChannel.AgentToolPermission_Result, resultListener)
|
||||
]
|
||||
|
||||
return () => {
|
||||
window.electron?.ipcRenderer.removeListener(IpcChannel.AgentToolPermission_Request, requestListener)
|
||||
window.electron?.ipcRenderer.removeListener(IpcChannel.AgentToolPermission_Result, resultListener)
|
||||
}
|
||||
return () => removeListeners.forEach((removeListener) => removeListener())
|
||||
}, [dispatch, t])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -12,19 +12,7 @@ export default function useUserTheme() {
|
||||
const colorPrimary = Color(theme.colorPrimary)
|
||||
|
||||
document.body.style.setProperty('--color-primary', colorPrimary.toString())
|
||||
// overwrite hero UI primary color.
|
||||
document.body.style.setProperty('--primary', colorPrimary.toString())
|
||||
document.body.style.setProperty('--heroui-primary', colorPrimary.toString())
|
||||
document.body.style.setProperty('--heroui-primary-900', colorPrimary.lighten(0.5).toString())
|
||||
document.body.style.setProperty('--heroui-primary-800', colorPrimary.lighten(0.4).toString())
|
||||
document.body.style.setProperty('--heroui-primary-700', colorPrimary.lighten(0.3).toString())
|
||||
document.body.style.setProperty('--heroui-primary-600', colorPrimary.lighten(0.2).toString())
|
||||
document.body.style.setProperty('--heroui-primary-500', colorPrimary.lighten(0.1).toString())
|
||||
document.body.style.setProperty('--heroui-primary-400', colorPrimary.toString())
|
||||
document.body.style.setProperty('--heroui-primary-300', colorPrimary.darken(0.1).toString())
|
||||
document.body.style.setProperty('--heroui-primary-200', colorPrimary.darken(0.2).toString())
|
||||
document.body.style.setProperty('--heroui-primary-100', colorPrimary.darken(0.3).toString())
|
||||
document.body.style.setProperty('--heroui-primary-50', colorPrimary.darken(0.4).toString())
|
||||
document.body.style.setProperty('--color-primary-soft', colorPrimary.alpha(0.6).toString())
|
||||
document.body.style.setProperty('--color-primary-mute', colorPrimary.alpha(0.3).toString())
|
||||
|
||||
|
||||
@ -1646,7 +1646,7 @@
|
||||
},
|
||||
"assistant": {
|
||||
"added": {
|
||||
"content": "智能体添加成功"
|
||||
"content": "助手添加成功"
|
||||
}
|
||||
},
|
||||
"attachments": {
|
||||
|
||||
@ -1646,7 +1646,7 @@
|
||||
},
|
||||
"assistant": {
|
||||
"added": {
|
||||
"content": "智慧代理人新增成功"
|
||||
"content": "助手新增成功"
|
||||
}
|
||||
},
|
||||
"attachments": {
|
||||
|
||||
@ -1611,7 +1611,7 @@
|
||||
},
|
||||
"assistant": {
|
||||
"added": {
|
||||
"content": "Agent erfolgreich hinzugefügt"
|
||||
"content": "Assistent erfolgreich hinzugefügt"
|
||||
}
|
||||
},
|
||||
"attachments": {
|
||||
|
||||
@ -1611,7 +1611,7 @@
|
||||
},
|
||||
"assistant": {
|
||||
"added": {
|
||||
"content": "Ο ενεργοποιημένος αστρόναυτης προστέθηκε επιτυχώς"
|
||||
"content": "Ο βοηθός προστέθηκε επιτυχώς"
|
||||
}
|
||||
},
|
||||
"attachments": {
|
||||
|
||||
@ -1611,7 +1611,7 @@
|
||||
},
|
||||
"assistant": {
|
||||
"added": {
|
||||
"content": "アシスタントが追加されました"
|
||||
"content": "助手が追加されました"
|
||||
}
|
||||
},
|
||||
"attachments": {
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { Alert } from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import type { ContentSearchRef } from '@renderer/components/ContentSearch'
|
||||
import { ContentSearch } from '@renderer/components/ContentSearch'
|
||||
@ -17,7 +16,7 @@ import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import type { Assistant, Topic } from '@renderer/types'
|
||||
import { classNames } from '@renderer/utils'
|
||||
import { Flex } from 'antd'
|
||||
import { Alert, Flex } from 'antd'
|
||||
import { debounce } from 'lodash'
|
||||
import { AnimatePresence, motion } from 'motion/react'
|
||||
import type { FC } from 'react'
|
||||
@ -170,11 +169,7 @@ const Chat: FC<Props> = (props) => {
|
||||
return () => <div> Active Session ID is invalid.</div>
|
||||
}
|
||||
if (!apiServer.enabled) {
|
||||
return () => (
|
||||
<div>
|
||||
<Alert color="warning" title={t('agent.warning.enable_server')} />
|
||||
</div>
|
||||
)
|
||||
return () => <Alert type="warning" message={t('agent.warning.enable_server')} style={{ margin: '5px 16px' }} />
|
||||
}
|
||||
return () => <AgentSessionMessages agentId={activeAgentId} sessionId={activeSessionId} />
|
||||
}, [activeAgentId, activeSessionId, apiServer.enabled, t])
|
||||
@ -191,22 +186,14 @@ const Chat: FC<Props> = (props) => {
|
||||
|
||||
// TODO: more info
|
||||
const AgentInvalid = useCallback(() => {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div>
|
||||
<Alert color="warning" title="Select an agent" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
return <Alert type="warning" message="Select an agent" style={{ margin: '5px 16px' }} />
|
||||
}, [])
|
||||
|
||||
// TODO: more info
|
||||
const SessionInvalid = useCallback(() => {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div>
|
||||
<Alert color="warning" title="Create a session" />
|
||||
</div>
|
||||
<Alert type="warning" message="Create a session" style={{ margin: '5px 16px' }} />
|
||||
</div>
|
||||
)
|
||||
}, [])
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { Tooltip } from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import { ActionIconButton } from '@renderer/components/Buttons'
|
||||
import { QuickPanelView } from '@renderer/components/QuickPanel'
|
||||
@ -11,6 +10,7 @@ import { useShortcutDisplay } from '@renderer/hooks/useShortcuts'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import PasteService from '@renderer/services/PasteService'
|
||||
import { pauseTrace } from '@renderer/services/SpanManagerService'
|
||||
import { estimateUserPromptUsage } from '@renderer/services/TokenService'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { newMessagesActions, selectMessagesForTopic } from '@renderer/store/newMessage'
|
||||
import { sendMessage as dispatchSendMessage } from '@renderer/store/thunk/messageThunk'
|
||||
@ -22,6 +22,7 @@ import { abortCompletion } from '@renderer/utils/abortController'
|
||||
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
|
||||
import { getSendMessageShortcutLabel, isSendMessageKeyPressed } from '@renderer/utils/input'
|
||||
import { createMainTextBlock, createMessage } from '@renderer/utils/messageUtils/create'
|
||||
import { Tooltip } from 'antd'
|
||||
import type { TextAreaRef } from 'antd/es/input/TextArea'
|
||||
import TextArea from 'antd/es/input/TextArea'
|
||||
import { isEmpty } from 'lodash'
|
||||
@ -199,11 +200,15 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
|
||||
}
|
||||
: undefined
|
||||
|
||||
// Calculate token usage for the user message
|
||||
const usage = await estimateUserPromptUsage({ content: text })
|
||||
|
||||
const userMessage: Message = createMessage('user', sessionTopicId, agentId, {
|
||||
id: userMessageId,
|
||||
blocks: userMessageBlocks.map((block) => block?.id),
|
||||
model,
|
||||
modelId: model?.id
|
||||
modelId: model?.id,
|
||||
usage
|
||||
})
|
||||
|
||||
const assistantStub: Assistant = {
|
||||
@ -309,7 +314,7 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
|
||||
/>
|
||||
<Toolbar>
|
||||
<ToolbarGroup>
|
||||
<Tooltip placement="top" content={t('chat.input.new_topic', { Command: newTopicShortcut })} delay={0}>
|
||||
<Tooltip placement="top" title={t('chat.input.new_topic', { Command: newTopicShortcut })}>
|
||||
<ActionIconButton
|
||||
onClick={handleCreateSession}
|
||||
disabled={createSessionDisabled}
|
||||
@ -321,7 +326,7 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
|
||||
<ToolbarGroup>
|
||||
<SendMessageButton sendMessage={sendMessage} disabled={sendDisabled} />
|
||||
{canAbort && (
|
||||
<Tooltip placement="top" content={t('chat.input.pause')}>
|
||||
<Tooltip placement="top" title={t('chat.input.pause')}>
|
||||
<ActionIconButton onClick={abortAgentSession} style={{ marginRight: -2 }}>
|
||||
<CirclePause size={20} color="var(--color-error)" />
|
||||
</ActionIconButton>
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { Button } from '@heroui/button'
|
||||
import CodeViewer from '@renderer/components/CodeViewer'
|
||||
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
@ -33,6 +32,7 @@ import {
|
||||
} from '@renderer/types/error'
|
||||
import type { ErrorMessageBlock, Message } from '@renderer/types/newMessage'
|
||||
import { formatAiSdkError, formatError, safeToString } from '@renderer/utils/error'
|
||||
import { Button } from 'antd'
|
||||
import { Alert as AntdAlert, Modal } from 'antd'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
@ -144,9 +144,11 @@ const MessageErrorInfo: React.FC<{ block: ErrorMessageBlock; message: Message }>
|
||||
onClick={showErrorDetail}
|
||||
style={{ cursor: 'pointer' }}
|
||||
action={
|
||||
<Button size="sm" className="p-0" variant="light" onPress={showErrorDetail}>
|
||||
{t('common.detail')}
|
||||
</Button>
|
||||
<>
|
||||
<Button size="middle" color="default" variant="text" onClick={showErrorDetail}>
|
||||
{t('common.detail')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<ErrorDetailModal open={showDetailModal} onClose={() => setShowDetailModal(false)} error={block.error} />
|
||||
@ -198,10 +200,10 @@ const ErrorDetailModal: React.FC<ErrorDetailModalProps> = ({ open, onClose, erro
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
footer={[
|
||||
<Button key="copy" size="sm" variant="light" onPress={copyErrorDetails}>
|
||||
<Button key="copy" variant="text" color="default" onClick={copyErrorDetails}>
|
||||
{t('common.copy')}
|
||||
</Button>,
|
||||
<Button key="close" size="sm" variant="light" onPress={onClose}>
|
||||
<Button key="close" variant="text" color={'default'} onClick={onClose}>
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
]}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Spinner } from '@heroui/react'
|
||||
import { MessageBlockStatus, MessageBlockType, type PlaceholderMessageBlock } from '@renderer/types/newMessage'
|
||||
import React from 'react'
|
||||
import { BeatLoader } from 'react-spinners'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface PlaceholderBlockProps {
|
||||
@ -10,7 +10,7 @@ const PlaceholderBlock: React.FC<PlaceholderBlockProps> = ({ block }) => {
|
||||
if (block.status === MessageBlockStatus.PROCESSING && block.type === MessageBlockType.UNKNOWN) {
|
||||
return (
|
||||
<MessageContentLoading>
|
||||
<Spinner color="current" variant="dots" />
|
||||
<BeatLoader color="var(--color-text-1)" size={8} speedMultiplier={0.8} />
|
||||
</MessageContentLoading>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { cn } from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import HorizontalScrollContainer from '@renderer/components/HorizontalScrollContainer'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
@ -15,7 +14,7 @@ import { getModelUniqId } from '@renderer/services/ModelService'
|
||||
import { estimateMessageUsage } from '@renderer/services/TokenService'
|
||||
import type { Assistant, Topic } from '@renderer/types'
|
||||
import type { Message, MessageBlock } from '@renderer/types/newMessage'
|
||||
import { classNames } from '@renderer/utils'
|
||||
import { classNames, cn } from '@renderer/utils'
|
||||
import { isMessageProcessing } from '@renderer/utils/messageUtils/is'
|
||||
import { Divider } from 'antd'
|
||||
import type { Dispatch, FC, SetStateAction } from 'react'
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { AccordionItem, Chip, Code } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { Tag } from 'antd'
|
||||
import { CheckCircle, Terminal, XCircle } from 'lucide-react'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
@ -15,7 +16,13 @@ interface ParsedBashOutput {
|
||||
tool_use_error?: string
|
||||
}
|
||||
|
||||
export function BashOutputTool({ input, output }: { input: BashOutputToolInput; output?: BashOutputToolOutput }) {
|
||||
export function BashOutputTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: BashOutputToolInput
|
||||
output?: BashOutputToolOutput
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
// 解析 XML 输出
|
||||
const parsedOutput = useMemo(() => {
|
||||
if (!output) return null
|
||||
@ -84,93 +91,88 @@ export function BashOutputTool({ input, output }: { input: BashOutputToolInput;
|
||||
} as const
|
||||
}, [parsedOutput])
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
key={AgentToolsType.BashOutput}
|
||||
aria-label="BashOutput Tool"
|
||||
title={
|
||||
const children = parsedOutput ? (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Status Info */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{parsedOutput.exit_code !== undefined && (
|
||||
<Tag color={parsedOutput.exit_code === 0 ? 'success' : 'danger'}>Exit Code: {parsedOutput.exit_code}</Tag>
|
||||
)}
|
||||
{parsedOutput.timestamp && (
|
||||
<Tag className="py-0 font-mono text-xs">{new Date(parsedOutput.timestamp).toLocaleString()}</Tag>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Standard Output */}
|
||||
{parsedOutput.stdout && (
|
||||
<div>
|
||||
<div className="mb-2 font-medium text-default-600 text-xs">stdout:</div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-default-700 text-xs dark:text-default-300">
|
||||
{parsedOutput.stdout}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Standard Error */}
|
||||
{parsedOutput.stderr && (
|
||||
<div className="border border-danger-200">
|
||||
<div className="mb-2 font-medium text-danger-600 text-xs">stderr:</div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-danger-600 text-xs dark:text-danger-400">
|
||||
{parsedOutput.stderr}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tool Use Error */}
|
||||
{parsedOutput.tool_use_error && (
|
||||
<div className="border border-danger-200">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<XCircle className="h-4 w-4 text-danger" />
|
||||
<span className="font-medium text-danger-600 text-xs">Error:</span>
|
||||
</div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-danger-600 text-xs dark:text-danger-400">
|
||||
{parsedOutput.tool_use_error}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
// 原始输出(如果解析失败或非 XML 格式)
|
||||
output && (
|
||||
<div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-default-700 text-xs dark:text-default-300">{output}</pre>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
return {
|
||||
key: AgentToolsType.BashOutput,
|
||||
label: (
|
||||
<>
|
||||
<ToolTitle
|
||||
icon={<Terminal className="h-4 w-4" />}
|
||||
label="Bash Output"
|
||||
params={
|
||||
<div className="flex items-center gap-2">
|
||||
<Code size="sm" className="py-0 text-xs">
|
||||
{input.bash_id}
|
||||
</Code>
|
||||
<Tag className="py-0 font-mono text-xs">{input.bash_id}</Tag>
|
||||
{statusConfig && (
|
||||
<Chip
|
||||
size="sm"
|
||||
<Tag
|
||||
color={statusConfig.color}
|
||||
variant="flat"
|
||||
startContent={statusConfig.icon}
|
||||
className="h-5">
|
||||
icon={statusConfig.icon}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: '2px'
|
||||
}}>
|
||||
{statusConfig.text}
|
||||
</Chip>
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
}
|
||||
classNames={{
|
||||
content: 'space-y-3 px-1'
|
||||
}}>
|
||||
{parsedOutput ? (
|
||||
<>
|
||||
{/* Status Info */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{parsedOutput.exit_code !== undefined && (
|
||||
<Chip size="sm" color={parsedOutput.exit_code === 0 ? 'success' : 'danger'} variant="flat">
|
||||
Exit Code: {parsedOutput.exit_code}
|
||||
</Chip>
|
||||
)}
|
||||
{parsedOutput.timestamp && (
|
||||
<Code size="sm" className="py-0 text-xs">
|
||||
{new Date(parsedOutput.timestamp).toLocaleString()}
|
||||
</Code>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
|
||||
{/* Standard Output */}
|
||||
{parsedOutput.stdout && (
|
||||
<div>
|
||||
<div className="mb-2 font-medium text-default-600 text-xs">stdout:</div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-default-700 text-xs dark:text-default-300">
|
||||
{parsedOutput.stdout}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Standard Error */}
|
||||
{parsedOutput.stderr && (
|
||||
<div className="border border-danger-200">
|
||||
<div className="mb-2 font-medium text-danger-600 text-xs">stderr:</div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-danger-600 text-xs dark:text-danger-400">
|
||||
{parsedOutput.stderr}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tool Use Error */}
|
||||
{parsedOutput.tool_use_error && (
|
||||
<div className="border border-danger-200">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<XCircle className="h-4 w-4 text-danger" />
|
||||
<span className="font-medium text-danger-600 text-xs">Error:</span>
|
||||
</div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-danger-600 text-xs dark:text-danger-400">
|
||||
{parsedOutput.tool_use_error}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
// 原始输出(如果解析失败或非 XML 格式)
|
||||
output && (
|
||||
<div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-default-700 text-xs dark:text-default-300">{output}</pre>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</AccordionItem>
|
||||
)
|
||||
children: children
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,31 +1,35 @@
|
||||
import { AccordionItem, Code } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { Tag } from 'antd'
|
||||
import { Terminal } from 'lucide-react'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
import type { BashToolInput as BashToolInputType, BashToolOutput as BashToolOutputType } from './types'
|
||||
|
||||
export function BashTool({ input, output }: { input: BashToolInputType; output?: BashToolOutputType }) {
|
||||
export function BashTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: BashToolInputType
|
||||
output?: BashToolOutputType
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
// 如果有输出,计算输出行数
|
||||
const outputLines = output ? output.split('\n').length : 0
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
key="tool"
|
||||
aria-label="Bash Tool"
|
||||
title={
|
||||
return {
|
||||
key: 'tool',
|
||||
label: (
|
||||
<>
|
||||
<ToolTitle
|
||||
icon={<Terminal className="h-4 w-4" />}
|
||||
label="Bash"
|
||||
params={input.description}
|
||||
stats={output ? `${outputLines} ${outputLines === 1 ? 'line' : 'lines'}` : undefined}
|
||||
/>
|
||||
}
|
||||
subtitle={
|
||||
<Code size="sm" className="line-clamp-1 w-max max-w-full text-ellipsis py-0 text-xs">
|
||||
{input.command}
|
||||
</Code>
|
||||
}>
|
||||
<div className="whitespace-pre-line">{output}</div>
|
||||
</AccordionItem>
|
||||
)
|
||||
<div className="mt-1">
|
||||
<Tag className="whitespace-pre-wrap break-all font-mono">{input.command}</Tag>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
children: <div className="whitespace-pre-line">{output}</div>
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { FileEdit } from 'lucide-react'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
@ -28,19 +28,26 @@ export const renderCodeBlock = (content: string, variant: 'old' | 'new') => {
|
||||
)
|
||||
}
|
||||
|
||||
export function EditTool({ input, output }: { input: EditToolInput; output?: EditToolOutput }) {
|
||||
return (
|
||||
<AccordionItem
|
||||
key={AgentToolsType.Edit}
|
||||
aria-label="Edit Tool"
|
||||
title={<ToolTitle icon={<FileEdit className="h-4 w-4" />} label="Edit" params={input.file_path} />}>
|
||||
{/* Diff View */}
|
||||
{/* Old Content */}
|
||||
{renderCodeBlock(input.old_string, 'old')}
|
||||
{/* New Content */}
|
||||
{renderCodeBlock(input.new_string, 'new')}
|
||||
{/* Output */}
|
||||
{output}
|
||||
</AccordionItem>
|
||||
)
|
||||
export function EditTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: EditToolInput
|
||||
output?: EditToolOutput
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
return {
|
||||
key: AgentToolsType.Edit,
|
||||
label: <ToolTitle icon={<FileEdit className="h-4 w-4" />} label="Edit" params={input.file_path} />,
|
||||
children: (
|
||||
<>
|
||||
{/* Diff View */}
|
||||
{/* Old Content */}
|
||||
{renderCodeBlock(input.old_string, 'old')}
|
||||
{/* New Content */}
|
||||
{renderCodeBlock(input.new_string, 'new')}
|
||||
{/* Output */}
|
||||
{output}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { DoorOpen } from 'lucide-react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
|
||||
@ -6,19 +6,22 @@ import { ToolTitle } from './GenericTools'
|
||||
import type { ExitPlanModeToolInput, ExitPlanModeToolOutput } from './types'
|
||||
import { AgentToolsType } from './types'
|
||||
|
||||
export function ExitPlanModeTool({ input, output }: { input: ExitPlanModeToolInput; output?: ExitPlanModeToolOutput }) {
|
||||
return (
|
||||
<AccordionItem
|
||||
key={AgentToolsType.ExitPlanMode}
|
||||
aria-label="ExitPlanMode Tool"
|
||||
title={
|
||||
<ToolTitle
|
||||
icon={<DoorOpen className="h-4 w-4" />}
|
||||
label="ExitPlanMode"
|
||||
stats={`${input.plan.split('\n\n').length} plans`}
|
||||
/>
|
||||
}>
|
||||
{<ReactMarkdown>{input.plan + '\n\n' + (output ?? '')}</ReactMarkdown>}
|
||||
</AccordionItem>
|
||||
)
|
||||
export function ExitPlanModeTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: ExitPlanModeToolInput
|
||||
output?: ExitPlanModeToolOutput
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
return {
|
||||
key: AgentToolsType.ExitPlanMode,
|
||||
label: (
|
||||
<ToolTitle
|
||||
icon={<DoorOpen className="h-4 w-4" />}
|
||||
label="ExitPlanMode"
|
||||
stats={`${input.plan.split('\n\n').length} plans`}
|
||||
/>
|
||||
),
|
||||
children: <ReactMarkdown>{input.plan + '\n\n' + (output ?? '')}</ReactMarkdown>
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,26 +1,29 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { FolderSearch } from 'lucide-react'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
import type { GlobToolInput as GlobToolInputType, GlobToolOutput as GlobToolOutputType } from './types'
|
||||
|
||||
export function GlobTool({ input, output }: { input: GlobToolInputType; output?: GlobToolOutputType }) {
|
||||
export function GlobTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: GlobToolInputType
|
||||
output?: GlobToolOutputType
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
// 如果有输出,计算文件数量
|
||||
const lineCount = output ? output.split('\n').filter((line) => line.trim()).length : 0
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
key="tool"
|
||||
aria-label="Glob Tool"
|
||||
title={
|
||||
<ToolTitle
|
||||
icon={<FolderSearch className="h-4 w-4" />}
|
||||
label="Glob"
|
||||
params={input.pattern}
|
||||
stats={output ? `${lineCount} of output` : undefined}
|
||||
/>
|
||||
}>
|
||||
<div>{output}</div>
|
||||
</AccordionItem>
|
||||
)
|
||||
return {
|
||||
key: 'tool',
|
||||
label: (
|
||||
<ToolTitle
|
||||
icon={<FolderSearch className="h-4 w-4" />}
|
||||
label="Glob"
|
||||
params={input.pattern}
|
||||
stats={output ? `${lineCount} ${lineCount === 1 ? 'file' : 'files'}` : undefined}
|
||||
/>
|
||||
),
|
||||
children: <div>{output}</div>
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,31 +1,34 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { FileSearch } from 'lucide-react'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
import type { GrepToolInput, GrepToolOutput } from './types'
|
||||
|
||||
export function GrepTool({ input, output }: { input: GrepToolInput; output?: GrepToolOutput }) {
|
||||
export function GrepTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: GrepToolInput
|
||||
output?: GrepToolOutput
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
// 如果有输出,计算结果行数
|
||||
const resultLines = output ? output.split('\n').filter((line) => line.trim()).length : 0
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
key="tool"
|
||||
aria-label="Grep Tool"
|
||||
title={
|
||||
<ToolTitle
|
||||
icon={<FileSearch className="h-4 w-4" />}
|
||||
label="Grep"
|
||||
params={
|
||||
<>
|
||||
{input.pattern}
|
||||
{input.output_mode && <span className="ml-1">({input.output_mode})</span>}
|
||||
</>
|
||||
}
|
||||
stats={output ? `${resultLines} ${resultLines === 1 ? 'line' : 'lines'}` : undefined}
|
||||
/>
|
||||
}>
|
||||
<div>{output}</div>
|
||||
</AccordionItem>
|
||||
)
|
||||
return {
|
||||
key: 'tool',
|
||||
label: (
|
||||
<ToolTitle
|
||||
icon={<FileSearch className="h-4 w-4" />}
|
||||
label="Grep"
|
||||
params={
|
||||
<>
|
||||
{input.pattern}
|
||||
{input.output_mode && <span className="ml-1">({input.output_mode})</span>}
|
||||
</>
|
||||
}
|
||||
stats={output ? `${resultLines} ${resultLines === 1 ? 'line' : 'lines'}` : undefined}
|
||||
/>
|
||||
),
|
||||
children: <div>{output}</div>
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { FileText } from 'lucide-react'
|
||||
|
||||
import { renderCodeBlock } from './EditTool'
|
||||
@ -6,18 +6,24 @@ import { ToolTitle } from './GenericTools'
|
||||
import type { MultiEditToolInput, MultiEditToolOutput } from './types'
|
||||
import { AgentToolsType } from './types'
|
||||
|
||||
export function MultiEditTool({ input }: { input: MultiEditToolInput; output?: MultiEditToolOutput }) {
|
||||
return (
|
||||
<AccordionItem
|
||||
key={AgentToolsType.MultiEdit}
|
||||
aria-label="MultiEdit Tool"
|
||||
title={<ToolTitle icon={<FileText className="h-4 w-4" />} label="MultiEdit" params={input.file_path} />}>
|
||||
{input.edits.map((edit, index) => (
|
||||
<div key={index}>
|
||||
{renderCodeBlock(edit.old_string, 'old')}
|
||||
{renderCodeBlock(edit.new_string, 'new')}
|
||||
</div>
|
||||
))}
|
||||
</AccordionItem>
|
||||
)
|
||||
export function MultiEditTool({
|
||||
input
|
||||
}: {
|
||||
input: MultiEditToolInput
|
||||
output?: MultiEditToolOutput
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
return {
|
||||
key: AgentToolsType.MultiEdit,
|
||||
label: <ToolTitle icon={<FileText className="h-4 w-4" />} label="MultiEdit" params={input.file_path} />,
|
||||
children: (
|
||||
<div>
|
||||
{input.edits.map((edit, index) => (
|
||||
<div key={index}>
|
||||
{renderCodeBlock(edit.old_string, 'old')}
|
||||
{renderCodeBlock(edit.new_string, 'new')}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { Tag } from 'antd'
|
||||
import { FileText } from 'lucide-react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
|
||||
@ -6,14 +7,23 @@ import { ToolTitle } from './GenericTools'
|
||||
import type { NotebookEditToolInput, NotebookEditToolOutput } from './types'
|
||||
import { AgentToolsType } from './types'
|
||||
|
||||
export function NotebookEditTool({ input, output }: { input: NotebookEditToolInput; output?: NotebookEditToolOutput }) {
|
||||
return (
|
||||
<AccordionItem
|
||||
key={AgentToolsType.NotebookEdit}
|
||||
aria-label="NotebookEdit Tool"
|
||||
title={<ToolTitle icon={<FileText className="h-4 w-4" />} label="NotebookEdit" />}
|
||||
subtitle={input.notebook_path}>
|
||||
<ReactMarkdown>{output}</ReactMarkdown>
|
||||
</AccordionItem>
|
||||
)
|
||||
export function NotebookEditTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: NotebookEditToolInput
|
||||
output?: NotebookEditToolOutput
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
return {
|
||||
key: AgentToolsType.NotebookEdit,
|
||||
label: (
|
||||
<>
|
||||
<ToolTitle icon={<FileText className="h-4 w-4" />} label="NotebookEdit" />
|
||||
<Tag className="mt-1" color="blue">
|
||||
{input.notebook_path}{' '}
|
||||
</Tag>
|
||||
</>
|
||||
),
|
||||
children: <ReactMarkdown>{output}</ReactMarkdown>
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { FileText } from 'lucide-react'
|
||||
import { useMemo } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
@ -7,7 +7,13 @@ import { ToolTitle } from './GenericTools'
|
||||
import type { ReadToolInput as ReadToolInputType, ReadToolOutput as ReadToolOutputType, TextOutput } from './types'
|
||||
import { AgentToolsType } from './types'
|
||||
|
||||
export function ReadTool({ input, output }: { input: ReadToolInputType; output?: ReadToolOutputType }) {
|
||||
export function ReadTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: ReadToolInputType
|
||||
output?: ReadToolOutputType
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
// 移除 system-reminder 标签及其内容的辅助函数
|
||||
const removeSystemReminderTags = (text: string): string => {
|
||||
// 使用正则表达式匹配 <system-reminder> 标签及其内容,包括换行符
|
||||
@ -53,19 +59,16 @@ export function ReadTool({ input, output }: { input: ReadToolInputType; output?:
|
||||
}
|
||||
}, [outputString])
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
key={AgentToolsType.Read}
|
||||
aria-label="Read Tool"
|
||||
title={
|
||||
<ToolTitle
|
||||
icon={<FileText className="h-4 w-4" />}
|
||||
label="Read File"
|
||||
params={input.file_path.split('/').pop()}
|
||||
stats={stats ? `${stats.lineCount} lines, ${stats.formatSize(stats.fileSize)}` : undefined}
|
||||
/>
|
||||
}>
|
||||
{outputString ? <ReactMarkdown>{outputString}</ReactMarkdown> : null}
|
||||
</AccordionItem>
|
||||
)
|
||||
return {
|
||||
key: AgentToolsType.Read,
|
||||
label: (
|
||||
<ToolTitle
|
||||
icon={<FileText className="h-4 w-4" />}
|
||||
label="Read File"
|
||||
params={input.file_path.split('/').pop()}
|
||||
stats={stats ? `${stats.lineCount} lines, ${stats.formatSize(stats.fileSize)}` : undefined}
|
||||
/>
|
||||
),
|
||||
children: outputString ? <ReactMarkdown>{outputString}</ReactMarkdown> : null
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,25 +1,30 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { Search } from 'lucide-react'
|
||||
|
||||
import { StringInputTool, StringOutputTool, ToolTitle } from './GenericTools'
|
||||
import type { SearchToolInput as SearchToolInputType, SearchToolOutput as SearchToolOutputType } from './types'
|
||||
|
||||
export function SearchTool({ input, output }: { input: SearchToolInputType; output?: SearchToolOutputType }) {
|
||||
export function SearchTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: SearchToolInputType
|
||||
output?: SearchToolOutputType
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
// 如果有输出,计算结果数量
|
||||
const resultCount = output ? output.split('\n').filter((line) => line.trim()).length : 0
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
key="tool"
|
||||
aria-label="Search Tool"
|
||||
title={
|
||||
<ToolTitle
|
||||
icon={<Search className="h-4 w-4" />}
|
||||
label="Search"
|
||||
params={`"${input}"`}
|
||||
stats={output ? `${resultCount} ${resultCount === 1 ? 'result' : 'results'}` : undefined}
|
||||
/>
|
||||
}>
|
||||
return {
|
||||
key: 'tool',
|
||||
label: (
|
||||
<ToolTitle
|
||||
icon={<Search className="h-4 w-4" />}
|
||||
label="Search"
|
||||
params={`"${input}"`}
|
||||
stats={output ? `${resultCount} ${resultCount === 1 ? 'result' : 'results'}` : undefined}
|
||||
/>
|
||||
),
|
||||
children: (
|
||||
<div>
|
||||
<StringInputTool input={input} label="Search Query" />
|
||||
{output && (
|
||||
@ -28,6 +33,6 @@ export function SearchTool({ input, output }: { input: SearchToolInputType; outp
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AccordionItem>
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,16 +1,19 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { PencilRuler } from 'lucide-react'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
import type { SkillToolInput, SkillToolOutput } from './types'
|
||||
|
||||
export function SkillTool({ input, output }: { input: SkillToolInput; output?: SkillToolOutput }) {
|
||||
return (
|
||||
<AccordionItem
|
||||
key="tool"
|
||||
aria-label="Skill Tool"
|
||||
title={<ToolTitle icon={<PencilRuler className="h-4 w-4" />} label="Skill" params={input.command} />}>
|
||||
{output}
|
||||
</AccordionItem>
|
||||
)
|
||||
export function SkillTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: SkillToolInput
|
||||
output?: SkillToolOutput
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
return {
|
||||
key: 'tool',
|
||||
label: <ToolTitle icon={<PencilRuler className="h-4 w-4" />} label="Skill" params={input.command} />,
|
||||
children: <div>{output}</div>
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,21 +1,28 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { Bot } from 'lucide-react'
|
||||
import Markdown from 'react-markdown'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
import type { TaskToolInput as TaskToolInputType, TaskToolOutput as TaskToolOutputType } from './types'
|
||||
|
||||
export function TaskTool({ input, output }: { input: TaskToolInputType; output?: TaskToolOutputType }) {
|
||||
return (
|
||||
<AccordionItem
|
||||
key="tool"
|
||||
aria-label="Task Tool"
|
||||
title={<ToolTitle icon={<Bot className="h-4 w-4" />} label="Task" params={input.description} />}>
|
||||
{output?.map((item) => (
|
||||
<div key={item.type}>
|
||||
<div>{item.type === 'text' ? <Markdown>{item.text}</Markdown> : item.text}</div>
|
||||
</div>
|
||||
))}
|
||||
</AccordionItem>
|
||||
)
|
||||
export function TaskTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: TaskToolInputType
|
||||
output?: TaskToolOutputType
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
return {
|
||||
key: 'tool',
|
||||
label: <ToolTitle icon={<Bot className="h-4 w-4" />} label="Task" params={input.description} />,
|
||||
children: (
|
||||
<div>
|
||||
{output?.map((item) => (
|
||||
<div key={item.type}>
|
||||
<div>{item.type === 'text' ? <Markdown>{item.text}</Markdown> : item.text}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import { AccordionItem, Card, CardBody, Chip } from '@heroui/react'
|
||||
import { cn } from '@renderer/utils'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { Card } from 'antd'
|
||||
import { CheckCircle, Circle, Clock, ListTodo } from 'lucide-react'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
@ -30,44 +32,59 @@ const getStatusConfig = (status: TodoItem['status']) => {
|
||||
}
|
||||
}
|
||||
|
||||
export function TodoWriteTool({ input }: { input: TodoWriteToolInputType }) {
|
||||
export function TodoWriteTool({
|
||||
input
|
||||
}: {
|
||||
input: TodoWriteToolInputType
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
const doneCount = input.todos.filter((todo) => todo.status === 'completed').length
|
||||
return (
|
||||
<AccordionItem
|
||||
key={AgentToolsType.TodoWrite}
|
||||
aria-label="Todo Write Tool"
|
||||
title={
|
||||
<ToolTitle
|
||||
icon={<ListTodo className="h-4 w-4" />}
|
||||
label="Todo Write"
|
||||
params={`${doneCount} Done`}
|
||||
stats={`${input.todos.length} ${input.todos.length === 1 ? 'item' : 'items'}`}
|
||||
/>
|
||||
}>
|
||||
|
||||
return {
|
||||
key: AgentToolsType.TodoWrite,
|
||||
label: (
|
||||
<ToolTitle
|
||||
icon={<ListTodo className="h-4 w-4" />}
|
||||
label="Todo Write"
|
||||
params={`${doneCount} Done`}
|
||||
stats={`${input.todos.length} ${input.todos.length === 1 ? 'item' : 'items'}`}
|
||||
/>
|
||||
),
|
||||
children: (
|
||||
<div className="space-y-3">
|
||||
{input.todos.map((todo, index) => {
|
||||
const statusConfig = getStatusConfig(todo.status)
|
||||
return (
|
||||
<Card key={index} className="shadow-sm">
|
||||
<CardBody className="p-2">
|
||||
<div className="flex items-start gap-3">
|
||||
<Chip color={statusConfig.color} variant="flat" size="sm" className="flex-shrink-0">
|
||||
{statusConfig.icon}
|
||||
</Chip>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className={`text-sm ${todo.status === 'completed' ? 'text-default-500 line-through' : ''}`}>
|
||||
{todo.status === 'completed' ? <s>{todo.content}</s> : todo.content}
|
||||
<div key={index}>
|
||||
<Card
|
||||
key={index}
|
||||
className="shadow-sm"
|
||||
styles={{
|
||||
body: { padding: 2 }
|
||||
}}>
|
||||
<div className="p-2">
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-center rounded-full border bg-opacity-50 p-2',
|
||||
`bg-${statusConfig.color}`
|
||||
)}>
|
||||
{statusConfig.icon}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className={`text-sm ${todo.status === 'completed' ? 'text-default-500 line-through' : ''}`}>
|
||||
{todo.status === 'completed' ? <s>{todo.content}</s> : todo.content}
|
||||
</div>
|
||||
{todo.status === 'in_progress' && (
|
||||
<div className="mt-1 text-default-400 text-xs">{todo.activeForm}</div>
|
||||
)}
|
||||
</div>
|
||||
{todo.status === 'in_progress' && (
|
||||
<div className="mt-1 text-default-400 text-xs">{todo.activeForm}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</AccordionItem>
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { Wrench } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
@ -11,7 +11,11 @@ interface UnknownToolProps {
|
||||
output?: unknown
|
||||
}
|
||||
|
||||
export function UnknownToolRenderer({ toolName = '', input, output }: UnknownToolProps) {
|
||||
export function UnknownToolRenderer({
|
||||
toolName = '',
|
||||
input,
|
||||
output
|
||||
}: UnknownToolProps): NonNullable<CollapseProps['items']>[number] {
|
||||
const { highlightCode } = useCodeStyle()
|
||||
const [inputHtml, setInputHtml] = useState<string>('')
|
||||
const [outputHtml, setOutputHtml] = useState<string>('')
|
||||
@ -47,17 +51,16 @@ export function UnknownToolRenderer({ toolName = '', input, output }: UnknownToo
|
||||
return 'Tool'
|
||||
}
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
key="unknown-tool"
|
||||
aria-label={toolName}
|
||||
title={
|
||||
<ToolTitle
|
||||
icon={<Wrench className="h-4 w-4" />}
|
||||
label={getToolDisplayName(toolName)}
|
||||
params={getToolDescription()}
|
||||
/>
|
||||
}>
|
||||
return {
|
||||
key: 'unknown-tool',
|
||||
label: (
|
||||
<ToolTitle
|
||||
icon={<Wrench className="h-4 w-4" />}
|
||||
label={getToolDisplayName(toolName)}
|
||||
params={getToolDescription()}
|
||||
/>
|
||||
),
|
||||
children: (
|
||||
<div className="space-y-3">
|
||||
{input !== undefined && (
|
||||
<div>
|
||||
@ -83,6 +86,6 @@ export function UnknownToolRenderer({ toolName = '', input, output }: UnknownToo
|
||||
<div className="text-foreground-500 text-xs">No data available for this tool</div>
|
||||
)}
|
||||
</div>
|
||||
</AccordionItem>
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,17 +1,19 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { Globe } from 'lucide-react'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
import type { WebFetchToolInput, WebFetchToolOutput } from './types'
|
||||
|
||||
export function WebFetchTool({ input, output }: { input: WebFetchToolInput; output?: WebFetchToolOutput }) {
|
||||
return (
|
||||
<AccordionItem
|
||||
key="tool"
|
||||
aria-label="Web Fetch Tool"
|
||||
title={<ToolTitle icon={<Globe className="h-4 w-4" />} label="Web Fetch" params={input.url} />}
|
||||
subtitle={input.prompt}>
|
||||
{output}
|
||||
</AccordionItem>
|
||||
)
|
||||
export function WebFetchTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: WebFetchToolInput
|
||||
output?: WebFetchToolOutput
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
return {
|
||||
key: 'tool',
|
||||
label: <ToolTitle icon={<Globe className="h-4 w-4" />} label="Web Fetch" params={input.url} />,
|
||||
children: <div>{output}</div>
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,26 +1,29 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { Globe } from 'lucide-react'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
import type { WebSearchToolInput, WebSearchToolOutput } from './types'
|
||||
|
||||
export function WebSearchTool({ input, output }: { input: WebSearchToolInput; output?: WebSearchToolOutput }) {
|
||||
export function WebSearchTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: WebSearchToolInput
|
||||
output?: WebSearchToolOutput
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
// 如果有输出,计算结果数量
|
||||
const resultCount = output ? output.split('\n').filter((line) => line.trim()).length : 0
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
key="tool"
|
||||
aria-label="Web Search Tool"
|
||||
title={
|
||||
<ToolTitle
|
||||
icon={<Globe className="h-4 w-4" />}
|
||||
label="Web Search"
|
||||
params={input.query}
|
||||
stats={output ? `${resultCount} ${resultCount === 1 ? 'result' : 'results'}` : undefined}
|
||||
/>
|
||||
}>
|
||||
{output}
|
||||
</AccordionItem>
|
||||
)
|
||||
return {
|
||||
key: 'tool',
|
||||
label: (
|
||||
<ToolTitle
|
||||
icon={<Globe className="h-4 w-4" />}
|
||||
label="Web Search"
|
||||
params={input.query}
|
||||
stats={output ? `${resultCount} ${resultCount === 1 ? 'result' : 'results'}` : undefined}
|
||||
/>
|
||||
),
|
||||
children: <div>{output}</div>
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,16 +1,18 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { FileText } from 'lucide-react'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
import type { WriteToolInput, WriteToolOutput } from './types'
|
||||
|
||||
export function WriteTool({ input }: { input: WriteToolInput; output?: WriteToolOutput }) {
|
||||
return (
|
||||
<AccordionItem
|
||||
key="tool"
|
||||
aria-label="Write Tool"
|
||||
title={<ToolTitle icon={<FileText className="h-4 w-4" />} label="Write" params={input.file_path} />}>
|
||||
<div>{input.content}</div>
|
||||
</AccordionItem>
|
||||
)
|
||||
export function WriteTool({
|
||||
input
|
||||
}: {
|
||||
input: WriteToolInput
|
||||
output?: WriteToolOutput
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
return {
|
||||
key: 'tool',
|
||||
label: <ToolTitle icon={<FileText className="h-4 w-4" />} label="Write" params={input.file_path} />,
|
||||
children: <div>{input.content}</div>
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
import { Accordion } from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import type { NormalToolResponse } from '@renderer/types'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { Collapse } from 'antd'
|
||||
|
||||
// 导出所有类型
|
||||
export * from './types'
|
||||
|
||||
import { useMemo } from 'react'
|
||||
|
||||
// 导入所有渲染器
|
||||
import ToolPermissionRequestCard from '../ToolPermissionRequestCard'
|
||||
import { BashOutputTool } from './BashOutputTool'
|
||||
@ -58,25 +61,27 @@ export function isValidAgentToolsType(toolName: unknown): toolName is AgentTools
|
||||
function renderToolContent(toolName: AgentToolsType, input: ToolInput, output?: ToolOutput) {
|
||||
const Renderer = toolRenderers[toolName]
|
||||
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const toolContentItem = useMemo(() => {
|
||||
const rendered = Renderer
|
||||
? Renderer({ input: input as any, output: output as any })
|
||||
: UnknownToolRenderer({ input: input as any, output: output as any, toolName })
|
||||
return {
|
||||
...rendered,
|
||||
classNames: {
|
||||
body: 'bg-foreground-50 p-2 text-foreground-900 dark:bg-foreground-100 max-h-96 p-2 overflow-scroll'
|
||||
} as NonNullable<CollapseProps['items']>[number]['classNames']
|
||||
} as NonNullable<CollapseProps['items']>[number]
|
||||
}, [Renderer, input, output, toolName])
|
||||
|
||||
return (
|
||||
<div className="w-max max-w-full rounded-md bg-foreground-100 py-1 transition-all duration-300 ease-in-out dark:bg-foreground-100">
|
||||
<Accordion
|
||||
className="w-max max-w-full"
|
||||
itemClasses={{
|
||||
trigger:
|
||||
'p-0 [&>div:first-child]:!flex-none [&>div:first-child]:flex [&>div:first-child]:flex-col [&>div:first-child]:text-start [&>div:first-child]:max-w-full',
|
||||
indicator: 'flex-shrink-0',
|
||||
subtitle: 'text-xs',
|
||||
content:
|
||||
'rounded-md bg-foreground-50 p-2 text-foreground-900 dark:bg-foreground-100 max-h-96 p-2 overflow-scroll',
|
||||
base: 'space-y-1'
|
||||
}}
|
||||
defaultExpandedKeys={toolName === AgentToolsType.TodoWrite ? [AgentToolsType.TodoWrite] : []}>
|
||||
{Renderer
|
||||
? Renderer({ input: input as any, output: output as any })
|
||||
: UnknownToolRenderer({ input: input as any, output: output as any, toolName })}
|
||||
</Accordion>
|
||||
</div>
|
||||
<Collapse
|
||||
className="w-max max-w-full"
|
||||
expandIconPosition="end"
|
||||
size="small"
|
||||
defaultActiveKey={toolName === AgentToolsType.TodoWrite ? [AgentToolsType.TodoWrite] : []}
|
||||
items={[toolContentItem]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import type { PermissionUpdate } from '@anthropic-ai/claude-agent-sdk'
|
||||
import { Button, Chip, ScrollShadow } from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { selectPendingPermissionByToolName, toolPermissionsActions } from '@renderer/store/toolPermissions'
|
||||
import type { NormalToolResponse } from '@renderer/types'
|
||||
import { Button } from 'antd'
|
||||
import { ChevronDown, CirclePlay, CircleX } from 'lucide-react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -127,33 +127,39 @@ export function ToolPermissionRequestCard({ toolResponse }: Props) {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||
<Chip color={isExpired ? 'danger' : 'warning'} size="sm" variant="flat">
|
||||
<div
|
||||
className={`rounded px-2 py-0.5 font-medium text-xs ${
|
||||
isExpired ? 'text-[var(--color-error)]' : 'text-[var(--color-status-warning)]'
|
||||
}`}>
|
||||
{isExpired
|
||||
? t('agent.toolPermission.expired')
|
||||
: t('agent.toolPermission.pending', { seconds: remainingSeconds })}
|
||||
</Chip>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
aria-label={t('agent.toolPermission.aria.denyRequest')}
|
||||
className="h-8"
|
||||
color="danger"
|
||||
isDisabled={isSubmitting || isExpired}
|
||||
isLoading={isSubmittingDeny}
|
||||
onPress={() => handleDecision('deny')}
|
||||
startContent={<CircleX size={16} />}
|
||||
variant="bordered">
|
||||
disabled={isSubmitting || isExpired}
|
||||
loading={isSubmittingDeny}
|
||||
onClick={() => handleDecision('deny')}
|
||||
icon={<CircleX size={16} />}
|
||||
iconPosition={'start'}
|
||||
variant="outlined">
|
||||
{t('agent.toolPermission.button.cancel')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
aria-label={t('agent.toolPermission.aria.allowRequest')}
|
||||
className="h-8 px-3"
|
||||
color="success"
|
||||
isDisabled={isSubmitting || isExpired}
|
||||
isLoading={isSubmittingAllow}
|
||||
onPress={() => handleDecision('allow')}
|
||||
startContent={<CirclePlay size={16} />}>
|
||||
color="primary"
|
||||
disabled={isSubmitting || isExpired}
|
||||
loading={isSubmittingAllow}
|
||||
onClick={() => handleDecision('allow')}
|
||||
icon={<CirclePlay size={16} />}
|
||||
iconPosition={'start'}
|
||||
variant="solid">
|
||||
{t('agent.toolPermission.button.run')}
|
||||
</Button>
|
||||
|
||||
@ -161,12 +167,12 @@ export function ToolPermissionRequestCard({ toolResponse }: Props) {
|
||||
aria-label={
|
||||
showDetails ? t('agent.toolPermission.aria.hideDetails') : t('agent.toolPermission.aria.showDetails')
|
||||
}
|
||||
className="h-8"
|
||||
isIconOnly
|
||||
onPress={() => setShowDetails((value) => !value)}
|
||||
variant="light">
|
||||
<ChevronDown className={`transition-transform ${showDetails ? 'rotate-180' : ''}`} size={16} />
|
||||
</Button>
|
||||
className="h-8 text-default-600 transition-colors hover:bg-default-200/50 hover:text-default-800"
|
||||
onClick={() => setShowDetails((value) => !value)}
|
||||
icon={<ChevronDown className={`transition-transform ${showDetails ? 'rotate-180' : ''}`} size={16} />}
|
||||
variant="text"
|
||||
style={{ backgroundColor: 'transparent' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -181,9 +187,9 @@ export function ToolPermissionRequestCard({ toolResponse }: Props) {
|
||||
<p className="mb-2 font-medium text-default-400 text-xs uppercase tracking-wide">
|
||||
{t('agent.toolPermission.inputPreview')}
|
||||
</p>
|
||||
<ScrollShadow className="max-h-48 font-mono text-xs" hideScrollBar>
|
||||
<pre className="whitespace-pre-wrap break-all text-left">{request.inputPreview}</pre>
|
||||
</ScrollShadow>
|
||||
<div className="max-h-[192px] overflow-auto font-mono text-xs">
|
||||
<pre className="whitespace-pre-wrap break-all p-2 text-left">{request.inputPreview}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{request.requiresPermissions && (
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { Alert, Spinner } from '@heroui/react'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { useAgents } from '@renderer/hooks/agents/useAgents'
|
||||
import { useApiServer } from '@renderer/hooks/useApiServer'
|
||||
@ -7,13 +6,9 @@ import { useAssistantPresets } from '@renderer/hooks/useAssistantPresets'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useAssistantsTabSortType } from '@renderer/hooks/useStore'
|
||||
import { useTags } from '@renderer/hooks/useTags'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { addIknowAction } from '@renderer/store/runtime'
|
||||
import type { Assistant, AssistantsSortType, Topic } from '@renderer/types'
|
||||
import { getErrorMessage } from '@renderer/utils'
|
||||
import type { FC } from 'react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import UnifiedAddButton from './components/UnifiedAddButton'
|
||||
@ -31,16 +26,12 @@ interface AssistantsTabProps {
|
||||
onCreateDefaultAssistant: () => void
|
||||
}
|
||||
|
||||
const ALERT_KEY = 'enable_api_server_to_use_agent'
|
||||
|
||||
const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
||||
const { activeAssistant, setActiveAssistant, onCreateAssistant, onCreateDefaultAssistant } = props
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const { t } = useTranslation()
|
||||
const { apiServerConfig, apiServerRunning, apiServerLoading } = useApiServer()
|
||||
const { apiServerConfig } = useApiServer()
|
||||
const apiServerEnabled = apiServerConfig.enabled
|
||||
const { iknow, chat } = useRuntime()
|
||||
const dispatch = useAppDispatch()
|
||||
const { chat } = useRuntime()
|
||||
|
||||
// Agent related hooks
|
||||
const { agents, deleteAgent, isLoading: agentsLoading, error: agentsError } = useAgents()
|
||||
@ -126,31 +117,6 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
||||
|
||||
return (
|
||||
<Container className="assistants-tab" ref={containerRef}>
|
||||
{!apiServerConfig.enabled && !apiServerRunning && !iknow[ALERT_KEY] && (
|
||||
<Alert
|
||||
color="warning"
|
||||
title={t('agent.warning.enable_server')}
|
||||
isClosable
|
||||
onClose={() => {
|
||||
dispatch(addIknowAction(ALERT_KEY))
|
||||
}}
|
||||
className="mb-2"
|
||||
/>
|
||||
)}
|
||||
|
||||
{(agentsLoading || apiServerLoading) && <Spinner />}
|
||||
{apiServerConfig.enabled && !apiServerLoading && !apiServerRunning && (
|
||||
<Alert color="danger" title={t('agent.server.error.not_running')} isClosable className="mb-2" />
|
||||
)}
|
||||
{apiServerRunning && agentsError && (
|
||||
<Alert
|
||||
color="danger"
|
||||
title={t('agent.list.error.failed')}
|
||||
description={getErrorMessage(agentsError)}
|
||||
className="mb-2"
|
||||
/>
|
||||
)}
|
||||
|
||||
<UnifiedAddButton
|
||||
onCreateAssistant={onCreateAssistant}
|
||||
setActiveAssistant={setActiveAssistant}
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { Button, Divider } from '@heroui/react'
|
||||
import type { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
|
||||
import { SettingDivider } from '@renderer/pages/settings'
|
||||
import { SessionSettingsPopup } from '@renderer/pages/settings/AgentSettings'
|
||||
import AdvancedSettings from '@renderer/pages/settings/AgentSettings/AdvancedSettings'
|
||||
import EssentialSettings from '@renderer/pages/settings/AgentSettings/EssentialSettings'
|
||||
import type { GetAgentSessionResponse } from '@renderer/types'
|
||||
import { Button } from 'antd'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@ -31,9 +32,10 @@ const SessionSettingsTab: FC<Props> = ({ session, update }) => {
|
||||
return (
|
||||
<div className="w-[var(--assistants-width)] p-2 px-3 pt-4">
|
||||
<EssentialSettings agentBase={session} update={update} showModelSetting={false} />
|
||||
<SettingDivider />
|
||||
<AdvancedSettings agentBase={session} update={update} />
|
||||
<Divider className="my-2" />
|
||||
<Button size="sm" fullWidth onPress={onMoreSetting}>
|
||||
<SettingDivider />
|
||||
<Button size="small" block onClick={onMoreSetting}>
|
||||
{t('settings.moresetting.label')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { Alert, cn } from '@heroui/react'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { cn } from '@renderer/utils'
|
||||
import { Alert } from 'antd'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import type { FC } from 'react'
|
||||
import { memo } from 'react'
|
||||
@ -17,19 +18,11 @@ const SessionsTab: FC<SessionsTabProps> = () => {
|
||||
const { apiServer } = useSettings()
|
||||
|
||||
if (!apiServer.enabled) {
|
||||
return (
|
||||
<div>
|
||||
<Alert color="warning" title={t('agent.warning.enable_server')} />
|
||||
</div>
|
||||
)
|
||||
return <Alert type="warning" message={t('agent.warning.enable_server')} style={{ margin: 10 }} />
|
||||
}
|
||||
|
||||
if (!activeAgentId) {
|
||||
return (
|
||||
<div>
|
||||
<Alert color="warning" title={'Select an agent'} />
|
||||
</div>
|
||||
)
|
||||
return <Alert type="warning" message={'Select an agent'} style={{ margin: 10 }} />
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@ -1,18 +1,32 @@
|
||||
import type { ButtonProps } from '@heroui/react'
|
||||
import { Button, cn } from '@heroui/react'
|
||||
import type { ButtonProps } from 'antd'
|
||||
import { Button } from 'antd'
|
||||
import { PlusIcon } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const AddButton = ({ children, className, ...props }: ButtonProps) => {
|
||||
const StyledButton = styled(Button)`
|
||||
height: 36px;
|
||||
width: calc(var(--assistants-width) - 20px);
|
||||
justify-content: flex-start;
|
||||
border-radius: var(--list-item-border-radius);
|
||||
padding: 0 12px;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-2);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-list-item);
|
||||
}
|
||||
`
|
||||
|
||||
const AddButton: FC<ButtonProps> = ({ ...props }) => {
|
||||
return (
|
||||
<Button
|
||||
className={cn(
|
||||
'h-9 w-[calc(var(--assistants-width)-20px)] justify-start rounded-lg bg-transparent px-3 text-[13px] text-[var(--color-text-2)] hover:bg-[var(--color-list-item)]',
|
||||
className
|
||||
)}
|
||||
startContent={<PlusIcon size={16} className="shrink-0" />}
|
||||
{...props}>
|
||||
{children}
|
||||
</Button>
|
||||
<StyledButton
|
||||
{...props}
|
||||
type="text"
|
||||
onClick={props.onClick}
|
||||
icon={<PlusIcon size={16} style={{ flexShrink: 0 }} />}>
|
||||
{props.children}
|
||||
</StyledButton>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { cn, Tooltip } from '@heroui/react'
|
||||
import { DeleteIcon, EditIcon } from '@renderer/components/Icons'
|
||||
import { useSessions } from '@renderer/hooks/agents/useSessions'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
@ -6,10 +5,12 @@ import AgentSettingsPopup from '@renderer/pages/settings/AgentSettings/AgentSett
|
||||
import { AgentLabel } from '@renderer/pages/settings/AgentSettings/shared'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import type { AgentEntity } from '@renderer/types'
|
||||
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@renderer/ui/context-menu'
|
||||
import { cn } from '@renderer/utils'
|
||||
import type { MenuProps } from 'antd'
|
||||
import { Dropdown, Tooltip } from 'antd'
|
||||
import { Bot } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import { memo, useCallback } from 'react'
|
||||
import { memo, useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
// const logger = loggerService.withContext('AgentItem')
|
||||
@ -36,45 +37,52 @@ const AgentItem: FC<AgentItemProps> = ({ agent, isActive, onDelete, onPress }) =
|
||||
onPress()
|
||||
}, [clickAssistantToShowTopic, topicPosition, onPress])
|
||||
|
||||
const menuItems: MenuProps['items'] = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: t('common.edit'),
|
||||
key: 'edit',
|
||||
icon: <EditIcon size={14} />,
|
||||
onClick: () => AgentSettingsPopup.show({ agentId: agent.id })
|
||||
},
|
||||
{
|
||||
label: t('common.delete'),
|
||||
key: 'delete',
|
||||
icon: <DeleteIcon size={14} className="lucide-custom" />,
|
||||
danger: true,
|
||||
onClick: () => {
|
||||
window.modal.confirm({
|
||||
title: t('agent.delete.title'),
|
||||
content: t('agent.delete.content'),
|
||||
centered: true,
|
||||
okButtonProps: { danger: true },
|
||||
onOk: () => onDelete(agent)
|
||||
})
|
||||
}
|
||||
}
|
||||
],
|
||||
[t, agent, onDelete]
|
||||
)
|
||||
|
||||
return (
|
||||
<ContextMenu modal={false}>
|
||||
<ContextMenuTrigger>
|
||||
<Container onClick={handlePress} isActive={isActive}>
|
||||
<AssistantNameRow className="name" title={agent.name ?? agent.id}>
|
||||
<AgentNameWrapper>
|
||||
<AgentLabel agent={agent} />
|
||||
</AgentNameWrapper>
|
||||
{isActive && (
|
||||
<MenuButton>
|
||||
<SessionCount>{sessions.length}</SessionCount>
|
||||
</MenuButton>
|
||||
)}
|
||||
{!isActive && <BotIcon />}
|
||||
</AssistantNameRow>
|
||||
</Container>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem key="edit" onClick={() => AgentSettingsPopup.show({ agentId: agent.id })}>
|
||||
<EditIcon size={14} />
|
||||
{t('common.edit')}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
key="delete"
|
||||
className="text-danger"
|
||||
onClick={() => {
|
||||
window.modal.confirm({
|
||||
title: t('agent.delete.title'),
|
||||
content: t('agent.delete.content'),
|
||||
centered: true,
|
||||
okButtonProps: { danger: true },
|
||||
onOk: () => onDelete(agent)
|
||||
})
|
||||
}}>
|
||||
<DeleteIcon size={14} className="lucide-custom text-danger" />
|
||||
<span className="text-danger">{t('common.delete')}</span>
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
<Dropdown
|
||||
menu={{ items: menuItems }}
|
||||
trigger={['contextMenu']}
|
||||
popupRender={(menu) => <div onPointerDown={(e) => e.stopPropagation()}>{menu}</div>}>
|
||||
<Container onClick={handlePress} isActive={isActive}>
|
||||
<AssistantNameRow className="name" title={agent.name ?? agent.id}>
|
||||
<AgentNameWrapper>
|
||||
<AgentLabel agent={agent} />
|
||||
</AgentNameWrapper>
|
||||
{isActive && (
|
||||
<MenuButton>
|
||||
<SessionCount>{sessions.length}</SessionCount>
|
||||
</MenuButton>
|
||||
)}
|
||||
{!isActive && <BotIcon />}
|
||||
</AssistantNameRow>
|
||||
</Container>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
||||
@ -118,7 +126,7 @@ export const MenuButton: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ cla
|
||||
export const BotIcon: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ ...props }) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<Tooltip content={t('common.agent_one')} delay={500} closeDelay={0}>
|
||||
<Tooltip title={t('common.agent_one')} mouseEnterDelay={0.5}>
|
||||
<MenuButton {...props}>
|
||||
<Bot size={14} className="text-primary" />
|
||||
</MenuButton>
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { cn } from '@heroui/react'
|
||||
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
|
||||
import EmojiIcon from '@renderer/components/EmojiIcon'
|
||||
import { CopyIcon, DeleteIcon, EditIcon } from '@renderer/components/Icons'
|
||||
@ -12,7 +11,7 @@ import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setActiveTopicOrSessionAction } from '@renderer/store/runtime'
|
||||
import type { Assistant, AssistantsSortType } from '@renderer/types'
|
||||
import { getLeadingEmoji, uuid } from '@renderer/utils'
|
||||
import { cn, getLeadingEmoji, uuid } from '@renderer/utils'
|
||||
import { hasTopicPendingRequests } from '@renderer/utils/queue'
|
||||
import type { MenuProps } from 'antd'
|
||||
import { Dropdown } from 'antd'
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { cn } from '@heroui/react'
|
||||
import { DeleteIcon, EditIcon } from '@renderer/components/Icons'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
|
||||
@ -8,26 +7,19 @@ import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { SessionSettingsPopup } from '@renderer/pages/settings/AgentSettings'
|
||||
import { SessionLabel } from '@renderer/pages/settings/AgentSettings/shared'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import store, { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { newMessagesActions } from '@renderer/store/newMessage'
|
||||
import type { AgentSessionEntity } from '@renderer/types'
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuSub,
|
||||
ContextMenuSubContent,
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuTrigger
|
||||
} from '@renderer/ui/context-menu'
|
||||
import { loadTopicMessagesThunk, renameAgentSessionIfNeeded } from '@renderer/store/thunk/messageThunk'
|
||||
import type { AgentSessionEntity, Assistant } from '@renderer/types'
|
||||
import { classNames } from '@renderer/utils'
|
||||
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
|
||||
import { Tooltip } from 'antd'
|
||||
import { MenuIcon, XIcon } from 'lucide-react'
|
||||
import type { MenuProps } from 'antd'
|
||||
import { Dropdown, Tooltip } from 'antd'
|
||||
import { MenuIcon, Sparkles, XIcon } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import React, { memo, startTransition, useEffect, useMemo, useState } from 'react'
|
||||
import React, { memo, startTransition, useDeferredValue, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { ListItem, ListItemEditInput, ListItemName, ListItemNameContainer, MenuButton, StatusIndicator } from './shared'
|
||||
import styled from 'styled-components'
|
||||
|
||||
// const logger = loggerService.withContext('AgentItem')
|
||||
|
||||
@ -46,6 +38,8 @@ const SessionItem: FC<SessionItemProps> = ({ session, agentId, onDelete, onPress
|
||||
const activeSessionId = chat.activeSessionIdMap[agentId]
|
||||
const [isConfirmingDeletion, setIsConfirmingDeletion] = useState(false)
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
const [_targetSession, setTargetSession] = useState<AgentSessionEntity>(session)
|
||||
const targetSession = useDeferredValue(_targetSession)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const { isEditing, isSaving, editValue, inputRef, startEdit, handleKeyDown, handleValueChange } = useInPlaceEdit({
|
||||
@ -68,6 +62,7 @@ const SessionItem: FC<SessionItemProps> = ({ session, agentId, onDelete, onPress
|
||||
</div>
|
||||
}>
|
||||
<MenuButton
|
||||
className="menu"
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (isConfirmingDeletion || e.ctrlKey || e.metaKey) {
|
||||
@ -111,82 +106,216 @@ const SessionItem: FC<SessionItemProps> = ({ session, agentId, onDelete, onPress
|
||||
const { topicPosition, setTopicPosition } = useSettings()
|
||||
const singlealone = topicPosition === 'right'
|
||||
|
||||
const menuItems: MenuProps['items'] = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: t('common.edit'),
|
||||
key: 'edit',
|
||||
icon: <EditIcon size={14} />,
|
||||
onClick: () => {
|
||||
SessionSettingsPopup.show({
|
||||
agentId,
|
||||
sessionId: session.id
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('chat.topics.auto_rename'),
|
||||
key: 'auto-rename',
|
||||
icon: <Sparkles size={14} />,
|
||||
onClick: () => {
|
||||
const assistant = {} as Assistant
|
||||
const agentSession = { agentId: agentId, sessionId: targetSession.id }
|
||||
dispatch(loadTopicMessagesThunk(sessionTopicId))
|
||||
renameAgentSessionIfNeeded(agentSession, assistant, sessionTopicId, store.getState)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('settings.topic.position.label'),
|
||||
key: 'topic-position',
|
||||
icon: <MenuIcon size={14} />,
|
||||
children: [
|
||||
{
|
||||
label: t('settings.topic.position.left'),
|
||||
key: 'left',
|
||||
onClick: () => setTopicPosition('left')
|
||||
},
|
||||
{
|
||||
label: t('settings.topic.position.right'),
|
||||
key: 'right',
|
||||
onClick: () => setTopicPosition('right')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: t('common.delete'),
|
||||
key: 'delete',
|
||||
icon: <DeleteIcon size={14} className="lucide-custom" />,
|
||||
danger: true,
|
||||
onClick: () => {
|
||||
onDelete()
|
||||
}
|
||||
}
|
||||
],
|
||||
[agentId, dispatch, onDelete, session.id, sessionTopicId, setTopicPosition, t, targetSession.id]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContextMenu modal={false}>
|
||||
<ContextMenuTrigger>
|
||||
<ListItem
|
||||
className={cn(
|
||||
isActive ? 'active' : undefined,
|
||||
singlealone ? 'singlealone' : undefined,
|
||||
isEditing ? 'cursor-default' : 'cursor-pointer',
|
||||
'rounded-[var(--list-item-border-radius)]'
|
||||
)}
|
||||
onClick={isEditing ? undefined : onPress}
|
||||
onDoubleClick={() => startEdit(session.name ?? '')}
|
||||
title={session.name ?? session.id}>
|
||||
{isPending && !isActive && <StatusIndicator variant="pending" />}
|
||||
{isFulfilled && !isActive && <StatusIndicator variant="fulfilled" />}
|
||||
<ListItemNameContainer>
|
||||
{isEditing ? (
|
||||
<ListItemEditInput
|
||||
ref={inputRef}
|
||||
value={editValue}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleValueChange(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onClick={(e: React.MouseEvent) => e.stopPropagation()}
|
||||
style={{ opacity: isSaving ? 0.5 : 1 }}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<ListItemName>
|
||||
<SessionLabel session={session} />
|
||||
</ListItemName>
|
||||
<DeleteButton />
|
||||
</>
|
||||
)}
|
||||
</ListItemNameContainer>
|
||||
</ListItem>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem
|
||||
key="edit"
|
||||
onClick={() => {
|
||||
SessionSettingsPopup.show({
|
||||
agentId,
|
||||
sessionId: session.id
|
||||
})
|
||||
}}>
|
||||
<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"
|
||||
onClick={() => {
|
||||
onDelete()
|
||||
}}>
|
||||
<DeleteIcon size={14} className="lucide-custom text-danger" />
|
||||
<span className="text-danger">{t('common.delete')}</span>
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
</>
|
||||
<Dropdown
|
||||
menu={{ items: menuItems }}
|
||||
trigger={['contextMenu']}
|
||||
popupRender={(menu) => <div onPointerDown={(e) => e.stopPropagation()}>{menu}</div>}>
|
||||
<SessionListItem
|
||||
className={classNames(isActive ? 'active' : '', singlealone ? 'singlealone' : '')}
|
||||
onClick={isEditing ? undefined : onPress}
|
||||
onDoubleClick={() => startEdit(session.name ?? '')}
|
||||
title={session.name ?? session.id}
|
||||
onContextMenu={() => setTargetSession(session)}
|
||||
style={{
|
||||
borderRadius: 'var(--list-item-border-radius)',
|
||||
cursor: isEditing ? 'default' : 'pointer'
|
||||
}}>
|
||||
{isPending && !isActive && <PendingIndicator />}
|
||||
{isFulfilled && !isActive && <FulfilledIndicator />}
|
||||
<SessionNameContainer>
|
||||
{isEditing ? (
|
||||
<SessionEditInput
|
||||
ref={inputRef}
|
||||
value={editValue}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleValueChange(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onClick={(e: React.MouseEvent) => e.stopPropagation()}
|
||||
style={{ opacity: isSaving ? 0.5 : 1 }}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<SessionName>
|
||||
<SessionLabel session={session} />
|
||||
</SessionName>
|
||||
<DeleteButton />
|
||||
</>
|
||||
)}
|
||||
</SessionNameContainer>
|
||||
</SessionListItem>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
||||
const SessionListItem = styled.div`
|
||||
padding: 7px 12px;
|
||||
border-radius: var(--list-item-border-radius);
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
width: calc(var(--assistants-width) - 20px);
|
||||
margin-bottom: 8px;
|
||||
|
||||
.menu {
|
||||
opacity: 0;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-list-item-hover);
|
||||
transition: background-color 0.1s;
|
||||
|
||||
.menu {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--color-list-item);
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
.menu {
|
||||
opacity: 1;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.singlealone {
|
||||
border-radius: 0 !important;
|
||||
&:hover {
|
||||
background-color: var(--color-background-soft);
|
||||
}
|
||||
&.active {
|
||||
border-left: 2px solid var(--color-primary);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const SessionNameContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
height: 20px;
|
||||
justify-content: space-between;
|
||||
`
|
||||
|
||||
const SessionName = styled.div`
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
position: relative;
|
||||
`
|
||||
|
||||
const SessionEditInput = styled.input`
|
||||
background: var(--color-background);
|
||||
border: none;
|
||||
color: var(--color-text-1);
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
padding: 2px 6px;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
`
|
||||
|
||||
const MenuButton = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: 20px;
|
||||
min-height: 20px;
|
||||
.anticon {
|
||||
font-size: 12px;
|
||||
}
|
||||
`
|
||||
|
||||
const PendingIndicator = styled.div.attrs({
|
||||
className: 'animation-pulse'
|
||||
})`
|
||||
--pulse-size: 5px;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
position: absolute;
|
||||
left: 3px;
|
||||
top: 15px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-status-warning);
|
||||
`
|
||||
|
||||
const FulfilledIndicator = styled.div.attrs({
|
||||
className: 'animation-pulse'
|
||||
})`
|
||||
--pulse-size: 5px;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
position: absolute;
|
||||
left: 3px;
|
||||
top: 15px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-status-success);
|
||||
`
|
||||
|
||||
export default memo(SessionItem)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Alert, Spinner } from '@heroui/react'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { DynamicVirtualList } from '@renderer/components/VirtualList'
|
||||
import { useCreateDefaultSession } from '@renderer/hooks/agents/useCreateDefaultSession'
|
||||
import { useSessions } from '@renderer/hooks/agents/useSessions'
|
||||
@ -11,13 +11,14 @@ import {
|
||||
setSessionWaitingAction
|
||||
} from '@renderer/store/runtime'
|
||||
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
|
||||
import { Alert, Spin } from 'antd'
|
||||
import { motion } from 'framer-motion'
|
||||
import { memo, useCallback, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import AddButton from './AddButton'
|
||||
import SessionItem from './SessionItem'
|
||||
import { ListContainer } from './shared'
|
||||
|
||||
// const logger = loggerService.withContext('SessionsTab')
|
||||
|
||||
@ -88,16 +89,18 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="flex h-full items-center justify-center">
|
||||
<Spinner size="lg" />
|
||||
<Spin />
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) return <Alert color="danger" content={t('agent.session.get.error.failed')} />
|
||||
if (error) {
|
||||
return <Alert type="error" message={t('agent.session.get.error.failed')} showIcon style={{ margin: 10 }} />
|
||||
}
|
||||
|
||||
return (
|
||||
<ListContainer className="sessions-tab">
|
||||
<AddButton onPress={createDefaultSession} className="mb-2" isDisabled={creatingSession}>
|
||||
<Container className="sessions-tab">
|
||||
<AddButton onClick={createDefaultSession} disabled={creatingSession} className="-mt-[4px] mb-[6px]">
|
||||
{t('agent.session.add.title')}
|
||||
</AddButton>
|
||||
{/* h-9 */}
|
||||
@ -119,8 +122,15 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
||||
/>
|
||||
)}
|
||||
</DynamicVirtualList>
|
||||
</ListContainer>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled(Scrollbar)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px 10px;
|
||||
overflow-x: hidden;
|
||||
`
|
||||
|
||||
export default memo(Sessions)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { DownOutlined, RightOutlined } from '@ant-design/icons'
|
||||
import { cn } from '@heroui/react'
|
||||
import { cn } from '@renderer/utils'
|
||||
import { Tooltip } from 'antd'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { cn } from '@heroui/react'
|
||||
import { DraggableVirtualList } from '@renderer/components/DraggableList'
|
||||
import { CopyIcon, DeleteIcon, EditIcon } from '@renderer/components/Icons'
|
||||
import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup'
|
||||
@ -18,7 +17,7 @@ import store from '@renderer/store'
|
||||
import { newMessagesActions } from '@renderer/store/newMessage'
|
||||
import { setGenerating } from '@renderer/store/runtime'
|
||||
import type { Assistant, Topic } from '@renderer/types'
|
||||
import { removeSpecialCharactersForFileName } from '@renderer/utils'
|
||||
import { classNames, removeSpecialCharactersForFileName } from '@renderer/utils'
|
||||
import { copyTopicAsMarkdown, copyTopicAsPlainText } from '@renderer/utils/copy'
|
||||
import {
|
||||
exportMarkdownToJoplin,
|
||||
@ -54,15 +53,6 @@ import { useDispatch, useSelector } from 'react-redux'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import AddButton from './AddButton'
|
||||
import {
|
||||
ListContainer,
|
||||
ListItem,
|
||||
ListItemEditInput,
|
||||
ListItemName,
|
||||
ListItemNameContainer,
|
||||
MenuButton,
|
||||
StatusIndicator
|
||||
} from './shared'
|
||||
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
@ -83,6 +73,8 @@ export const Topics: React.FC<Props> = ({ assistant: _assistant, activeTopic, se
|
||||
const topicFulfilledQuery = useSelector((state: RootState) => state.messages.fulfilledByTopic)
|
||||
const newlyRenamedTopics = useSelector((state: RootState) => state.runtime.chat.newlyRenamedTopics)
|
||||
|
||||
const borderRadius = showTopicTime ? 12 : 'var(--list-item-border-radius)'
|
||||
|
||||
const [deletingTopicId, setDeletingTopicId] = useState<string | null>(null)
|
||||
const deleteTimerRef = useRef<NodeJS.Timeout>(null)
|
||||
const [editingTopicId, setEditingTopicId] = useState<string | null>(null)
|
||||
@ -497,107 +489,255 @@ export const Topics: React.FC<Props> = ({ assistant: _assistant, activeTopic, se
|
||||
const singlealone = topicPosition === 'right' && position === 'right'
|
||||
|
||||
return (
|
||||
<ListContainer className="topics-tab">
|
||||
<AddButton onPress={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)} className="mb-2">
|
||||
{t('chat.add.topic.title')}
|
||||
</AddButton>
|
||||
<DraggableVirtualList list={sortedTopics} onUpdate={updateTopics} className="overflow-y-auto overflow-x-hidden">
|
||||
{(topic) => {
|
||||
const isActive = topic.id === activeTopic?.id
|
||||
const topicName = topic.name.replace('`', '')
|
||||
const topicPrompt = topic.prompt
|
||||
const fullTopicPrompt = t('common.prompt') + ': ' + topicPrompt
|
||||
<DraggableVirtualList
|
||||
className="topics-tab"
|
||||
list={sortedTopics}
|
||||
onUpdate={updateTopics}
|
||||
style={{ height: '100%', padding: '11px 0 10px 10px' }}
|
||||
itemContainerStyle={{ paddingBottom: '8px' }}
|
||||
header={
|
||||
<>
|
||||
<AddButton onClick={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)}>
|
||||
{t('chat.add.topic.title')}
|
||||
</AddButton>
|
||||
<div className="my-1"></div>
|
||||
</>
|
||||
}>
|
||||
{(topic) => {
|
||||
const isActive = topic.id === activeTopic?.id
|
||||
const topicName = topic.name.replace('`', '')
|
||||
const topicPrompt = topic.prompt
|
||||
const fullTopicPrompt = t('common.prompt') + ': ' + topicPrompt
|
||||
|
||||
const getTopicNameClassName = () => {
|
||||
if (isRenaming(topic.id)) return 'shimmer'
|
||||
if (isNewlyRenamed(topic.id)) return 'typing'
|
||||
return ''
|
||||
}
|
||||
const getTopicNameClassName = () => {
|
||||
if (isRenaming(topic.id)) return 'shimmer'
|
||||
if (isNewlyRenamed(topic.id)) return 'typing'
|
||||
return ''
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown menu={{ items: getTopicMenuItems }} trigger={['contextMenu']}>
|
||||
<ListItem
|
||||
onContextMenu={() => setTargetTopic(topic)}
|
||||
className={cn(
|
||||
isActive ? 'active' : undefined,
|
||||
singlealone ? 'singlealone' : undefined,
|
||||
editingTopicId === topic.id && topicEdit.isEditing ? 'cursor-default' : 'cursor-pointer',
|
||||
showTopicTime ? 'rounded-2xl' : 'rounded-[var(--list-item-border-radius)]'
|
||||
return (
|
||||
<Dropdown menu={{ items: getTopicMenuItems }} trigger={['contextMenu']}>
|
||||
<TopicListItem
|
||||
onContextMenu={() => setTargetTopic(topic)}
|
||||
className={classNames(isActive ? 'active' : '', singlealone ? 'singlealone' : '')}
|
||||
onClick={editingTopicId === topic.id && topicEdit.isEditing ? undefined : () => onSwitchTopic(topic)}
|
||||
style={{
|
||||
borderRadius,
|
||||
cursor: editingTopicId === topic.id && topicEdit.isEditing ? 'default' : 'pointer'
|
||||
}}>
|
||||
{isPending(topic.id) && !isActive && <PendingIndicator />}
|
||||
{isFulfilled(topic.id) && !isActive && <FulfilledIndicator />}
|
||||
<TopicNameContainer>
|
||||
{editingTopicId === topic.id && topicEdit.isEditing ? (
|
||||
<TopicEditInput
|
||||
ref={topicEdit.inputRef}
|
||||
value={topicEdit.editValue}
|
||||
onChange={topicEdit.handleInputChange}
|
||||
onKeyDown={topicEdit.handleKeyDown}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
) : (
|
||||
<TopicName
|
||||
className={getTopicNameClassName()}
|
||||
title={topicName}
|
||||
onDoubleClick={() => {
|
||||
setEditingTopicId(topic.id)
|
||||
topicEdit.startEdit(topic.name)
|
||||
}}>
|
||||
{topicName}
|
||||
</TopicName>
|
||||
)}
|
||||
onClick={editingTopicId === topic.id && topicEdit.isEditing ? undefined : () => onSwitchTopic(topic)}>
|
||||
{isPending(topic.id) && !isActive && <StatusIndicator variant="pending" />}
|
||||
{isFulfilled(topic.id) && !isActive && <StatusIndicator variant="fulfilled" />}
|
||||
<ListItemNameContainer>
|
||||
{editingTopicId === topic.id && topicEdit.isEditing ? (
|
||||
<ListItemEditInput
|
||||
ref={topicEdit.inputRef}
|
||||
value={topicEdit.editValue}
|
||||
onChange={topicEdit.handleInputChange}
|
||||
onKeyDown={topicEdit.handleKeyDown}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
) : (
|
||||
<ListItemName
|
||||
className={getTopicNameClassName()}
|
||||
title={topicName}
|
||||
onDoubleClick={() => {
|
||||
setEditingTopicId(topic.id)
|
||||
topicEdit.startEdit(topic.name)
|
||||
{!topic.pinned && (
|
||||
<Tooltip
|
||||
placement="bottom"
|
||||
mouseEnterDelay={0.7}
|
||||
mouseLeaveDelay={0}
|
||||
title={
|
||||
<div style={{ fontSize: '12px', opacity: 0.8, fontStyle: 'italic' }}>
|
||||
{t('chat.topics.delete.shortcut', { key: isMac ? '⌘' : 'Ctrl' })}
|
||||
</div>
|
||||
}>
|
||||
<MenuButton
|
||||
className="menu"
|
||||
onClick={(e) => {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
handleConfirmDelete(topic, e)
|
||||
} else if (deletingTopicId === topic.id) {
|
||||
handleConfirmDelete(topic, e)
|
||||
} else {
|
||||
handleDeleteClick(topic.id, e)
|
||||
}
|
||||
}}>
|
||||
{topicName}
|
||||
</ListItemName>
|
||||
)}
|
||||
{!topic.pinned && (
|
||||
<Tooltip
|
||||
placement="bottom"
|
||||
mouseEnterDelay={0.7}
|
||||
mouseLeaveDelay={0}
|
||||
title={
|
||||
<div style={{ fontSize: '12px', opacity: 0.8, fontStyle: 'italic' }}>
|
||||
{t('chat.topics.delete.shortcut', { key: isMac ? '⌘' : 'Ctrl' })}
|
||||
</div>
|
||||
}>
|
||||
<MenuButton
|
||||
onClick={(e) => {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
handleConfirmDelete(topic, e)
|
||||
} else if (deletingTopicId === topic.id) {
|
||||
handleConfirmDelete(topic, e)
|
||||
} else {
|
||||
handleDeleteClick(topic.id, e)
|
||||
}
|
||||
}}>
|
||||
{deletingTopicId === topic.id ? (
|
||||
<DeleteIcon size={14} color="var(--color-error)" style={{ pointerEvents: 'none' }} />
|
||||
) : (
|
||||
<XIcon size={14} color="var(--color-text-3)" style={{ pointerEvents: 'none' }} />
|
||||
)}
|
||||
</MenuButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{topic.pinned && (
|
||||
<MenuButton className="pin">
|
||||
<PinIcon size={14} color="var(--color-text-3)" />
|
||||
{deletingTopicId === topic.id ? (
|
||||
<DeleteIcon size={14} color="var(--color-error)" style={{ pointerEvents: 'none' }} />
|
||||
) : (
|
||||
<XIcon size={14} color="var(--color-text-3)" style={{ pointerEvents: 'none' }} />
|
||||
)}
|
||||
</MenuButton>
|
||||
)}
|
||||
</ListItemNameContainer>
|
||||
{topicPrompt && (
|
||||
<TopicPromptText className="prompt" title={fullTopicPrompt}>
|
||||
{fullTopicPrompt}
|
||||
</TopicPromptText>
|
||||
</Tooltip>
|
||||
)}
|
||||
{showTopicTime && (
|
||||
<TopicTime className="time">{dayjs(topic.createdAt).format('MM/DD HH:mm')}</TopicTime>
|
||||
{topic.pinned && (
|
||||
<MenuButton className="pin">
|
||||
<PinIcon size={14} color="var(--color-text-3)" />
|
||||
</MenuButton>
|
||||
)}
|
||||
</ListItem>
|
||||
</Dropdown>
|
||||
)
|
||||
}}
|
||||
</DraggableVirtualList>
|
||||
</ListContainer>
|
||||
</TopicNameContainer>
|
||||
{topicPrompt && (
|
||||
<TopicPromptText className="prompt" title={fullTopicPrompt}>
|
||||
{fullTopicPrompt}
|
||||
</TopicPromptText>
|
||||
)}
|
||||
{showTopicTime && <TopicTime className="time">{dayjs(topic.createdAt).format('MM/DD HH:mm')}</TopicTime>}
|
||||
</TopicListItem>
|
||||
</Dropdown>
|
||||
)
|
||||
}}
|
||||
</DraggableVirtualList>
|
||||
)
|
||||
}
|
||||
|
||||
const TopicListItem = styled.div`
|
||||
padding: 7px 12px;
|
||||
border-radius: var(--list-item-border-radius);
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
width: calc(var(--assistants-width) - 20px);
|
||||
|
||||
.menu {
|
||||
opacity: 0;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-list-item-hover);
|
||||
transition: background-color 0.1s;
|
||||
|
||||
.menu {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--color-list-item);
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
.menu {
|
||||
opacity: 1;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
}
|
||||
}
|
||||
&.singlealone {
|
||||
border-radius: 0 !important;
|
||||
&:hover {
|
||||
background-color: var(--color-background-soft);
|
||||
}
|
||||
&.active {
|
||||
border-left: 2px solid var(--color-primary);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const TopicNameContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
height: 20px;
|
||||
justify-content: space-between;
|
||||
`
|
||||
|
||||
const TopicName = styled.div`
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
position: relative;
|
||||
will-change: background-position, width;
|
||||
|
||||
--color-shimmer-mid: var(--color-text-1);
|
||||
--color-shimmer-end: color-mix(in srgb, var(--color-text-1) 25%, transparent);
|
||||
|
||||
&.shimmer {
|
||||
background: linear-gradient(to left, var(--color-shimmer-end), var(--color-shimmer-mid), var(--color-shimmer-end));
|
||||
background-size: 200% 100%;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
animation: shimmer 3s linear infinite;
|
||||
}
|
||||
|
||||
&.typing {
|
||||
display: block;
|
||||
-webkit-line-clamp: unset;
|
||||
-webkit-box-orient: unset;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
animation: typewriter 0.5s steps(40, end);
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes typewriter {
|
||||
from {
|
||||
width: 0;
|
||||
}
|
||||
to {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const TopicEditInput = styled.input`
|
||||
background: var(--color-background);
|
||||
border: none;
|
||||
color: var(--color-text-1);
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
padding: 2px 6px;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
`
|
||||
|
||||
const PendingIndicator = styled.div.attrs({
|
||||
className: 'animation-pulse'
|
||||
})`
|
||||
--pulse-size: 5px;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
position: absolute;
|
||||
left: 3px;
|
||||
top: 15px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-status-warning);
|
||||
`
|
||||
|
||||
const FulfilledIndicator = styled.div.attrs({
|
||||
className: 'animation-pulse'
|
||||
})`
|
||||
--pulse-size: 5px;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
position: absolute;
|
||||
left: 3px;
|
||||
top: 15px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-status-success);
|
||||
`
|
||||
|
||||
const TopicPromptText = styled.div`
|
||||
color: var(--color-text-2);
|
||||
font-size: 12px;
|
||||
@ -614,3 +754,15 @@ const TopicTime = styled.div`
|
||||
color: var(--color-text-3);
|
||||
font-size: 11px;
|
||||
`
|
||||
|
||||
const MenuButton = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: 20px;
|
||||
min-height: 20px;
|
||||
.anticon {
|
||||
font-size: 12px;
|
||||
}
|
||||
`
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useDisclosure } from '@heroui/react'
|
||||
import AddAssistantOrAgentPopup from '@renderer/components/Popups/AddAssistantOrAgentPopup'
|
||||
import { AgentModal } from '@renderer/components/Popups/agent/AgentModal'
|
||||
import AgentModalPopup from '@renderer/components/Popups/agent/AgentModal'
|
||||
import { useApiServer } from '@renderer/hooks/useApiServer'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setActiveTopicOrSessionAction } from '@renderer/store/runtime'
|
||||
import type { AgentEntity, Assistant, Topic } from '@renderer/types'
|
||||
@ -18,20 +18,8 @@ interface UnifiedAddButtonProps {
|
||||
|
||||
const UnifiedAddButton: FC<UnifiedAddButtonProps> = ({ onCreateAssistant, setActiveAssistant, setActiveAgentId }) => {
|
||||
const { t } = useTranslation()
|
||||
const { isOpen: isAgentModalOpen, onOpen: onAgentModalOpen, onClose: onAgentModalClose } = useDisclosure()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const handleAddButtonClick = () => {
|
||||
AddAssistantOrAgentPopup.show({
|
||||
onSelect: (type) => {
|
||||
if (type === 'assistant') {
|
||||
onCreateAssistant()
|
||||
} else if (type === 'agent') {
|
||||
onAgentModalOpen()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
const { apiServerRunning, startApiServer } = useApiServer()
|
||||
|
||||
const afterCreate = useCallback(
|
||||
(a: AgentEntity) => {
|
||||
@ -58,12 +46,24 @@ const UnifiedAddButton: FC<UnifiedAddButtonProps> = ({ onCreateAssistant, setAct
|
||||
[dispatch, setActiveAgentId, setActiveAssistant]
|
||||
)
|
||||
|
||||
const handleAddButtonClick = async () => {
|
||||
AddAssistantOrAgentPopup.show({
|
||||
onSelect: (type) => {
|
||||
if (type === 'assistant') {
|
||||
onCreateAssistant()
|
||||
}
|
||||
|
||||
if (type === 'agent') {
|
||||
!apiServerRunning && startApiServer()
|
||||
AgentModalPopup.show({ afterSubmit: afterCreate })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-1">
|
||||
<AddButton onPress={handleAddButtonClick} className="-mt-[1px] mb-[2px]">
|
||||
{t('chat.add.assistant.title')}
|
||||
</AddButton>
|
||||
<AgentModal isOpen={isAgentModalOpen} onClose={onAgentModalClose} afterSubmit={afterCreate} />
|
||||
<div className="-mt-[4px] mb-[6px]">
|
||||
<AddButton onClick={handleAddButtonClick}>{t('chat.add.assistant.title')}</AddButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,131 +0,0 @@
|
||||
import { cn } from '@heroui/react'
|
||||
import type { ComponentPropsWithoutRef, ComponentPropsWithRef } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const ListItem = ({ children, className, ...props }: ComponentPropsWithoutRef<'div'>) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'mb-2 flex w-[calc(var(--assistants-width)-20px)] cursor-pointer flex-col justify-between rounded-lg px-3 py-2 text-sm',
|
||||
'transition-colors duration-100',
|
||||
'hover:bg-[var(--color-list-item-hover)]',
|
||||
'[.active]:bg-[var(--color-list-item)] [.active]:shadow-[0_1px_2px_0_rgba(0,0,0,0.05)]',
|
||||
'[&_.menu]:text-[var(--color-text-3)] [&_.menu]:opacity-0',
|
||||
'hover:[&_.menu]:opacity-100',
|
||||
'[.active]:[&_.menu]:opacity-100 [.active]:[&_.menu]:hover:text-[var(--color-text-2)]',
|
||||
'[.singlealone.active]:border-[var(--color-primary)] [.singlealone.active]:shadow-none [.singlealone]:rounded-none [.singlealone]:border-transparent [.singlealone]:border-l-2 [.singlealone]:hover:bg-[var(--color-background-soft)]',
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export const ListItemNameContainer = ({ children, className, ...props }: ComponentPropsWithoutRef<'div'>) => {
|
||||
return (
|
||||
<div className={cn('flex h-5 flex-row items-center justify-between gap-1', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// This component involves complex animations and will not be migrated for now.
|
||||
export const ListItemName = styled.div`
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
will-change: background-position, width;
|
||||
|
||||
--color-shimmer-mid: var(--color-text-1);
|
||||
--color-shimmer-end: color-mix(in srgb, var(--color-text-1) 25%, transparent);
|
||||
|
||||
&.shimmer {
|
||||
background: linear-gradient(to left, var(--color-shimmer-end), var(--color-shimmer-mid), var(--color-shimmer-end));
|
||||
background-size: 200% 100%;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
animation: shimmer 3s linear infinite;
|
||||
}
|
||||
|
||||
&.typing {
|
||||
display: block;
|
||||
-webkit-line-clamp: unset;
|
||||
-webkit-box-orient: unset;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
animation: typewriter 0.5s steps(40, end);
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes typewriter {
|
||||
from {
|
||||
width: 0;
|
||||
}
|
||||
to {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const ListItemEditInput = ({ className, ...props }: ComponentPropsWithRef<'input'>) => {
|
||||
return (
|
||||
<input
|
||||
className={cn(
|
||||
'w-full border-none bg-[var(--color-background)] p-0 font-inherit text-[var(--color-text-1)] text-sm outline-none',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const ListContainer = ({ children, className, ...props }: ComponentPropsWithoutRef<'div'>) => {
|
||||
return (
|
||||
<div className={cn('flex h-full w-full flex-col p-2', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const MenuButton = ({ children, className, ...props }: ComponentPropsWithoutRef<'div'>) => {
|
||||
return (
|
||||
<div className={cn('menu', 'flex min-h-5 min-w-5 flex-row items-center justify-center', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const StatusIndicator = ({ variant }: { variant: 'pending' | 'fulfilled' }) => {
|
||||
const colors = useMemo(() => {
|
||||
switch (variant) {
|
||||
case 'pending':
|
||||
return {
|
||||
wave: 'bg-warning-400',
|
||||
back: 'bg-warning-500'
|
||||
}
|
||||
case 'fulfilled':
|
||||
return {
|
||||
wave: 'bg-success-400',
|
||||
back: 'bg-success-500'
|
||||
}
|
||||
}
|
||||
}, [variant])
|
||||
return (
|
||||
<div className="absolute top-4 left-1 flex size-1">
|
||||
<span className={cn('absolute inline-flex h-full w-full animate-ping rounded-full opacity-75', colors.wave)} />
|
||||
<span className={cn('relative inline-flex size-1 rounded-full bg-warning-500', colors.back)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,4 +1,3 @@
|
||||
import { Alert, Skeleton } from '@heroui/react'
|
||||
import AddAssistantPopup from '@renderer/components/Popups/AddAssistantPopup'
|
||||
import { useActiveSession } from '@renderer/hooks/agents/useActiveSession'
|
||||
import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
|
||||
@ -12,6 +11,7 @@ import { setActiveAgentId, setActiveTopicOrSessionAction } from '@renderer/store
|
||||
import type { Assistant, Topic } from '@renderer/types'
|
||||
import type { Tab } from '@renderer/types/chat'
|
||||
import { classNames, getErrorMessage, uuid } from '@renderer/utils'
|
||||
import { Alert, Skeleton } from 'antd'
|
||||
import type { FC } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -166,16 +166,17 @@ const HomeTabs: FC<Props> = ({
|
||||
)}
|
||||
{tab === 'settings' && isTopicView && <Settings assistant={activeAssistant} />}
|
||||
{tab === 'settings' && isSessionView && !sessionError && (
|
||||
<Skeleton isLoaded={!isSessionLoading} className="h-full">
|
||||
<Skeleton loading={isSessionLoading} active style={{ height: '100%', padding: '16px' }}>
|
||||
<SessionSettingsTab session={session} update={updateSession} />
|
||||
</Skeleton>
|
||||
)}
|
||||
{tab === 'settings' && isSessionView && sessionError && (
|
||||
<div className="w-[var(--assistants-width)] p-2 px-3 pt-4">
|
||||
<Alert
|
||||
color="danger"
|
||||
title={t('agent.session.get.error.failed')}
|
||||
type="error"
|
||||
message={t('agent.session.get.error.failed')}
|
||||
description={getErrorMessage(sessionError)}
|
||||
style={{ padding: '10px 15px' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { BreadcrumbItem, Breadcrumbs, cn } from '@heroui/react'
|
||||
import HorizontalScrollContainer from '@renderer/components/HorizontalScrollContainer'
|
||||
import { useActiveAgent } from '@renderer/hooks/agents/useActiveAgent'
|
||||
import { useActiveSession } from '@renderer/hooks/agents/useActiveSession'
|
||||
@ -7,15 +6,18 @@ import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import type { AgentEntity, AgentSessionEntity, ApiModel, Assistant } from '@renderer/types'
|
||||
import { formatErrorMessageWithPrefix } from '@renderer/utils/error'
|
||||
import { t } from 'i18next'
|
||||
import { Folder } from 'lucide-react'
|
||||
import { ChevronRight, Folder } from 'lucide-react'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
import { AgentSettingsPopup, SessionSettingsPopup } from '../../settings/AgentSettings'
|
||||
import { AgentLabel, SessionLabel } from '../../settings/AgentSettings/shared'
|
||||
import SelectAgentBaseModelButton from './SelectAgentBaseModelButton'
|
||||
import SelectModelButton from './SelectModelButton'
|
||||
|
||||
const cn = (...inputs: any[]) => twMerge(inputs)
|
||||
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
}
|
||||
@ -40,43 +42,53 @@ const ChatNavbarContent: FC<Props> = ({ assistant }) => {
|
||||
{activeTopicOrSession === 'topic' && <SelectModelButton assistant={assistant} />}
|
||||
{activeTopicOrSession === 'session' && activeAgent && (
|
||||
<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' }}>
|
||||
<div className="flex flex-nowrap items-center gap-2">
|
||||
{/* Agent Label */}
|
||||
<div
|
||||
className="flex h-full cursor-pointer items-center"
|
||||
onClick={() => AgentSettingsPopup.show({ agentId: activeAgent.id })}>
|
||||
<AgentLabel
|
||||
agent={activeAgent}
|
||||
classNames={{ name: 'max-w-40 text-xs', avatar: 'h-4.5 w-4.5', container: 'gap-1.5' }}
|
||||
/>
|
||||
</BreadcrumbItem>
|
||||
</div>
|
||||
|
||||
{activeSession && (
|
||||
<BreadcrumbItem
|
||||
onPress={() =>
|
||||
SessionSettingsPopup.show({
|
||||
agentId: activeAgent.id,
|
||||
sessionId: activeSession.id
|
||||
})
|
||||
}
|
||||
classNames={{ base: 'self-stretch', item: 'h-full' }}>
|
||||
<SessionLabel session={activeSession} className="max-w-40 text-xs" />
|
||||
</BreadcrumbItem>
|
||||
)}
|
||||
{activeSession && (
|
||||
<BreadcrumbItem>
|
||||
<>
|
||||
{/* Separator */}
|
||||
<ChevronRight className="h-4 w-4 text-gray-400" />
|
||||
|
||||
{/* Session Label */}
|
||||
<div
|
||||
className="flex h-full cursor-pointer items-center"
|
||||
onClick={() =>
|
||||
SessionSettingsPopup.show({
|
||||
agentId: activeAgent.id,
|
||||
sessionId: activeSession.id
|
||||
})
|
||||
}>
|
||||
<SessionLabel session={activeSession} className="max-w-40 text-xs" />
|
||||
</div>
|
||||
|
||||
{/* Separator */}
|
||||
<ChevronRight className="h-4 w-4 text-gray-400" />
|
||||
|
||||
{/* Model Button */}
|
||||
<SelectAgentBaseModelButton
|
||||
agentBase={activeSession}
|
||||
onSelect={async (model) => {
|
||||
await handleUpdateModel(model)
|
||||
}}
|
||||
/>
|
||||
</BreadcrumbItem>
|
||||
)}
|
||||
{activeAgent && activeSession && (
|
||||
<BreadcrumbItem>
|
||||
|
||||
{/* Separator */}
|
||||
<ChevronRight className="h-4 w-4 text-gray-400" />
|
||||
|
||||
{/* Workspace Meta */}
|
||||
<SessionWorkspaceMeta agent={activeAgent} session={activeSession} />
|
||||
</BreadcrumbItem>
|
||||
</>
|
||||
)}
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
</HorizontalScrollContainer>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -1,28 +1,59 @@
|
||||
import { Button } from '@heroui/react'
|
||||
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
|
||||
import { SelectApiModelPopup } from '@renderer/components/Popups/SelectModelPopup'
|
||||
import { agentModelFilter } from '@renderer/config/models'
|
||||
import { useApiModel } from '@renderer/hooks/agents/useModel'
|
||||
import { getProviderNameById } from '@renderer/services/ProviderService'
|
||||
import type { AgentBaseWithId, ApiModel } from '@renderer/types'
|
||||
import { isAgentSessionEntity } from '@renderer/types'
|
||||
import { isAgentEntity } from '@renderer/types'
|
||||
import { getModelFilterByAgentType } from '@renderer/utils/agentSession'
|
||||
import { apiModelAdapter } from '@renderer/utils/model'
|
||||
import type { ButtonProps } from 'antd'
|
||||
import { Button } from 'antd'
|
||||
import { ChevronsUpDown } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import type { CSSProperties, FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
agentBase: AgentBaseWithId
|
||||
onSelect: (model: ApiModel) => Promise<void>
|
||||
isDisabled?: boolean
|
||||
/** Custom className for the button */
|
||||
className?: string
|
||||
/** Custom inline styles for the button (merged with default styles) */
|
||||
buttonStyle?: CSSProperties
|
||||
/** Custom button size */
|
||||
buttonSize?: ButtonProps['size']
|
||||
/** Custom avatar size */
|
||||
avatarSize?: number
|
||||
/** Custom font size */
|
||||
fontSize?: number
|
||||
/** Custom icon size */
|
||||
iconSize?: number
|
||||
/** Custom className for the inner container (e.g., for justify-between) */
|
||||
containerClassName?: string
|
||||
}
|
||||
|
||||
const SelectAgentBaseModelButton: FC<Props> = ({ agentBase: agent, onSelect, isDisabled }) => {
|
||||
const SelectAgentBaseModelButton: FC<Props> = ({
|
||||
agentBase: agent,
|
||||
onSelect,
|
||||
isDisabled,
|
||||
className,
|
||||
buttonStyle,
|
||||
buttonSize = 'small',
|
||||
avatarSize = 20,
|
||||
fontSize = 12,
|
||||
iconSize = 14,
|
||||
containerClassName
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const model = useApiModel({ id: agent?.model })
|
||||
|
||||
const apiFilter = isAgentEntity(agent) ? getModelFilterByAgentType(agent.type) : undefined
|
||||
const apiFilter = isAgentEntity(agent)
|
||||
? getModelFilterByAgentType(agent.type)
|
||||
: isAgentSessionEntity(agent)
|
||||
? getModelFilterByAgentType(agent.agent_type)
|
||||
: undefined
|
||||
|
||||
if (!agent) return null
|
||||
|
||||
@ -35,20 +66,31 @@ const SelectAgentBaseModelButton: FC<Props> = ({ agentBase: agent, onSelect, isD
|
||||
|
||||
const providerName = model?.provider ? getProviderNameById(model.provider) : model?.provider_name
|
||||
|
||||
// Merge default styles with custom styles
|
||||
const mergedStyle: CSSProperties = {
|
||||
borderRadius: 20,
|
||||
fontSize,
|
||||
padding: 2,
|
||||
...buttonStyle
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="light"
|
||||
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 text-[var(--color-text)]">
|
||||
{model ? model.name : t('button.select_model')} {providerName ? ' | ' + providerName : ''}
|
||||
</span>
|
||||
size={buttonSize}
|
||||
type="text"
|
||||
className={className}
|
||||
style={mergedStyle}
|
||||
onClick={onSelectModel}
|
||||
disabled={isDisabled}>
|
||||
<div className={containerClassName || 'flex w-full items-center gap-1.5'}>
|
||||
<div className="flex flex-1 items-center gap-1.5 overflow-x-hidden">
|
||||
<ModelAvatar model={model ? apiModelAdapter(model) : undefined} size={avatarSize} />
|
||||
<span className="truncate text-[var(--color-text)]">
|
||||
{model ? model.name : t('button.select_model')} {providerName ? ' | ' + providerName : ''}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronsUpDown size={iconSize} color="var(--color-icon)" />
|
||||
</div>
|
||||
<ChevronsUpDown size={14} color="var(--color-icon)" />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { SyncOutlined } from '@ant-design/icons'
|
||||
import { useDisclosure } from '@heroui/react'
|
||||
import UpdateDialog from '@renderer/components/UpdateDialog'
|
||||
import UpdateDialogPopup from '@renderer/components/Popups/UpdateDialogPopup'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { Button } from 'antd'
|
||||
@ -12,7 +11,6 @@ const UpdateAppButton: FC = () => {
|
||||
const { update } = useRuntime()
|
||||
const { autoCheckUpdate } = useSettings()
|
||||
const { t } = useTranslation()
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
|
||||
if (!update) {
|
||||
return null
|
||||
@ -22,19 +20,21 @@ const UpdateAppButton: FC = () => {
|
||||
return null
|
||||
}
|
||||
|
||||
const handleOpenUpdateDialog = () => {
|
||||
UpdateDialogPopup.show({ releaseInfo: update.info || null })
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<UpdateButton
|
||||
className="nodrag"
|
||||
onClick={onOpen}
|
||||
onClick={handleOpenUpdateDialog}
|
||||
icon={<SyncOutlined />}
|
||||
color="orange"
|
||||
variant="outlined"
|
||||
size="small">
|
||||
{t('button.update_available')}
|
||||
</UpdateButton>
|
||||
|
||||
<UpdateDialog isOpen={isOpen} onClose={onClose} releaseInfo={update.info || null} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Button, Input } from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import type { InputRef } from 'antd'
|
||||
import { Button, Input } from 'antd'
|
||||
import type { WebviewTag } from 'electron'
|
||||
import { ChevronDown, ChevronUp, X } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
@ -22,7 +23,7 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
|
||||
const [query, setQuery] = useState('')
|
||||
const [matchCount, setMatchCount] = useState(0)
|
||||
const [activeIndex, setActiveIndex] = useState(0)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const inputRef = useRef<InputRef>(null)
|
||||
const focusFrameRef = useRef<number | null>(null)
|
||||
const lastAppIdRef = useRef<string>(appId)
|
||||
const attachedWebviewRef = useRef<WebviewTag | null>(null)
|
||||
@ -315,19 +316,13 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
|
||||
ref={inputRef}
|
||||
autoFocus
|
||||
value={query}
|
||||
onValueChange={setQuery}
|
||||
spellCheck={'false'}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
spellCheck={false}
|
||||
placeholder={t('common.search')}
|
||||
size="sm"
|
||||
radius="sm"
|
||||
variant="flat"
|
||||
classNames={{
|
||||
base: 'w-[240px]',
|
||||
inputWrapper:
|
||||
'h-8 bg-transparent border border-transparent shadow-none hover:border-transparent hover:bg-transparent focus:border-transparent data-[hover=true]:border-transparent data-[focus=true]:border-transparent data-[focus-visible=true]:outline-none data-[focus-visible=true]:ring-0',
|
||||
input: 'text-small focus:outline-none focus-visible:outline-none',
|
||||
innerWrapper: 'gap-0'
|
||||
}}
|
||||
size="small"
|
||||
variant="borderless"
|
||||
className="w-[240px]"
|
||||
style={{ height: '32px' }}
|
||||
/>
|
||||
<span
|
||||
className="min-w-[44px] text-center text-default-500 text-small tabular-nums"
|
||||
@ -339,38 +334,32 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
|
||||
</span>
|
||||
<div className="h-4 w-px bg-default-200" />
|
||||
<Button
|
||||
size="sm"
|
||||
variant="light"
|
||||
radius="full"
|
||||
isIconOnly
|
||||
onPress={goToPrevious}
|
||||
isDisabled={disableNavigation}
|
||||
size="small"
|
||||
type="text"
|
||||
onClick={goToPrevious}
|
||||
disabled={disableNavigation}
|
||||
aria-label="Previous match"
|
||||
className="text-default-500 hover:text-default-900">
|
||||
<ChevronUp size={16} />
|
||||
</Button>
|
||||
icon={<ChevronUp size={16} className="w-6" />}
|
||||
className="text-default-500 hover:text-default-900"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="light"
|
||||
radius="full"
|
||||
isIconOnly
|
||||
onPress={goToNext}
|
||||
isDisabled={disableNavigation}
|
||||
size="small"
|
||||
type="text"
|
||||
onClick={goToNext}
|
||||
disabled={disableNavigation}
|
||||
aria-label="Next match"
|
||||
className="text-default-500 hover:text-default-900">
|
||||
<ChevronDown size={16} />
|
||||
</Button>
|
||||
icon={<ChevronDown size={16} className="w-6" />}
|
||||
className="text-default-500 hover:text-default-900"
|
||||
/>
|
||||
<div className="h-4 w-px bg-default-200" />
|
||||
<Button
|
||||
size="sm"
|
||||
variant="light"
|
||||
radius="full"
|
||||
isIconOnly
|
||||
onPress={closeSearch}
|
||||
size="small"
|
||||
type="text"
|
||||
onClick={closeSearch}
|
||||
aria-label={t('common.close')}
|
||||
className="text-default-500 hover:text-default-900">
|
||||
<X size={16} />
|
||||
</Button>
|
||||
icon={<X size={16} className="w-6" />}
|
||||
className="text-default-500 hover:text-default-900"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { BreadcrumbItem, Breadcrumbs } from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import { NavbarCenter, NavbarHeader, NavbarRight } from '@renderer/components/app/Navbar'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
@ -6,7 +5,7 @@ import { useActiveNode } from '@renderer/hooks/useNotesQuery'
|
||||
import { useNotesSettings } from '@renderer/hooks/useNotesSettings'
|
||||
import { useShowWorkspace } from '@renderer/hooks/useShowWorkspace'
|
||||
import { findNode } from '@renderer/services/NotesTreeService'
|
||||
import { Dropdown, Input, Tooltip } from 'antd'
|
||||
import { Breadcrumb, Dropdown, Input, Tooltip } from 'antd'
|
||||
import { t } from 'i18next'
|
||||
import { MoreHorizontal, PanelLeftClose, PanelRightClose, Star } from 'lucide-react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
@ -191,42 +190,43 @@ const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar, onExpand
|
||||
</HStack>
|
||||
<NavbarCenter style={{ flex: 1, minWidth: 0 }}>
|
||||
<BreadcrumbsContainer>
|
||||
<Breadcrumbs style={{ borderRadius: 0 }}>
|
||||
{breadcrumbItems.map((item, index) => {
|
||||
<Breadcrumb
|
||||
separator={'>'}
|
||||
items={breadcrumbItems.map((item, index) => {
|
||||
const isLastItem = index === breadcrumbItems.length - 1
|
||||
const isCurrentNote = isLastItem && !item.isFolder
|
||||
|
||||
return (
|
||||
<BreadcrumbItem key={item.key} isCurrent={isLastItem}>
|
||||
{isCurrentNote ? (
|
||||
<TitleInputWrapper>
|
||||
<TitleInput
|
||||
ref={titleInputRef}
|
||||
value={titleValue}
|
||||
onChange={handleTitleChange}
|
||||
onBlur={handleTitleBlur}
|
||||
onKeyDown={handleTitleKeyDown}
|
||||
size="small"
|
||||
variant="borderless"
|
||||
style={{
|
||||
fontSize: 'inherit',
|
||||
padding: 0,
|
||||
height: 'auto',
|
||||
lineHeight: 'inherit'
|
||||
}}
|
||||
/>
|
||||
</TitleInputWrapper>
|
||||
) : (
|
||||
<BreadcrumbTitle
|
||||
onClick={() => handleBreadcrumbClick(item)}
|
||||
$clickable={item.isFolder && !isLastItem}>
|
||||
{item.title}
|
||||
</BreadcrumbTitle>
|
||||
)}
|
||||
</BreadcrumbItem>
|
||||
)
|
||||
})}
|
||||
</Breadcrumbs>
|
||||
return {
|
||||
title: (
|
||||
<div key={item.key} className="flex">
|
||||
{isCurrentNote ? (
|
||||
<TitleInputWrapper>
|
||||
<TitleInput
|
||||
ref={titleInputRef}
|
||||
value={titleValue}
|
||||
onChange={handleTitleChange}
|
||||
onBlur={handleTitleBlur}
|
||||
onKeyDown={handleTitleKeyDown}
|
||||
size="small"
|
||||
variant="borderless"
|
||||
style={{
|
||||
fontSize: 'inherit',
|
||||
padding: 0,
|
||||
height: 'auto',
|
||||
lineHeight: 'inherit'
|
||||
}}
|
||||
/>
|
||||
</TitleInputWrapper>
|
||||
) : (
|
||||
<BreadcrumbTitle
|
||||
onClick={() => handleBreadcrumbClick(item)}
|
||||
$clickable={item.isFolder && !isLastItem}>
|
||||
{item.title}
|
||||
</BreadcrumbTitle>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})}></Breadcrumb>
|
||||
</BreadcrumbsContainer>
|
||||
</NavbarCenter>
|
||||
<NavbarRight style={{ paddingRight: 0 }}>
|
||||
@ -347,13 +347,6 @@ export const BreadcrumbsContainer = styled.div`
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
/* 覆盖 HeroUI BreadcrumbItem 的样式 */
|
||||
& li:last-child [data-slot="item"] {
|
||||
flex: 1 !important;
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
/* 更强的样式覆盖 */
|
||||
& li:last-child * {
|
||||
max-width: none !important;
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { Select, SelectItem } from '@heroui/react'
|
||||
import { ProviderAvatarPrimitive } from '@renderer/components/ProviderAvatar'
|
||||
import { getProviderLogo } from '@renderer/config/providers'
|
||||
import ImageStorage from '@renderer/services/ImageStorage'
|
||||
import { getProviderNameById } from '@renderer/services/ProviderService'
|
||||
import type { Provider } from '@types'
|
||||
import { Select } from 'antd'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
@ -54,46 +54,46 @@ const ProviderSelect: FC<ProviderSelectProps> = ({ provider, options, onChange,
|
||||
|
||||
return (
|
||||
<Select
|
||||
selectedKeys={[provider.id]}
|
||||
onSelectionChange={(keys) => {
|
||||
const selectedKey = Array.from(keys)[0] as string
|
||||
onChange(selectedKey)
|
||||
}}
|
||||
style={style}
|
||||
className={`w-full ${className || ''}`}
|
||||
renderValue={(items) => {
|
||||
return items.map((item) => (
|
||||
<div key={item.key} className="flex items-center gap-2">
|
||||
value={provider.id}
|
||||
onChange={onChange}
|
||||
style={{ width: '100%', ...style }}
|
||||
className={className}
|
||||
options={providerOptions}
|
||||
labelRender={(props) => {
|
||||
const providerId = props.value as string
|
||||
const providerName = providerOptions.find((opt) => opt.value === providerId)?.label || ''
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex h-4 w-4 items-center justify-center">
|
||||
<ProviderAvatarPrimitive
|
||||
providerId={item.key as string}
|
||||
providerName={item.textValue || ''}
|
||||
logoSrc={getProviderLogoSrc(item.key as string)}
|
||||
providerId={providerId}
|
||||
providerName={providerName}
|
||||
logoSrc={getProviderLogoSrc(providerId)}
|
||||
size={16}
|
||||
/>
|
||||
</div>
|
||||
<span>{item.textValue}</span>
|
||||
<span>{providerName}</span>
|
||||
</div>
|
||||
))
|
||||
}}>
|
||||
{providerOptions.map((providerOption) => (
|
||||
<SelectItem
|
||||
key={providerOption.value}
|
||||
textValue={providerOption.label}
|
||||
startContent={
|
||||
)
|
||||
}}
|
||||
optionRender={(option) => {
|
||||
const providerId = option.value as string
|
||||
const providerName = option.label as string
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex h-4 w-4 items-center justify-center">
|
||||
<ProviderAvatarPrimitive
|
||||
providerId={providerOption.value}
|
||||
providerName={providerOption.label}
|
||||
logoSrc={getProviderLogoSrc(providerOption.value)}
|
||||
providerId={providerId}
|
||||
providerName={providerName}
|
||||
logoSrc={getProviderLogoSrc(providerId)}
|
||||
size={16}
|
||||
/>
|
||||
</div>
|
||||
}>
|
||||
{providerOption.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
<span>{providerName}</span>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { GithubOutlined } from '@ant-design/icons'
|
||||
import { useDisclosure } from '@heroui/react'
|
||||
import IndicatorLight from '@renderer/components/IndicatorLight'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import UpdateDialog from '@renderer/components/UpdateDialog'
|
||||
import UpdateDialogPopup from '@renderer/components/Popups/UpdateDialogPopup'
|
||||
import { APP_NAME, AppLogo } from '@renderer/config/env'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||
@ -15,7 +14,6 @@ import { ThemeMode } from '@renderer/types'
|
||||
import { runAsyncFunction } from '@renderer/utils'
|
||||
import { UpgradeChannel } from '@shared/config/constant'
|
||||
import { Avatar, Button, Progress, Radio, Row, Switch, Tag, Tooltip } from 'antd'
|
||||
import type { UpdateInfo } from 'builder-util-runtime'
|
||||
import { debounce } from 'lodash'
|
||||
import { Bug, Building2, Github, Globe, Mail, Rss } from 'lucide-react'
|
||||
import { BadgeQuestionMark } from 'lucide-react'
|
||||
@ -31,8 +29,6 @@ import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingTitl
|
||||
const AboutSettings: FC = () => {
|
||||
const [version, setVersion] = useState('')
|
||||
const [isPortable, setIsPortable] = useState(false)
|
||||
const [updateDialogInfo, setUpdateDialogInfo] = useState<UpdateInfo | null>(null)
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const { t } = useTranslation()
|
||||
const { autoCheckUpdate, setAutoCheckUpdate, testPlan, setTestPlan, testChannel, setTestChannel } = useSettings()
|
||||
const { theme } = useTheme()
|
||||
@ -48,8 +44,7 @@ const AboutSettings: FC = () => {
|
||||
|
||||
if (update.downloaded) {
|
||||
// Open update dialog directly in renderer
|
||||
setUpdateDialogInfo(update.info || null)
|
||||
onOpen()
|
||||
UpdateDialogPopup.show({ releaseInfo: update.info || null })
|
||||
return
|
||||
}
|
||||
|
||||
@ -342,9 +337,6 @@ const AboutSettings: FC = () => {
|
||||
<Button onClick={debug}>{t('settings.about.debug.open')}</Button>
|
||||
</SettingRow>
|
||||
</SettingGroup>
|
||||
|
||||
{/* Update Dialog */}
|
||||
<UpdateDialog isOpen={isOpen} onClose={onClose} releaseInfo={updateDialogInfo} />
|
||||
</SettingContainer>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Button, Tooltip } from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import type { AgentBaseWithId, UpdateAgentBaseForm, UpdateAgentFunctionUnion } from '@renderer/types'
|
||||
import { Button, Tooltip } from 'antd'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -65,21 +65,21 @@ export const AccessibleDirsSetting = ({ base, update }: AccessibleDirsSettingPro
|
||||
<SettingsItem>
|
||||
<SettingsTitle
|
||||
actions={
|
||||
<Tooltip content={t('agent.session.accessible_paths.add')}>
|
||||
<Button variant="light" size="sm" startContent={<Plus />} isIconOnly onPress={addAccessiblePath} />
|
||||
<Tooltip title={t('agent.session.accessible_paths.add')}>
|
||||
<Button type="text" icon={<Plus size={16} />} shape="circle" onClick={addAccessiblePath} />
|
||||
</Tooltip>
|
||||
}>
|
||||
{t('agent.session.accessible_paths.label')}
|
||||
</SettingsTitle>
|
||||
<ul className="flex flex-col gap-2">
|
||||
<ul className="flex flex-col">
|
||||
{base.accessible_paths.map((path) => (
|
||||
<li
|
||||
key={path}
|
||||
className="flex items-center justify-between gap-2 rounded-medium border border-default-200 px-2 py-1">
|
||||
<span className="w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap text-sm" title={path}>
|
||||
<li key={path} className="flex items-center justify-between gap-2 py-1">
|
||||
<span
|
||||
className="w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap text-[var(--color-text-2)] text-sm"
|
||||
title={path}>
|
||||
{path}
|
||||
</span>
|
||||
<Button size="sm" variant="light" color="danger" onPress={() => removeAccessiblePath(path)}>
|
||||
<Button size="small" type="text" danger onClick={() => removeAccessiblePath(path)}>
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
</li>
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { Input, Tooltip } from '@heroui/react'
|
||||
import type { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
|
||||
import type { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
|
||||
import type {
|
||||
@ -8,6 +7,7 @@ import type {
|
||||
UpdateAgentBaseForm
|
||||
} from '@renderer/types'
|
||||
import { AgentConfigurationSchema } from '@renderer/types'
|
||||
import { InputNumber, Tooltip } from 'antd'
|
||||
import { Info } from 'lucide-react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -31,34 +31,33 @@ const defaultConfiguration: AgentConfigurationState = AgentConfigurationSchema.p
|
||||
export const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({ agentBase, update }) => {
|
||||
const { t } = useTranslation()
|
||||
const [configuration, setConfiguration] = useState<AgentConfigurationState>(defaultConfiguration)
|
||||
const [maxTurnsInput, setMaxTurnsInput] = useState<string>(String(defaultConfiguration.max_turns))
|
||||
const [maxTurnsInput, setMaxTurnsInput] = useState<number>(defaultConfiguration.max_turns)
|
||||
|
||||
useEffect(() => {
|
||||
if (!agentBase) {
|
||||
setConfiguration(defaultConfiguration)
|
||||
setMaxTurnsInput(String(defaultConfiguration.max_turns))
|
||||
setMaxTurnsInput(defaultConfiguration.max_turns)
|
||||
return
|
||||
}
|
||||
const parsed: AgentConfigurationState = AgentConfigurationSchema.parse(agentBase.configuration ?? {})
|
||||
setConfiguration(parsed)
|
||||
setMaxTurnsInput(String(parsed.max_turns))
|
||||
setMaxTurnsInput(parsed.max_turns)
|
||||
}, [agentBase])
|
||||
|
||||
const commitMaxTurns = useCallback(() => {
|
||||
if (!agentBase) return
|
||||
const parsedValue = Number.parseInt(maxTurnsInput, 10)
|
||||
if (!Number.isFinite(parsedValue)) {
|
||||
setMaxTurnsInput(String(configuration.max_turns))
|
||||
if (!Number.isFinite(maxTurnsInput)) {
|
||||
setMaxTurnsInput(configuration.max_turns)
|
||||
return
|
||||
}
|
||||
const sanitized = Math.max(1, parsedValue)
|
||||
const sanitized = Math.max(1, maxTurnsInput)
|
||||
if (sanitized === configuration.max_turns) {
|
||||
setMaxTurnsInput(String(configuration.max_turns))
|
||||
setMaxTurnsInput(configuration.max_turns)
|
||||
return
|
||||
}
|
||||
const next: AgentConfigurationState = { ...configuration, max_turns: sanitized }
|
||||
setConfiguration(next)
|
||||
setMaxTurnsInput(String(sanitized))
|
||||
setMaxTurnsInput(sanitized)
|
||||
update({ id: agentBase.id, configuration: next } satisfies UpdateAgentBaseForm)
|
||||
}, [agentBase, configuration, maxTurnsInput, update])
|
||||
|
||||
@ -71,27 +70,23 @@ export const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({ agentBase, u
|
||||
<SettingsItem divider={false}>
|
||||
<SettingsTitle
|
||||
actions={
|
||||
<Tooltip content={t('agent.settings.advance.maxTurns.description')} placement="right">
|
||||
<Tooltip title={t('agent.settings.advance.maxTurns.description')} placement="left">
|
||||
<Info size={16} className="text-foreground-400" />
|
||||
</Tooltip>
|
||||
}>
|
||||
{t('agent.settings.advance.maxTurns.label')}
|
||||
</SettingsTitle>
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
<div className="my-2 flex w-full flex-col gap-2">
|
||||
<InputNumber
|
||||
min={1}
|
||||
value={maxTurnsInput}
|
||||
onValueChange={setMaxTurnsInput}
|
||||
onChange={(value) => setMaxTurnsInput(value ?? 1)}
|
||||
onBlur={commitMaxTurns}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
commitMaxTurns()
|
||||
}
|
||||
}}
|
||||
onPressEnter={commitMaxTurns}
|
||||
aria-label={t('agent.settings.advance.maxTurns.label')}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<span className="text-foreground-500 text-xs">{t('agent.settings.advance.maxTurns.helper')}</span>
|
||||
<span className="mt-1 text-foreground-500 text-xs">{t('agent.settings.advance.maxTurns.helper')}</span>
|
||||
</div>
|
||||
</SettingsItem>
|
||||
</SettingsContainer>
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { Alert, Spinner } from '@heroui/react'
|
||||
import { Center } from '@renderer/components/Layout'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { useAgent } from '@renderer/hooks/agents/useAgent'
|
||||
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
|
||||
import { Alert, Spin } from 'antd'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@ -71,18 +72,25 @@ const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ tab, ag
|
||||
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>
|
||||
<Center flex={1}>
|
||||
<Spin />
|
||||
</Center>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Center flex={1}>
|
||||
<Alert type="error" message={t('agent.get.error.failed')} />
|
||||
</Center>
|
||||
)
|
||||
}
|
||||
|
||||
if (!agent) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-1">
|
||||
<LeftMenu>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Textarea } from '@heroui/react'
|
||||
import type { AgentBaseWithId, UpdateAgentBaseForm, UpdateAgentFunctionUnion } from '@renderer/types'
|
||||
import TextArea from 'antd/es/input/TextArea'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@ -24,11 +24,12 @@ export const DescriptionSetting = ({ base, update }: DescriptionSettingProps) =>
|
||||
if (!base) return null
|
||||
|
||||
return (
|
||||
<SettingsItem>
|
||||
<SettingsItem divider={false}>
|
||||
<SettingsTitle>{t('common.description')}</SettingsTitle>
|
||||
<Textarea
|
||||
<TextArea
|
||||
value={description}
|
||||
onValueChange={setDescription}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
rows={4}
|
||||
onBlur={() => {
|
||||
if (description !== base.description) {
|
||||
updateDesc(description)
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { Avatar } from '@heroui/react'
|
||||
import { getAgentTypeAvatar } from '@renderer/config/agent'
|
||||
import type { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
|
||||
import type { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
|
||||
import { getAgentTypeLabel } from '@renderer/i18n/label'
|
||||
import type { GetAgentResponse, GetAgentSessionResponse } from '@renderer/types'
|
||||
import { isAgentEntity } from '@renderer/types'
|
||||
import { Avatar } from 'antd'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@ -42,7 +42,7 @@ const EssentialSettings: FC<EssentialSettingsProps> = ({ agentBase, update, show
|
||||
<SettingsItem inline>
|
||||
<SettingsTitle>{t('agent.type.label')}</SettingsTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar src={getAgentTypeAvatar(agentBase.type)} className="h-6 w-6 text-lg" />
|
||||
<Avatar size={24} src={getAgentTypeAvatar(agentBase.type)} className="h-6 w-6 text-lg" />
|
||||
<span>{(agentBase?.name ?? agentBase?.type) ? getAgentTypeLabel(agentBase.type) : ''}</span>
|
||||
</div>
|
||||
</SettingsItem>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Input } from '@heroui/react'
|
||||
import type { AgentBaseWithId, UpdateAgentBaseForm, UpdateAgentFunctionUnion } from '@renderer/types'
|
||||
import { Input } from 'antd'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@ -25,14 +25,13 @@ export const NameSetting = ({ base, update }: NameSettingsProps) => {
|
||||
<Input
|
||||
placeholder={t('common.agent_one') + t('common.name')}
|
||||
value={name}
|
||||
size="sm"
|
||||
onValueChange={(value) => setName(value)}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
onBlur={() => {
|
||||
if (name !== base.name) {
|
||||
updateName(name)
|
||||
}
|
||||
}}
|
||||
className="max-w-80 flex-1"
|
||||
className="max-w-70 flex-1"
|
||||
/>
|
||||
</SettingsItem>
|
||||
)
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import { Card, CardBody, Tab, Tabs } from '@heroui/react'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { useAvailablePlugins, useInstalledPlugins, usePluginActions } from '@renderer/hooks/usePlugins'
|
||||
import type { GetAgentResponse, GetAgentSessionResponse, UpdateAgentFunctionUnion } from '@renderer/types/agent'
|
||||
import { Card, Segmented } from 'antd'
|
||||
import type { FC } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { InstalledPluginsList } from './components/InstalledPluginsList'
|
||||
import { PluginBrowser } from './components/PluginBrowser'
|
||||
import { SettingsContainer } from './shared'
|
||||
|
||||
interface PluginSettingsProps {
|
||||
agentBase: GetAgentResponse | GetAgentSessionResponse
|
||||
@ -16,6 +17,7 @@ interface PluginSettingsProps {
|
||||
|
||||
const PluginSettings: FC<PluginSettingsProps> = ({ agentBase }) => {
|
||||
const { t } = useTranslation()
|
||||
const [activeTab, setActiveTab] = useState<string>('available')
|
||||
|
||||
// Fetch available plugins
|
||||
const { agents, commands, skills, loading: loadingAvailable, error: errorAvailable } = useAvailablePlugins()
|
||||
@ -54,61 +56,89 @@ const PluginSettings: FC<PluginSettingsProps> = ({ agentBase }) => {
|
||||
[uninstall, t]
|
||||
)
|
||||
|
||||
return (
|
||||
<SettingsContainer className="pr-0">
|
||||
<Tabs
|
||||
aria-label="Plugin settings tabs"
|
||||
classNames={{
|
||||
base: 'w-full',
|
||||
tabList: 'w-full',
|
||||
panel: 'w-full flex-1 overflow-hidden'
|
||||
}}>
|
||||
<Tab key="available" title={t('agent.settings.plugins.available.title')}>
|
||||
<div className="flex h-full flex-col overflow-y-auto pt-1 pr-2">
|
||||
{errorAvailable ? (
|
||||
<Card className="bg-danger-50 dark:bg-danger-900/20">
|
||||
<CardBody>
|
||||
<p className="text-danger">
|
||||
{t('agent.settings.plugins.error.load')}: {errorAvailable}
|
||||
</p>
|
||||
</CardBody>
|
||||
</Card>
|
||||
) : (
|
||||
<PluginBrowser
|
||||
agentId={agentBase.id}
|
||||
agents={agents}
|
||||
commands={commands}
|
||||
skills={skills}
|
||||
installedPlugins={plugins}
|
||||
onInstall={handleInstall}
|
||||
onUninstall={handleUninstall}
|
||||
loading={loadingAvailable || installing || uninstalling}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Tab>
|
||||
const segmentOptions = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
value: 'available',
|
||||
label: t('agent.settings.plugins.available.title')
|
||||
},
|
||||
{
|
||||
value: 'installed',
|
||||
label: t('agent.settings.plugins.installed.title')
|
||||
}
|
||||
]
|
||||
}, [t])
|
||||
|
||||
<Tab key="installed" title={t('agent.settings.plugins.installed.title')}>
|
||||
<div className="flex h-full flex-col overflow-y-auto pt-4 pr-2">
|
||||
{errorInstalled ? (
|
||||
<Card className="bg-danger-50 dark:bg-danger-900/20">
|
||||
<CardBody>
|
||||
<p className="text-danger">
|
||||
{t('agent.settings.plugins.error.load')}: {errorInstalled}
|
||||
</p>
|
||||
</CardBody>
|
||||
</Card>
|
||||
) : (
|
||||
<InstalledPluginsList
|
||||
plugins={plugins}
|
||||
onUninstall={handleUninstall}
|
||||
loading={loadingInstalled || uninstalling}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</SettingsContainer>
|
||||
const renderContent = useMemo(() => {
|
||||
if (activeTab === 'available') {
|
||||
return (
|
||||
<div className="flex h-full flex-col overflow-y-auto pt-4 pr-2">
|
||||
{errorAvailable ? (
|
||||
<Card variant="borderless">
|
||||
<p className="text-danger">
|
||||
{t('agent.settings.plugins.error.load')}: {errorAvailable}
|
||||
</p>
|
||||
</Card>
|
||||
) : (
|
||||
<PluginBrowser
|
||||
agentId={agentBase.id}
|
||||
agents={agents}
|
||||
commands={commands}
|
||||
skills={skills}
|
||||
installedPlugins={plugins}
|
||||
onInstall={handleInstall}
|
||||
onUninstall={handleUninstall}
|
||||
loading={loadingAvailable || installing || uninstalling}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col overflow-y-auto pt-4 pr-2">
|
||||
{errorInstalled ? (
|
||||
<Card className="bg-danger-50 dark:bg-danger-900/20">
|
||||
<p className="text-danger">
|
||||
{t('agent.settings.plugins.error.load')}: {errorInstalled}
|
||||
</p>
|
||||
</Card>
|
||||
) : (
|
||||
<InstalledPluginsList
|
||||
plugins={plugins}
|
||||
onUninstall={handleUninstall}
|
||||
loading={loadingInstalled || uninstalling}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}, [
|
||||
activeTab,
|
||||
agentBase.id,
|
||||
agents,
|
||||
commands,
|
||||
errorAvailable,
|
||||
errorInstalled,
|
||||
handleInstall,
|
||||
handleUninstall,
|
||||
installing,
|
||||
loadingAvailable,
|
||||
loadingInstalled,
|
||||
plugins,
|
||||
skills,
|
||||
t,
|
||||
uninstalling
|
||||
])
|
||||
|
||||
return (
|
||||
<Scrollbar>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex justify-center">
|
||||
<Segmented options={segmentOptions} value={activeTab} onChange={(value) => setActiveTab(value as string)} />
|
||||
</div>
|
||||
{renderContent}
|
||||
</div>
|
||||
</Scrollbar>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { Alert, Spinner } from '@heroui/react'
|
||||
import { Center } from '@renderer/components/Layout'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { useSession } from '@renderer/hooks/agents/useSession'
|
||||
import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
|
||||
import { Alert, Spin } from 'antd'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@ -68,15 +69,21 @@ const SessionSettingPopupContainer: React.FC<SessionSettingPopupParams> = ({ tab
|
||||
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>
|
||||
<Center flex={1}>
|
||||
<Spin />
|
||||
</Center>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Center flex={1}>
|
||||
<Alert type="error" message={t('agent.get.error.failed')} />
|
||||
</Center>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-1">
|
||||
<LeftMenu>
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { Alert, Card, CardBody, CardHeader, Chip, Input, Switch } from '@heroui/react'
|
||||
import { permissionModeCards } from '@renderer/config/agent'
|
||||
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||
import useScrollPosition from '@renderer/hooks/useScrollPosition'
|
||||
@ -13,8 +12,9 @@ import type {
|
||||
UpdateAgentSessionFunction
|
||||
} from '@renderer/types'
|
||||
import { AgentConfigurationSchema } from '@renderer/types'
|
||||
import { Modal } from 'antd'
|
||||
import { ShieldAlert, ShieldCheck, Wrench } from 'lucide-react'
|
||||
import { Modal, Tag } from 'antd'
|
||||
import { Alert, Card, Input, Switch } from 'antd'
|
||||
import { ShieldAlert, Wrench } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -272,47 +272,50 @@ export const ToolingSettings: FC<AgentToolingSettingsProps> = ({ agentBase, upda
|
||||
const showCaution = card.caution
|
||||
|
||||
return (
|
||||
<Card
|
||||
<div
|
||||
key={card.mode}
|
||||
isPressable={!disabled}
|
||||
isDisabled={disabled || isUpdatingMode}
|
||||
shadow="none"
|
||||
onPress={() => handleSelectPermissionMode(card.mode)}
|
||||
className={`border ${
|
||||
isSelected ? 'border-primary' : 'border-default-200'
|
||||
} ${disabled ? 'opacity-60' : ''}`}>
|
||||
<CardHeader className="flex items-start justify-between gap-2">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-left font-semibold text-sm">{t(card.titleKey, card.titleFallback)}</span>
|
||||
<span className="text-left text-foreground-500 text-xs">
|
||||
className={`flex flex-col gap-3 overflow-hidden rounded-lg border p-4 transition-colors ${
|
||||
isSelected
|
||||
? 'border-primary bg-primary-50/30 dark:bg-primary-950/20'
|
||||
: 'border-default-200 hover:bg-default-50 dark:hover:bg-default-900/20'
|
||||
} ${disabled ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'}`}
|
||||
onClick={() => !disabled && handleSelectPermissionMode(card.mode)}>
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
||||
<span className="whitespace-normal break-words text-left font-semibold text-sm">
|
||||
{t(card.titleKey, card.titleFallback)}
|
||||
</span>
|
||||
<span className="whitespace-normal break-words text-left text-foreground-500 text-xs">
|
||||
{t(card.descriptionKey, card.descriptionFallback)}
|
||||
</span>
|
||||
</div>
|
||||
{disabled ? (
|
||||
<Chip color="warning" size="sm" variant="flat">
|
||||
{t('common.coming_soon', 'Coming soon')}
|
||||
</Chip>
|
||||
) : isSelected ? (
|
||||
<Chip color="primary" size="sm" variant="flat" startContent={<ShieldCheck size={14} />}>
|
||||
{t('common.selected', 'Selected')}
|
||||
</Chip>
|
||||
) : null}
|
||||
</CardHeader>
|
||||
<CardBody className="gap-2 text-left text-xs">
|
||||
<span className="text-foreground-600">{t(card.behaviorKey, card.behaviorFallback)}</span>
|
||||
{showCaution ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<ShieldAlert className="text-danger-600" size={24} />
|
||||
<span className="text-danger-600">
|
||||
{disabled && <Tag color="warning">{t('common.coming_soon', 'Coming soon')}</Tag>}
|
||||
{isSelected && !disabled && (
|
||||
<Tag color="success">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>{t('common.selected', 'Selected')}</span>
|
||||
</div>
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-foreground-600 text-xs">{t(card.behaviorKey, card.behaviorFallback)}</span>
|
||||
{showCaution && (
|
||||
<div className="flex items-start gap-2 rounded-md bg-danger-50 p-2 dark:bg-danger-950/30">
|
||||
<ShieldAlert className="mt-0.5 flex-shrink-0 text-danger-600" size={16} />
|
||||
<span className="text-danger-600 text-xs">
|
||||
{t(
|
||||
'agent.settings.tooling.permissionMode.bypassPermissions.warning',
|
||||
'Use with caution — all tools will run without asking for approval.'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
@ -324,23 +327,31 @@ export const ToolingSettings: FC<AgentToolingSettingsProps> = ({ agentBase, upda
|
||||
</SettingsTitle>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Alert
|
||||
color="warning"
|
||||
title={t(
|
||||
'agent.settings.tooling.preapproved.warning.title',
|
||||
'Pre-approved tools run without manual review.'
|
||||
)}
|
||||
description={t(
|
||||
'agent.settings.tooling.preapproved.warning.description',
|
||||
'Enable only tools you trust. Mode defaults are highlighted automatically.'
|
||||
)}
|
||||
showIcon
|
||||
type="warning"
|
||||
style={{ padding: '8px 12px' }}
|
||||
message={
|
||||
<span className="font-semibold text-sm text-warning">
|
||||
{t('agent.settings.tooling.preapproved.warning.title', 'Pre-approved tools run without manual review.')}
|
||||
</span>
|
||||
}
|
||||
description={
|
||||
<span className="text-warning text-xs">
|
||||
{t(
|
||||
'agent.settings.tooling.preapproved.warning.description',
|
||||
'Enable only tools you trust. Mode defaults are highlighted automatically.'
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
isClearable
|
||||
allowClear
|
||||
value={searchTerm}
|
||||
onValueChange={setSearchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
placeholder={t('agent.settings.tooling.preapproved.search', 'Search tools')}
|
||||
aria-label={t('agent.settings.tooling.preapproved.search', 'Search tools')}
|
||||
className="w-full"
|
||||
size={'large'}
|
||||
/>
|
||||
<div className="flex flex-col gap-3">
|
||||
{filteredTools.length === 0 ? (
|
||||
@ -352,54 +363,71 @@ export const ToolingSettings: FC<AgentToolingSettingsProps> = ({ agentBase, upda
|
||||
const isAuto = autoToolIds.includes(tool.id)
|
||||
const isApproved = approvedToolIds.includes(tool.id)
|
||||
return (
|
||||
<Card key={tool.id} shadow="none" className="border border-default-200">
|
||||
<CardHeader className="flex items-start justify-between gap-3">
|
||||
<div className="flex min-w-0 flex-col gap-1">
|
||||
<span className="truncate font-medium text-sm">{tool.name}</span>
|
||||
{tool.description ? (
|
||||
<span className="line-clamp-2 text-foreground-500 text-xs">{tool.description}</span>
|
||||
) : null}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{isAuto ? (
|
||||
<Chip size="sm" color="primary" variant="flat">
|
||||
{t('agent.settings.tooling.preapproved.autoBadge', 'Added by mode')}
|
||||
</Chip>
|
||||
) : null}
|
||||
{tool.type === 'mcp' ? (
|
||||
<Chip size="sm" color="secondary" variant="flat">
|
||||
{t('agent.settings.tooling.preapproved.mcpBadge', 'MCP tool')}
|
||||
</Chip>
|
||||
) : null}
|
||||
{tool.requirePermissions ? (
|
||||
<Chip size="sm" color="warning" variant="flat">
|
||||
{t(
|
||||
'agent.settings.tooling.preapproved.requiresApproval',
|
||||
'Requires approval when disabled'
|
||||
)}
|
||||
</Chip>
|
||||
<Card
|
||||
key={tool.id}
|
||||
className="border border-default-200"
|
||||
title={
|
||||
<div className="flex items-start justify-between gap-3 py-2">
|
||||
<div className="flex min-w-0 flex-col gap-1">
|
||||
<span className="truncate font-medium text-sm">{tool.name}</span>
|
||||
{tool.description ? (
|
||||
<span className="line-clamp-2 whitespace-normal text-foreground-500 text-xs">
|
||||
{tool.description}
|
||||
</span>
|
||||
) : null}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{isAuto ? (
|
||||
<Tag color="success">
|
||||
{t('agent.settings.tooling.preapproved.autoBadge', 'Added by mode')}
|
||||
</Tag>
|
||||
) : null}
|
||||
{tool.type === 'mcp' ? (
|
||||
<Tag color="default">{t('agent.settings.tooling.preapproved.mcpBadge', 'MCP tool')}</Tag>
|
||||
) : null}
|
||||
{tool.requirePermissions ? (
|
||||
<Tag color="warning">
|
||||
{t(
|
||||
'agent.settings.tooling.preapproved.requiresApproval',
|
||||
'Requires approval when disabled'
|
||||
)}
|
||||
</Tag>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
aria-label={t('agent.settings.tooling.preapproved.toggle', {
|
||||
defaultValue: `Toggle ${tool.name}`,
|
||||
name: tool.name
|
||||
})}
|
||||
checked={isApproved}
|
||||
disabled={isAuto || isUpdatingTools}
|
||||
size="small"
|
||||
onChange={(checked) => handleToggleTool(tool.id, checked)}
|
||||
/>
|
||||
</div>
|
||||
<Switch
|
||||
aria-label={t('agent.settings.tooling.preapproved.toggle', {
|
||||
defaultValue: `Toggle ${tool.name}`,
|
||||
name: tool.name
|
||||
})}
|
||||
isSelected={isApproved}
|
||||
isDisabled={isAuto || isUpdatingTools}
|
||||
size="sm"
|
||||
onValueChange={(value) => handleToggleTool(tool.id, value)}
|
||||
/>
|
||||
</CardHeader>
|
||||
}
|
||||
styles={{
|
||||
header: {
|
||||
paddingLeft: '12px',
|
||||
paddingRight: '12px',
|
||||
borderBottom: 'none'
|
||||
},
|
||||
body: {
|
||||
paddingLeft: '12px',
|
||||
paddingRight: '12px',
|
||||
paddingTop: '0px',
|
||||
paddingBottom: '0px'
|
||||
}
|
||||
}}>
|
||||
{isAuto ? (
|
||||
<CardBody className="py-0 pb-3">
|
||||
<div className="py-0 pb-3">
|
||||
<span className="text-foreground-400 text-xs">
|
||||
{t(
|
||||
'agent.settings.tooling.preapproved.autoDescription',
|
||||
'This tool is auto-approved by the current permission mode.'
|
||||
)}
|
||||
</span>
|
||||
</CardBody>
|
||||
</div>
|
||||
) : null}
|
||||
</Card>
|
||||
)
|
||||
@ -427,26 +455,43 @@ export const ToolingSettings: FC<AgentToolingSettingsProps> = ({ agentBase, upda
|
||||
{availableServers.map((server) => {
|
||||
const isSelected = selectedMcpIds.includes(server.id)
|
||||
return (
|
||||
<Card key={server.id} shadow="none" className="border border-default-200">
|
||||
<CardHeader className="flex items-center justify-between gap-2">
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<span className="truncate font-medium text-sm">{server.name}</span>
|
||||
{server.description ? (
|
||||
<span className="line-clamp-2 text-foreground-500 text-xs">{server.description}</span>
|
||||
) : null}
|
||||
<Card
|
||||
key={server.id}
|
||||
className="border border-default-200"
|
||||
title={
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<span className="truncate font-medium text-sm">{server.name}</span>
|
||||
{server.description ? (
|
||||
<span className="line-clamp-2 text-foreground-500 text-xs">{server.description}</span>
|
||||
) : null}
|
||||
</div>
|
||||
<Switch
|
||||
aria-label={t('agent.settings.tooling.mcp.toggle', {
|
||||
defaultValue: `Toggle ${server.name}`,
|
||||
name: server.name
|
||||
})}
|
||||
checked={isSelected}
|
||||
size="small"
|
||||
disabled={!server.isActive || isUpdatingMcp}
|
||||
onChange={(checked) => handleToggleMcp(server.id, checked)}
|
||||
/>
|
||||
</div>
|
||||
<Switch
|
||||
aria-label={t('agent.settings.tooling.mcp.toggle', {
|
||||
defaultValue: `Toggle ${server.name}`,
|
||||
name: server.name
|
||||
})}
|
||||
isSelected={isSelected}
|
||||
size="sm"
|
||||
isDisabled={!server.isActive || isUpdatingMcp}
|
||||
onValueChange={(value) => handleToggleMcp(server.id, value)}
|
||||
/>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
}
|
||||
styles={{
|
||||
header: {
|
||||
paddingLeft: '12px',
|
||||
paddingRight: '12px',
|
||||
borderBottom: 'none'
|
||||
},
|
||||
body: {
|
||||
paddingLeft: '12px',
|
||||
paddingRight: '12px',
|
||||
paddingTop: '0px',
|
||||
paddingBottom: '0px'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
@ -462,33 +507,47 @@ export const ToolingSettings: FC<AgentToolingSettingsProps> = ({ agentBase, upda
|
||||
|
||||
<SettingsItem divider={false}>
|
||||
<SettingsTitle>{t('agent.settings.tooling.steps.review.title', 'Step 3 · Review')}</SettingsTitle>
|
||||
<Card shadow="none" className="border border-default-200">
|
||||
<CardBody className="flex flex-col gap-2 text-sm">
|
||||
<Card
|
||||
className="border border-default-200"
|
||||
styles={{
|
||||
header: {
|
||||
paddingLeft: '12px',
|
||||
paddingRight: '12px',
|
||||
borderBottom: 'none'
|
||||
},
|
||||
body: {
|
||||
paddingLeft: '12px',
|
||||
paddingRight: '12px',
|
||||
paddingTop: '12px',
|
||||
paddingBottom: '12px'
|
||||
}
|
||||
}}>
|
||||
<div className="flex flex-col gap-2 text-sm">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Chip variant="flat" color="primary">
|
||||
<Tag color="success">
|
||||
{t('agent.settings.tooling.review.mode', {
|
||||
defaultValue: `Mode: ${selectedMode}`,
|
||||
mode: selectedMode
|
||||
})}
|
||||
</Chip>
|
||||
<Chip variant="flat" color="default">
|
||||
</Tag>
|
||||
<Tag color="default">
|
||||
{t('agent.settings.tooling.review.autoTools', {
|
||||
defaultValue: `Auto: ${autoCount}`,
|
||||
count: autoCount
|
||||
})}
|
||||
</Chip>
|
||||
<Chip variant="flat" color="success">
|
||||
</Tag>
|
||||
<Tag color="success">
|
||||
{t('agent.settings.tooling.review.customTools', {
|
||||
defaultValue: `Custom: ${customCount}`,
|
||||
count: customCount
|
||||
})}
|
||||
</Chip>
|
||||
<Chip variant="flat" color="warning">
|
||||
</Tag>
|
||||
<Tag color="warning">
|
||||
{t('agent.settings.tooling.review.mcp', {
|
||||
defaultValue: `MCP: ${agentSummary.mcps}`,
|
||||
count: agentSummary.mcps
|
||||
})}
|
||||
</Chip>
|
||||
</Tag>
|
||||
</div>
|
||||
<span className="text-foreground-500 text-xs">
|
||||
{t(
|
||||
@ -496,7 +555,7 @@ export const ToolingSettings: FC<AgentToolingSettingsProps> = ({ agentBase, upda
|
||||
'Changes save automatically. Adjust the steps above any time to fine-tune permissions.'
|
||||
)}
|
||||
</span>
|
||||
</CardBody>
|
||||
</div>
|
||||
</Card>
|
||||
</SettingsItem>
|
||||
</SettingsContainer>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { Button, Chip, Skeleton, Table, TableBody, TableCell, TableColumn, TableHeader, TableRow } from '@heroui/react'
|
||||
import type { InstalledPlugin } from '@renderer/types/plugin'
|
||||
import { Trash2 } from 'lucide-react'
|
||||
import type { TableProps } from 'antd'
|
||||
import { Button, Skeleton, Table as AntTable, Tag } from 'antd'
|
||||
import { Dot, Trash2 } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -33,10 +34,10 @@ export const InstalledPluginsList: FC<InstalledPluginsListProps> = ({ plugins, o
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-12 w-full rounded-lg" />
|
||||
<Skeleton className="h-12 w-full rounded-lg" />
|
||||
<Skeleton className="h-12 w-full rounded-lg" />
|
||||
<div className="flex flex-col space-y-2">
|
||||
<Skeleton.Input active className="w-full" size={'large'} style={{ width: '100%' }} />
|
||||
<Skeleton.Input active className="w-full" size={'large'} style={{ width: '100%' }} />
|
||||
<Skeleton.Input active className="w-full" size={'large'} style={{ width: '100%' }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -50,50 +51,61 @@ export const InstalledPluginsList: FC<InstalledPluginsListProps> = ({ plugins, o
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Table aria-label="Installed plugins table" removeWrapper>
|
||||
<TableHeader>
|
||||
<TableColumn>{t('plugins.name')}</TableColumn>
|
||||
<TableColumn>{t('plugins.type')}</TableColumn>
|
||||
<TableColumn>{t('plugins.category')}</TableColumn>
|
||||
<TableColumn align="end">{t('plugins.actions')}</TableColumn>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{plugins.map((plugin) => (
|
||||
<TableRow key={plugin.filename}>
|
||||
<TableCell>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-semibold text-small">{plugin.metadata.name}</span>
|
||||
{plugin.metadata.description && (
|
||||
<span className="line-clamp-1 text-default-400 text-tiny">{plugin.metadata.description}</span>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip size="sm" variant="flat" color={plugin.type === 'agent' ? 'primary' : 'secondary'}>
|
||||
{plugin.type}
|
||||
</Chip>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip size="sm" variant="dot">
|
||||
{plugin.metadata.category}
|
||||
</Chip>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
size="sm"
|
||||
color="danger"
|
||||
variant="light"
|
||||
isIconOnly
|
||||
onPress={() => handleUninstall(plugin)}
|
||||
isLoading={uninstallingPlugin === plugin.filename}
|
||||
isDisabled={loading}>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)
|
||||
const columns: TableProps<InstalledPlugin>['columns'] = [
|
||||
{
|
||||
title: t('plugins.name'),
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
render: (_: any, plugin: InstalledPlugin) => (
|
||||
<div className="flex flex-col">
|
||||
<span className="font-semibold text-small">{plugin.metadata.name}</span>
|
||||
{plugin.metadata.description && (
|
||||
<span className="line-clamp-1 text-default-400 text-tiny">{plugin.metadata.description}</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('plugins.type'),
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
align: 'center',
|
||||
render: (type: string) => <Tag color={type === 'agent' ? 'magenta' : 'purple'}>{type}</Tag>
|
||||
},
|
||||
{
|
||||
title: t('plugins.category'),
|
||||
dataIndex: 'category',
|
||||
key: 'category',
|
||||
align: 'center',
|
||||
render: (_: any, plugin: InstalledPlugin) => (
|
||||
<Tag
|
||||
icon={<Dot size={14} strokeWidth={8} />}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: '2px'
|
||||
}}>
|
||||
{plugin.metadata.category}
|
||||
</Tag>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('plugins.actions'),
|
||||
key: 'actions',
|
||||
align: 'center',
|
||||
render: (_: any, plugin: InstalledPlugin) => (
|
||||
<Button
|
||||
danger
|
||||
type="text"
|
||||
onClick={() => handleUninstall(plugin)}
|
||||
loading={uninstallingPlugin === plugin.filename}
|
||||
disabled={loading}
|
||||
icon={<Trash2 className="h-4 w-4" />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return <AntTable columns={columns} dataSource={plugins} size="small" />
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Input, Tab, Tabs } from '@heroui/react'
|
||||
import type { InstalledPlugin, PluginMetadata } from '@renderer/types/plugin'
|
||||
import { Button as AntButton, Dropdown as AntDropdown, Input as AntInput, Tabs as AntTabs } from 'antd'
|
||||
import type { ItemType } from 'antd/es/menu/interface'
|
||||
import { Filter, Search } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
@ -42,6 +43,7 @@ export const PluginBrowser: FC<PluginBrowserProps> = ({
|
||||
const [selectedPlugin, setSelectedPlugin] = useState<PluginMetadata | null>(null)
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
const observerTarget = useRef<HTMLDivElement>(null)
|
||||
const [filterDropdownOpen, setFilterDropdownOpen] = useState(false)
|
||||
|
||||
// Combine all plugins based on active type
|
||||
const allPlugins = useMemo(() => {
|
||||
@ -92,6 +94,68 @@ export const PluginBrowser: FC<PluginBrowserProps> = ({
|
||||
return filteredPlugins.slice(0, displayCount)
|
||||
}, [filteredPlugins, displayCount])
|
||||
|
||||
const pluginCategoryMenuItems = useMemo(() => {
|
||||
const isSelected = (category: string): boolean =>
|
||||
category === 'all' ? selectedCategories.length === 0 : selectedCategories.includes(category)
|
||||
const handleClick = (category: string) => {
|
||||
if (category === 'all') {
|
||||
handleCategoryChange(new Set(['all']))
|
||||
} else {
|
||||
const newKeys = selectedCategories.includes(category)
|
||||
? new Set(selectedCategories.filter((c) => c !== category))
|
||||
: new Set([...selectedCategories, category])
|
||||
handleCategoryChange(newKeys)
|
||||
}
|
||||
}
|
||||
|
||||
const itemLabel = (category: string) => (
|
||||
<div className="flex flex-row justify-between">
|
||||
{category}
|
||||
{isSelected(category) && <span className="ml-2 text-primary text-sm">✓</span>}
|
||||
</div>
|
||||
)
|
||||
|
||||
return [
|
||||
{
|
||||
key: 'all',
|
||||
title: t('plugins.all_categories'),
|
||||
label: itemLabel('all'),
|
||||
onClick: () => handleClick('all')
|
||||
},
|
||||
...allCategories.map(
|
||||
(category) =>
|
||||
({
|
||||
key: category,
|
||||
title: category,
|
||||
label: itemLabel(category),
|
||||
onClick: () => handleClick(category)
|
||||
}) satisfies ItemType
|
||||
)
|
||||
]
|
||||
}, [allCategories, selectedCategories, t])
|
||||
|
||||
const pluginTypeTabItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: 'all',
|
||||
label: t('plugins.all_types')
|
||||
},
|
||||
{
|
||||
key: 'agent',
|
||||
label: t('plugins.agents')
|
||||
},
|
||||
{
|
||||
key: 'command',
|
||||
label: t('plugins.commands')
|
||||
},
|
||||
{
|
||||
key: 'skill',
|
||||
label: t('plugins.skills')
|
||||
}
|
||||
],
|
||||
[t]
|
||||
)
|
||||
|
||||
const hasMore = displayCount < filteredPlugins.length
|
||||
|
||||
// Reset display count when filters change
|
||||
@ -169,74 +233,37 @@ export const PluginBrowser: FC<PluginBrowserProps> = ({
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Search and Filter */}
|
||||
<div className="relative flex gap-0">
|
||||
<Input
|
||||
<div className="flex gap-2">
|
||||
<AntInput
|
||||
placeholder={t('plugins.search_placeholder')}
|
||||
value={searchQuery}
|
||||
onValueChange={handleSearchChange}
|
||||
startContent={<Search className="h-4 w-4 text-default-400" />}
|
||||
isClearable
|
||||
size="md"
|
||||
className="flex-1"
|
||||
classNames={{
|
||||
inputWrapper: 'pr-12'
|
||||
}}
|
||||
onChange={(e) => handleSearchChange(e.target.value)}
|
||||
prefix={<Search className="h-4 w-4 text-default-400" />}
|
||||
/>
|
||||
<Dropdown placement="bottom-end" classNames={{ content: 'max-h-60 overflow-y-auto p-0' }}>
|
||||
<DropdownTrigger>
|
||||
<Button
|
||||
isIconOnly
|
||||
variant={selectedCategories.length > 0 ? 'flat' : 'light'}
|
||||
color={selectedCategories.length > 0 ? 'primary' : 'default'}
|
||||
size="sm"
|
||||
className="-translate-y-1/2 absolute top-1/2 right-2 z-10">
|
||||
<Filter className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu
|
||||
aria-label="Category filter"
|
||||
closeOnSelect={false}
|
||||
className="max-h-60 overflow-y-auto"
|
||||
items={[
|
||||
{ key: 'all', label: t('plugins.all_categories') },
|
||||
...allCategories.map((category) => ({ key: category, label: category }))
|
||||
]}>
|
||||
{(item) => {
|
||||
const isSelected =
|
||||
item.key === 'all' ? selectedCategories.length === 0 : selectedCategories.includes(item.key)
|
||||
|
||||
return (
|
||||
<DropdownItem
|
||||
key={item.key}
|
||||
textValue={item.label}
|
||||
onPress={() => {
|
||||
if (item.key === 'all') {
|
||||
handleCategoryChange(new Set(['all']))
|
||||
} else {
|
||||
const newKeys = selectedCategories.includes(item.key)
|
||||
? new Set(selectedCategories.filter((c) => c !== item.key))
|
||||
: new Set([...selectedCategories, item.key])
|
||||
handleCategoryChange(newKeys)
|
||||
}
|
||||
}}
|
||||
className={isSelected ? 'bg-primary-50' : ''}>
|
||||
{item.label}
|
||||
{isSelected && <span className="ml-2 text-primary text-sm">✓</span>}
|
||||
</DropdownItem>
|
||||
)
|
||||
}}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
<AntDropdown
|
||||
menu={{ items: pluginCategoryMenuItems }}
|
||||
trigger={['click']}
|
||||
open={filterDropdownOpen}
|
||||
placement="bottomRight"
|
||||
onOpenChange={setFilterDropdownOpen}>
|
||||
<AntButton
|
||||
variant={selectedCategories.length > 0 ? 'filled' : 'outlined'}
|
||||
color={selectedCategories.length > 0 ? 'primary' : 'default'}
|
||||
size="middle"
|
||||
icon={<Filter className="h-4 w-4" color="var(--color-text-2)" />}
|
||||
/>
|
||||
</AntDropdown>
|
||||
</div>
|
||||
|
||||
{/* Type Tabs */}
|
||||
<div className="-mt-3 flex justify-center">
|
||||
<Tabs selectedKey={activeType} onSelectionChange={handleTypeChange} variant="underlined">
|
||||
<Tab key="all" title={t('plugins.all_types')} />
|
||||
<Tab key="agent" title={t('plugins.agents')} />
|
||||
<Tab key="command" title={t('plugins.commands')} />
|
||||
<Tab key="skill" title={t('plugins.skills')} />
|
||||
</Tabs>
|
||||
<div className="-mb-3 flex w-full justify-center">
|
||||
<AntTabs
|
||||
activeKey={activeType}
|
||||
onChange={handleTypeChange}
|
||||
items={pluginTypeTabItems}
|
||||
className="w-full"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Result Count */}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Button, Card, CardBody, CardFooter, CardHeader, Chip, Spinner } from '@heroui/react'
|
||||
import type { PluginMetadata } from '@renderer/types/plugin'
|
||||
import { Button, Card, Spin, Tag } from 'antd'
|
||||
import { upperFirst } from 'lodash'
|
||||
import { Download, Trash2 } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
@ -17,73 +17,73 @@ export interface PluginCardProps {
|
||||
export const PluginCard: FC<PluginCardProps> = ({ plugin, installed, onInstall, onUninstall, loading, onClick }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const getTypeTagColor = () => {
|
||||
if (plugin.type === 'agent') return 'blue'
|
||||
if (plugin.type === 'skill') return 'green'
|
||||
return 'default'
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="flex h-full w-full cursor-pointer flex-col border-[0.5px] border-default-200"
|
||||
isPressable
|
||||
shadow="none"
|
||||
onPress={onClick}>
|
||||
<CardHeader className="flex flex-col items-start gap-2 pb-2">
|
||||
className="flex h-full w-full cursor-pointer flex-col"
|
||||
onClick={onClick}
|
||||
styles={{
|
||||
body: { display: 'flex', flexDirection: 'column', height: '100%', padding: '16px' }
|
||||
}}>
|
||||
<div className="flex flex-col items-start gap-2 pb-2">
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<h3 className="truncate font-medium text-small">{plugin.name}</h3>
|
||||
<Chip
|
||||
size="sm"
|
||||
variant="solid"
|
||||
color={plugin.type === 'agent' ? 'primary' : plugin.type === 'skill' ? 'success' : 'secondary'}
|
||||
className="h-4 min-w-0 flex-shrink-0 px-0.5 text-xs">
|
||||
<h3 className="truncate font-medium text-sm">{plugin.name}</h3>
|
||||
<Tag color={getTypeTagColor()} className="m-0 text-xs">
|
||||
{upperFirst(plugin.type)}
|
||||
</Chip>
|
||||
</Tag>
|
||||
</div>
|
||||
<Chip size="sm" variant="dot" color="default">
|
||||
{plugin.category}
|
||||
</Chip>
|
||||
</CardHeader>
|
||||
<Tag className="m-0">{plugin.category}</Tag>
|
||||
</div>
|
||||
|
||||
<CardBody className="flex-1 py-2">
|
||||
<p className="line-clamp-3 text-default-500 text-small">{plugin.description || t('plugins.no_description')}</p>
|
||||
<div className="flex-1 py-2">
|
||||
<p className="line-clamp-3 text-gray-500 text-sm">{plugin.description || t('plugins.no_description')}</p>
|
||||
|
||||
{plugin.tags && plugin.tags.length > 0 && (
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{plugin.tags.map((tag) => (
|
||||
<Chip key={tag} size="sm" variant="bordered" className="text-tiny">
|
||||
<Tag key={tag} bordered className="text-xs">
|
||||
{tag}
|
||||
</Chip>
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardBody>
|
||||
</div>
|
||||
|
||||
<CardFooter className="pt-2">
|
||||
<div className="pt-2">
|
||||
{installed ? (
|
||||
<Button
|
||||
color="danger"
|
||||
variant="flat"
|
||||
size="sm"
|
||||
startContent={loading ? <Spinner size="sm" color="current" /> : <Trash2 className="h-4 w-4" />}
|
||||
danger
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={loading ? <Spin size="small" /> : <Trash2 className="h-4 w-4" />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onUninstall()
|
||||
}}
|
||||
isDisabled={loading}
|
||||
fullWidth>
|
||||
disabled={loading}
|
||||
block>
|
||||
{loading ? t('plugins.uninstalling') : t('plugins.uninstall')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
color="primary"
|
||||
variant="flat"
|
||||
size="sm"
|
||||
startContent={loading ? <Spinner size="sm" color="current" /> : <Download className="h-4 w-4" />}
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={loading ? <Spin size="small" /> : <Download className="h-4 w-4" />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onInstall()
|
||||
}}
|
||||
isDisabled={loading}
|
||||
fullWidth>
|
||||
disabled={loading}
|
||||
block>
|
||||
{loading ? t('plugins.installing') : t('plugins.install')}
|
||||
</Button>
|
||||
)}
|
||||
</CardFooter>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,16 +1,6 @@
|
||||
import {
|
||||
Button,
|
||||
Chip,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
Spinner,
|
||||
Textarea
|
||||
} from '@heroui/react'
|
||||
import type { PluginMetadata } from '@renderer/types/plugin'
|
||||
import { Download, Edit, Save, Trash2, X } from 'lucide-react'
|
||||
import { Button, Input, Modal, Spin, Tag } from 'antd'
|
||||
import { Dot, Download, Edit, Save, Trash2, X } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
@ -122,198 +112,201 @@ export const PluginDetailModal: FC<PluginDetailModalProps> = ({
|
||||
|
||||
const modalContent = (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
size="2xl"
|
||||
scrollBehavior="inside"
|
||||
classNames={{
|
||||
wrapper: 'z-[9999]'
|
||||
}}>
|
||||
<ModalContent>
|
||||
<ModalHeader className="flex flex-col gap-1">
|
||||
centered
|
||||
open={isOpen}
|
||||
onCancel={onClose}
|
||||
styles={{
|
||||
body: {
|
||||
maxHeight: '60vh',
|
||||
overflowY: 'auto'
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
width: '70%'
|
||||
}}
|
||||
title={
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="font-bold text-xl">{plugin.name}</h2>
|
||||
<Chip size="sm" variant="solid" color={plugin.type === 'agent' ? 'primary' : 'secondary'}>
|
||||
{plugin.type}
|
||||
</Chip>
|
||||
<Tag color={plugin.type === 'agent' ? 'magenta' : 'purple'}>{plugin.type}</Tag>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Chip size="sm" variant="dot" color="default">
|
||||
<Tag
|
||||
icon={<Dot size={14} strokeWidth={8} />}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: '2px'
|
||||
}}>
|
||||
{plugin.category}
|
||||
</Chip>
|
||||
{plugin.version && (
|
||||
<Chip size="sm" variant="bordered">
|
||||
v{plugin.version}
|
||||
</Chip>
|
||||
)}
|
||||
</Tag>
|
||||
{plugin.version && <Tag>v{plugin.version}</Tag>}
|
||||
</div>
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{/* Description */}
|
||||
{plugin.description && (
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-2 font-semibold text-small">Description</h3>
|
||||
<p className="text-default-600 text-small">{plugin.description}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Author */}
|
||||
{plugin.author && (
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-2 font-semibold text-small">Author</h3>
|
||||
<p className="text-default-600 text-small">{plugin.author}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tools (for agents) */}
|
||||
{plugin.tools && plugin.tools.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-2 font-semibold text-small">Tools</h3>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{plugin.tools.map((tool) => (
|
||||
<Chip key={tool} size="sm" variant="flat">
|
||||
{tool}
|
||||
</Chip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Allowed Tools (for commands) */}
|
||||
{plugin.allowed_tools && plugin.allowed_tools.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-2 font-semibold text-small">Allowed Tools</h3>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{plugin.allowed_tools.map((tool) => (
|
||||
<Chip key={tool} size="sm" variant="flat">
|
||||
{tool}
|
||||
</Chip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tags */}
|
||||
{plugin.tags && plugin.tags.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-2 font-semibold text-small">Tags</h3>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{plugin.tags.map((tag) => (
|
||||
<Chip key={tag} size="sm" variant="bordered">
|
||||
{tag}
|
||||
</Chip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Metadata */}
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-2 font-semibold text-small">Metadata</h3>
|
||||
<div className="space-y-1 text-small">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-default-500">File:</span>
|
||||
<span className="font-mono text-default-600 text-tiny">{plugin.filename}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-default-500">Size:</span>
|
||||
<span className="text-default-600">{(plugin.size / 1024).toFixed(2)} KB</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-default-500">Source:</span>
|
||||
<span className="font-mono text-default-600 text-tiny">{plugin.sourcePath}</span>
|
||||
</div>
|
||||
{plugin.installedAt && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-default-500">Installed:</span>
|
||||
<span className="text-default-600">{new Date(plugin.installedAt).toLocaleString()}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="mb-4">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h3 className="font-semibold text-small">Content</h3>
|
||||
{installed && !contentLoading && !contentError && (
|
||||
<div className="flex gap-2">
|
||||
{isEditing ? (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
color="danger"
|
||||
startContent={<X className="h-3 w-3" />}
|
||||
onPress={handleCancelEdit}
|
||||
isDisabled={saving}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
color="primary"
|
||||
startContent={saving ? <Spinner size="sm" color="current" /> : <Save className="h-3 w-3" />}
|
||||
onPress={handleSave}
|
||||
isDisabled={saving}>
|
||||
{saving ? 'Saving...' : 'Save'}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button size="sm" variant="flat" startContent={<Edit className="h-3 w-3" />} onPress={handleEdit}>
|
||||
Edit
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{contentLoading ? (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<Spinner size="sm" />
|
||||
</div>
|
||||
) : contentError ? (
|
||||
<div className="rounded-md bg-danger-50 p-3 text-danger text-small">{contentError}</div>
|
||||
) : isEditing ? (
|
||||
<Textarea
|
||||
value={editedContent}
|
||||
onValueChange={setEditedContent}
|
||||
minRows={20}
|
||||
classNames={{
|
||||
input: 'font-mono text-tiny'
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<pre className="max-h-96 overflow-auto whitespace-pre-wrap rounded-md bg-default-100 p-3 font-mono text-tiny">
|
||||
{content}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button variant="light" onPress={onClose}>
|
||||
Close
|
||||
</div>
|
||||
}
|
||||
footer={
|
||||
<div className="flex flex-row justify-end gap-4">
|
||||
<Button type="text" onClick={onClose}>
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
{installed ? (
|
||||
<Button
|
||||
color="danger"
|
||||
variant="flat"
|
||||
startContent={loading ? <Spinner size="sm" color="current" /> : <Trash2 className="h-4 w-4" />}
|
||||
onPress={onUninstall}
|
||||
isDisabled={loading}>
|
||||
danger
|
||||
variant="filled"
|
||||
icon={loading ? <Spin size="small" /> : <Trash2 className="h-4 w-4" />}
|
||||
iconPosition={'start'}
|
||||
onClick={onUninstall}
|
||||
disabled={loading}>
|
||||
{loading ? t('plugins.uninstalling') : t('plugins.uninstall')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
color="primary"
|
||||
startContent={loading ? <Spinner size="sm" color="current" /> : <Download className="h-4 w-4" />}
|
||||
onPress={onInstall}
|
||||
isDisabled={loading}>
|
||||
variant="solid"
|
||||
icon={loading ? <Spin size="small" /> : <Download className="h-4 w-4" />}
|
||||
iconPosition={'start'}
|
||||
onClick={onInstall}
|
||||
disabled={loading}>
|
||||
{loading ? t('plugins.installing') : t('plugins.install')}
|
||||
</Button>
|
||||
)}
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</div>
|
||||
}>
|
||||
<div>
|
||||
{/* Description */}
|
||||
{plugin.description && (
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-2 font-semibold text-small">Description</h3>
|
||||
<p className="text-default-600 text-small">{plugin.description}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Author */}
|
||||
{plugin.author && (
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-2 font-semibold text-small">Author</h3>
|
||||
<p className="text-default-600 text-small">{plugin.author}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tools (for agents) */}
|
||||
{plugin.tools && plugin.tools.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-2 font-semibold text-small">Tools</h3>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{plugin.tools.map((tool) => (
|
||||
<Tag key={tool}>{tool}</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Allowed Tools (for commands) */}
|
||||
{plugin.allowed_tools && plugin.allowed_tools.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-2 font-semibold text-small">Allowed Tools</h3>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{plugin.allowed_tools.map((tool) => (
|
||||
<Tag key={tool}>{tool}</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tags */}
|
||||
{plugin.tags && plugin.tags.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-2 font-semibold text-small">Tags</h3>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{plugin.tags.map((tag) => (
|
||||
<Tag key={tag}>{tag}</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Metadata */}
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-2 font-semibold text-small">Metadata</h3>
|
||||
<div className="space-y-1 text-small">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-default-500">File:</span>
|
||||
<span className="font-mono text-default-600 text-tiny">{plugin.filename}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-default-500">Size:</span>
|
||||
<span className="text-default-600">{(plugin.size / 1024).toFixed(2)} KB</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-default-500">Source:</span>
|
||||
<span className="font-mono text-default-600 text-tiny">{plugin.sourcePath}</span>
|
||||
</div>
|
||||
{plugin.installedAt && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-default-500">Installed:</span>
|
||||
<span className="text-default-600">{new Date(plugin.installedAt).toLocaleString()}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="mb-4">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h3 className="font-semibold text-small">Content</h3>
|
||||
{installed && !contentLoading && !contentError && (
|
||||
<div className="flex gap-2">
|
||||
{isEditing ? (
|
||||
<>
|
||||
<Button
|
||||
danger
|
||||
variant="filled"
|
||||
icon={<X className="h-3 w-3" />}
|
||||
iconPosition="start"
|
||||
onClick={handleCancelEdit}
|
||||
disabled={saving}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
variant="filled"
|
||||
icon={saving ? <Spin size="small" /> : <Save className="h-3 w-3" />}
|
||||
onClick={handleSave}
|
||||
disabled={saving}>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button variant="filled" icon={<Edit className="h-3 w-3" />} onClick={handleEdit}>
|
||||
{t('common.edit')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{contentLoading ? (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<Spin size="small" />
|
||||
</div>
|
||||
) : contentError ? (
|
||||
<div className="rounded-md bg-danger-50 p-3 text-danger text-small">{contentError}</div>
|
||||
) : isEditing ? (
|
||||
<Input.TextArea
|
||||
value={editedContent}
|
||||
onChange={(e) => setEditedContent(e.target.value)}
|
||||
autoSize={{ minRows: 20 }}
|
||||
classNames={{
|
||||
textarea: 'font-mono text-tiny'
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<pre className="max-h-96 overflow-auto whitespace-pre-wrap rounded-md bg-default-100 p-3 font-mono text-tiny">
|
||||
{content}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { cn } from '@heroui/react'
|
||||
import EmojiIcon from '@renderer/components/EmojiIcon'
|
||||
import { getAgentTypeLabel } from '@renderer/i18n/label'
|
||||
import type { AgentEntity, AgentSessionEntity } from '@renderer/types'
|
||||
import { cn } from '@renderer/utils'
|
||||
import { Menu, Modal } from 'antd'
|
||||
import type { ReactNode } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
@ -6,8 +6,6 @@ import {
|
||||
WifiOutlined,
|
||||
YuqueOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { Button } from '@heroui/button'
|
||||
import { Switch } from '@heroui/switch'
|
||||
import DividerWithText from '@renderer/components/DividerWithText'
|
||||
import { NutstoreIcon } from '@renderer/components/Icons/NutstoreIcons'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
@ -24,7 +22,7 @@ import { setSkipBackupFile as _setSkipBackupFile } from '@renderer/store/setting
|
||||
import type { AppInfo } from '@renderer/types'
|
||||
import { formatFileSize } from '@renderer/utils'
|
||||
import { occupiedDirs } from '@shared/config/constant'
|
||||
import { Progress, Typography } from 'antd'
|
||||
import { Button, Progress, Switch, Typography } from 'antd'
|
||||
import { FileText, FolderCog, FolderInput, FolderOpen, SaveIcon, Sparkle } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
@ -295,16 +293,11 @@ const DataSettings: FC = () => {
|
||||
<div>
|
||||
<MigrationPathRow style={{ marginTop: '20px', flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Switch
|
||||
defaultSelected={shouldCopyData}
|
||||
onValueChange={(checked) => {
|
||||
shouldCopyData = checked
|
||||
}}
|
||||
size="sm">
|
||||
<span style={{ fontWeight: 'normal', fontSize: '14px' }}>
|
||||
{t('settings.data.app_data.copy_data_option')}
|
||||
</span>
|
||||
</Switch>
|
||||
|
||||
defaultChecked={shouldCopyData}
|
||||
onChange={(checked) => (shouldCopyData = checked)}
|
||||
style={{ marginRight: '8px' }}
|
||||
title={t('settings.data.app_data.copy_data_option')}
|
||||
/>
|
||||
<MigrationPathLabel style={{ fontWeight: 'normal', fontSize: '14px' }}>
|
||||
{t('settings.data.app_data.copy_data_option')}
|
||||
</MigrationPathLabel>
|
||||
@ -614,10 +607,10 @@ const DataSettings: FC = () => {
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle>
|
||||
<HStack gap="5px" justifyContent="space-between">
|
||||
<Button variant="ghost" size="sm" onPress={BackupPopup.show} startContent={<SaveIcon size={14} />}>
|
||||
<Button onClick={BackupPopup.show} icon={<SaveIcon size={14} />}>
|
||||
{t('settings.general.backup.button')}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onPress={RestorePopup.show} startContent={<FolderOpen size={14} />}>
|
||||
<Button onClick={RestorePopup.show} icon={<FolderOpen size={14} />}>
|
||||
{t('settings.general.restore.button')}
|
||||
</Button>
|
||||
</HStack>
|
||||
@ -625,7 +618,7 @@ const DataSettings: FC = () => {
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.backup.skip_file_data_title')}</SettingRowTitle>
|
||||
<Switch isSelected={skipBackupFile} onValueChange={onSkipBackupFilesChange} size="sm" />
|
||||
<Switch checked={skipBackupFile} onChange={onSkipBackupFilesChange} />
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingHelpText>{t('settings.data.backup.skip_file_data_help')}</SettingHelpText>
|
||||
@ -634,11 +627,7 @@ const DataSettings: FC = () => {
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.export_to_phone.title')}</SettingRowTitle>
|
||||
<HStack gap="5px" justifyContent="space-between">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onPress={ExportToPhoneLanPopup.show}
|
||||
startContent={<WifiOutlined />}>
|
||||
<Button onClick={ExportToPhoneLanPopup.show} icon={<WifiOutlined size={14} />}>
|
||||
{t('settings.data.export_to_phone.lan.title')}
|
||||
</Button>
|
||||
</HStack>
|
||||
@ -657,9 +646,7 @@ const DataSettings: FC = () => {
|
||||
</PathText>
|
||||
<StyledIcon onClick={() => handleOpenPath(appInfo?.appDataPath)} style={{ flexShrink: 0 }} />
|
||||
<HStack gap="5px" style={{ marginLeft: '8px' }}>
|
||||
<Button variant="ghost" size="sm" onClick={handleSelectAppDataPath}>
|
||||
{t('settings.data.app_data.select')}
|
||||
</Button>
|
||||
<Button onClick={handleSelectAppDataPath}>{t('settings.data.app_data.select')}</Button>
|
||||
</HStack>
|
||||
</PathRow>
|
||||
</SettingRow>
|
||||
@ -672,7 +659,7 @@ const DataSettings: FC = () => {
|
||||
</PathText>
|
||||
<StyledIcon onClick={() => handleOpenPath(appInfo?.logsPath)} style={{ flexShrink: 0 }} />
|
||||
<HStack gap="5px" style={{ marginLeft: '8px' }}>
|
||||
<Button variant="ghost" size="sm" onClick={() => handleOpenPath(appInfo?.logsPath)}>
|
||||
<Button onClick={() => handleOpenPath(appInfo?.logsPath)}>
|
||||
{t('settings.data.app_logs.button')}
|
||||
</Button>
|
||||
</HStack>
|
||||
@ -682,9 +669,7 @@ const DataSettings: FC = () => {
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.app_knowledge.label')}</SettingRowTitle>
|
||||
<HStack alignItems="center" gap="5px">
|
||||
<Button variant="ghost" size="sm" onClick={handleRemoveAllFiles}>
|
||||
{t('settings.data.app_knowledge.button.delete')}
|
||||
</Button>
|
||||
<Button onClick={handleRemoveAllFiles}>{t('settings.data.app_knowledge.button.delete')}</Button>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
@ -694,16 +679,14 @@ const DataSettings: FC = () => {
|
||||
{cacheSize && <CacheText>({cacheSize}MB)</CacheText>}
|
||||
</SettingRowTitle>
|
||||
<HStack gap="5px">
|
||||
<Button variant="ghost" size="sm" onClick={handleClearCache}>
|
||||
{t('settings.data.clear_cache.button')}
|
||||
</Button>
|
||||
<Button onClick={handleClearCache}>{t('settings.data.clear_cache.button')}</Button>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.general.reset.title')}</SettingRowTitle>
|
||||
<HStack gap="5px">
|
||||
<Button variant="ghost" size="sm" onPress={reset} color="danger">
|
||||
<Button onClick={reset} danger>
|
||||
{t('settings.general.reset.title')}
|
||||
</Button>
|
||||
</HStack>
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { Alert, Skeleton } from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import { ErrorTag } from '@renderer/components/Tags/ErrorTag'
|
||||
import { isMac, isWin } from '@renderer/config/constant'
|
||||
@ -6,7 +5,7 @@ import { useOcrProviders } from '@renderer/hooks/useOcrProvider'
|
||||
import type { ImageOcrProvider, OcrProvider } from '@renderer/types'
|
||||
import { BuiltinOcrProviderIds, isImageOcrProvider } from '@renderer/types'
|
||||
import { getErrorMessage } from '@renderer/utils'
|
||||
import { Select } from 'antd'
|
||||
import { Alert, Select, Skeleton } from 'antd'
|
||||
import { useCallback, useEffect, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useSWRImmutable from 'swr/immutable'
|
||||
@ -70,27 +69,39 @@ const OcrImageSettings = ({ setProvider }: Props) => {
|
||||
<SettingRowTitle>{t('settings.tool.ocr.image_provider')}</SettingRowTitle>
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
{!platformSupport && isSystem && <ErrorTag message={t('settings.tool.ocr.error.not_system')} />}
|
||||
<Skeleton isLoaded={!isLoading}>
|
||||
{!error && (
|
||||
<Select
|
||||
value={imageProvider.id}
|
||||
style={{ width: '200px' }}
|
||||
onChange={(id: string) => setImageProvider(id)}
|
||||
options={options}
|
||||
/>
|
||||
)}
|
||||
{error && (
|
||||
<Alert
|
||||
color="danger"
|
||||
title={t('ocr.error.provider.get_providers')}
|
||||
description={getErrorMessage(error)}
|
||||
/>
|
||||
)}
|
||||
</Skeleton>
|
||||
<OcrProviderSelector
|
||||
isLoading={isLoading}
|
||||
error={error}
|
||||
value={imageProvider.id}
|
||||
options={options}
|
||||
onChange={setImageProvider}
|
||||
/>
|
||||
</div>
|
||||
</SettingRow>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
type OcrProviderSelectorProps = {
|
||||
isLoading: boolean
|
||||
error: any
|
||||
value: string
|
||||
options: Array<{ value: string; label: string }>
|
||||
onChange: (id: string) => void
|
||||
}
|
||||
|
||||
const OcrProviderSelector = ({ isLoading, error, value, options, onChange }: OcrProviderSelectorProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (isLoading) {
|
||||
return <Skeleton.Input active style={{ width: '200px', height: '32px' }} />
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <Alert type="error" message={t('ocr.error.provider.get_providers')} description={getErrorMessage(error)} />
|
||||
}
|
||||
|
||||
return <Select value={value} style={{ width: '200px' }} onChange={onChange} options={options} />
|
||||
}
|
||||
|
||||
export default OcrImageSettings
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
// TODO: Refactor this component to use HeroUI
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useApiServer } from '@renderer/hooks/useApiServer'
|
||||
import type { RootState } from '@renderer/store'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setApiServerApiKey, setApiServerPort } from '@renderer/store/settings'
|
||||
import { formatErrorMessage } from '@renderer/utils/error'
|
||||
import { Button, Input, InputNumber, Tooltip, Typography } from 'antd'
|
||||
import { Alert, Button, Input, InputNumber, Tooltip, Typography } from 'antd'
|
||||
import { Copy, ExternalLink, Play, RotateCcw, Square } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -86,6 +85,10 @@ const ApiServerSettings: FC = () => {
|
||||
)}
|
||||
</HeaderSection>
|
||||
|
||||
{!apiServerRunning && (
|
||||
<Alert type="warning" message={t('agent.warning.enable_server')} style={{ marginBottom: 10 }} showIcon />
|
||||
)}
|
||||
|
||||
{/* Server Control Panel with integrated configuration */}
|
||||
<ServerControlPanel $status={apiServerRunning}>
|
||||
<StatusSection>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user