diff --git a/.agents/skills/react/SKILL.md b/.agents/skills/react/SKILL.md index 29fbee9ac3..3cfe2247fa 100644 --- a/.agents/skills/react/SKILL.md +++ b/.agents/skills/react/SKILL.md @@ -1,6 +1,6 @@ --- name: react -description: "LobeHub React component conventions — styling via `antd-style` `createStaticStyles` + `cssVar.*` (zero-runtime preferred over `createStyles` + `token`), `@lobehub/ui` over antd when both exist, routing via `react-router-dom` (not `next/link`). Use when writing or editing any `.tsx` under `src/**`. Triggers on `createStaticStyles`, `createStyles`, `cssVar`, `antd-style`, `Flexbox`, `Center`, `Select`, `Modal`, `Drawer`, `Button`, `Tooltip`, `DropdownMenu`, `Popover`, `Switch`, `ScrollArea`, `Link`, `useNavigate`, `react-router-dom`, `next/link`, `desktopRouter`, `componentMap.desktop`, `.desktop.tsx`, 'new component', 'new page', 'edit layout', 'add styles', 'zustand selector', '@lobehub/ui', 'antd import'." +description: "LobeHub React component conventions — base-ui (`@lobehub/ui/base-ui`) first for headless primitives (Select, Modal, DropdownMenu, ContextMenu, Popover, ScrollArea, Switch, Toast, FloatingSheet), then `@lobehub/ui` root, antd as last resort; styling via `antd-style` `createStaticStyles` + `cssVar.*` (zero-runtime preferred over `createStyles` + `token`); routing via `react-router-dom` (not `next/link`). Use when writing or editing any `.tsx` under `src/**`. Triggers on `createStaticStyles`, `createStyles`, `cssVar`, `antd-style`, `Flexbox`, `Center`, `Select`, `Modal`, `Drawer`, `Button`, `Tooltip`, `DropdownMenu`, `ContextMenu`, `Popover`, `Switch`, `ScrollArea`, `Toast`, `FloatingSheet`, `Link`, `useNavigate`, `react-router-dom`, `next/link`, `desktopRouter`, `componentMap.desktop`, `.desktop.tsx`, `base-ui`, `@lobehub/ui/base-ui`, 'new component', 'new page', 'edit layout', 'add styles', 'zustand selector', '@lobehub/ui', 'antd import'." user-invocable: false --- @@ -17,22 +17,41 @@ user-invocable: false ## Component Priority 1. **`src/components`** — project-specific reusable components -2. **`@lobehub/ui/base-ui`** — headless primitives (Select, Modal, DropdownMenu, Popover, Switch, ScrollArea…) -3. **`@lobehub/ui`** — higher-level components (ActionIcon, Markdown, DragPage…) -4. **Custom implementation** — last resort; never reach for antd directly +2. **`@lobehub/ui/base-ui`** — headless primitives. **If the component lives here, use it. Do NOT import the same-named root export.** +3. **`@lobehub/ui`** — higher-level / antd-wrapping components (only when no base-ui equivalent) +4. **antd** — only when neither base-ui nor `@lobehub/ui` root provides it +5. **Custom implementation** — true last resort -If unsure about available components, search existing code or check `node_modules/@lobehub/ui/es/index.mjs`. +If unsure about available components, search existing code or check `node_modules/@lobehub/ui/es/index.mjs` and `node_modules/@lobehub/ui/es/base-ui/`. -### Common @lobehub/ui Components +### `@lobehub/ui/base-ui` — always prefer for these -| Category | Components | -| ------------ | ------------------------------------------------------------------------------- | -| General | ActionIcon, ActionIconGroup, Block, Button, Icon | -| Data Display | Avatar, Collapse, Empty, Highlighter, Markdown, Tag, Tooltip | -| Data Entry | CodeEditor, CopyButton, EditableText, Form, FormModal, Input, SearchBar, Select | -| Feedback | Alert, Drawer, Modal | -| Layout | Center, DraggablePanel, Flexbox, Grid, Header, MaskShadow | -| Navigation | Burger, Dropdown, Menu, SideNav, Tabs | +| Component | Import | +| ------------------------------------------ | ------------------------------------------------------------------------------------------------------- | +| `Select` (+ `SelectProps`, `SelectOption`) | `import { Select } from '@lobehub/ui/base-ui';` | +| `Modal` (imperative API) | `import { createModal, confirmModal, useModalContext, type ModalInstance } from '@lobehub/ui/base-ui';` | +| `DropdownMenu` | `import { DropdownMenu } from '@lobehub/ui/base-ui';` | +| `ContextMenu` | `import { ContextMenu } from '@lobehub/ui/base-ui';` | +| `Popover` | `import { Popover } from '@lobehub/ui/base-ui';` | +| `ScrollArea` | `import { ScrollArea } from '@lobehub/ui/base-ui';` | +| `Switch` | `import { Switch } from '@lobehub/ui/base-ui';` | +| `Toast` | `import { Toast } from '@lobehub/ui/base-ui';` | +| `FloatingSheet` | `import { FloatingSheet } from '@lobehub/ui/base-ui';` | + +For Modal specifically, see the dedicated **modal** skill — use the imperative `createModal({ content: … })` pattern over the legacy `` declarative pattern. base-ui has its own `ModalHost` already mounted in `SPAGlobalProvider`. + +> Common slip: `import { Select } from '@lobehub/ui'` looks fine but it's the antd-backed Select. Use base-ui Select. Same for `Modal`, `DropdownMenu`, etc. + +### `@lobehub/ui` root — use when base-ui has no equivalent + +| Category | Components | +| ------------ | ------------------------------------------------------------------------------------- | +| General | ActionIcon, ActionIconGroup, Block, Button, Icon | +| Data Display | Avatar, Collapse, Empty, Highlighter, Markdown, Tag, Tooltip | +| Data Entry | CodeEditor, CopyButton, EditableText, Form, Input, InputPassword, SearchBar, TextArea | +| Feedback | Alert, Drawer | +| Layout | Center, DraggablePanel, Flexbox, Grid, Header, MaskShadow | +| Navigation | Burger, Menu, SideNav, Tabs | ## Layout @@ -85,12 +104,15 @@ errorElement: ; ## Common Mistakes -| Mistake | Fix | -| ----------------------------------------------------------------- | ----------------------------------------------------------------- | -| Using `next/link` in SPA | Use `react-router-dom` `Link` | -| Using antd directly | Use `@lobehub/ui/base-ui` first, then `@lobehub/ui` | -| `createStyles` for static styles | Use `createStaticStyles` + `cssVar` | -| Editing only `desktopRouter.config.tsx` | Must edit both `.tsx` and `.desktop.tsx` | -| Using `margin` for flex spacing | Use `gap` prop on Flexbox | -| Accessing zustand store without selector | Use selectors to access store data (see zustand skill) | -| Text or icon-text actions built with `Flexbox`/`Text` + `onClick` | Use `Button type={'text'} size={'small'}` with `icon` when needed | +| Mistake | Fix | +| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | +| Using `next/link` in SPA | Use `react-router-dom` `Link` | +| Using antd directly | Use `@lobehub/ui/base-ui` first, then `@lobehub/ui` | +| `import { Select } from '@lobehub/ui'` | `import { Select } from '@lobehub/ui/base-ui'` | +| `import { Modal } from '@lobehub/ui'` + `` declarative | `createModal` / `confirmModal` from `@lobehub/ui/base-ui` (see modal skill) | +| `import { DropdownMenu/Popover/Switch } from '@lobehub/ui'` | Import same name from `@lobehub/ui/base-ui` instead | +| `createStyles` for static styles | Use `createStaticStyles` + `cssVar` | +| Editing only `desktopRouter.config.tsx` | Must edit both `.tsx` and `.desktop.tsx` | +| Using `margin` for flex spacing | Use `gap` prop on Flexbox | +| Accessing zustand store without selector | Use selectors to access store data (see zustand skill) | +| Text or icon-text actions built with `Flexbox`/`Text` + `onClick` | Use `Button type={'text'} size={'small'}` with `icon` when needed | diff --git a/AGENTS.md b/AGENTS.md index 2edaa312c0..ffd256e92f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,6 +7,7 @@ Guidelines for using AI coding agents in this LobeHub repository. - Next.js 16 + React 19 + TypeScript - SPA inside Next.js with `react-router-dom` - `@lobehub/ui`, antd for components; antd-style for CSS-in-JS — **prefer `createStaticStyles` with `cssVar.*`** (zero-runtime); only fall back to `createStyles` + `token` when styles genuinely need runtime computation. See `.cursor/docs/createStaticStyles_migration_guide.md`. +- **Component priority**: `@lobehub/ui/base-ui` (headless primitives) **first**, then `@lobehub/ui` root, then antd as last resort. When the component exists in base-ui, use it — never reach for the root or antd counterpart. Base-ui covers `Select`, `Modal` / `createModal` / `confirmModal`, `DropdownMenu`, `ContextMenu`, `Popover`, `ScrollArea`, `Switch`, `Toast`, `FloatingSheet`. Prefer `@lobehub/ui/base-ui` for new code and migrate root-package call sites opportunistically. - react-i18next for i18n; zustand for state management - SWR for data fetching; TRPC for type-safe backend - Drizzle ORM with PostgreSQL; Vitest for testing diff --git a/e2e/src/steps/agent/conversation-mgmt.steps.ts b/e2e/src/steps/agent/conversation-mgmt.steps.ts index 8dd2ec1b6b..3bd099f008 100644 --- a/e2e/src/steps/agent/conversation-mgmt.steps.ts +++ b/e2e/src/steps/agent/conversation-mgmt.steps.ts @@ -427,10 +427,10 @@ When('用户选择删除选项', async function (this: CustomWorld) { When('用户确认删除', async function (this: CustomWorld) { console.log(' 📍 Step: 确认删除...'); - // A confirmation modal should appear - const confirmButton = this.page.locator('.ant-modal-confirm-btns button.ant-btn-dangerous'); + const confirmButton = this.page + .getByRole('dialog') + .getByRole('button', { name: /^(ok|delete|删除|确认|确定)$/i }); - // Wait for modal to appear await expect(confirmButton).toBeVisible({ timeout: 5000 }); await confirmButton.click(); diff --git a/e2e/src/steps/home/sidebarAgent.steps.ts b/e2e/src/steps/home/sidebarAgent.steps.ts index f1ddccf845..3b7996333c 100644 --- a/e2e/src/steps/home/sidebarAgent.steps.ts +++ b/e2e/src/steps/home/sidebarAgent.steps.ts @@ -294,7 +294,9 @@ When('用户在菜单中选择删除', async function (this: CustomWorld) { When('用户在弹窗中确认删除', async function (this: CustomWorld) { console.log(' 📍 Step: 确认删除...'); - const confirmButton = this.page.locator('.ant-modal-confirm-btns button.ant-btn-dangerous'); + const confirmButton = this.page + .getByRole('dialog') + .getByRole('button', { name: /^(ok|delete|删除|确认|确定)$/i }); await expect(confirmButton).toBeVisible({ timeout: 5000 }); await confirmButton.click(); await this.page.waitForTimeout(500); diff --git a/src/components/DotsLoading/index.tsx b/src/components/DotsLoading/index.tsx index 1c6b78cd1e..ae3e6c93fc 100644 --- a/src/components/DotsLoading/index.tsx +++ b/src/components/DotsLoading/index.tsx @@ -1,36 +1,32 @@ -import { createStyles, keyframes } from 'antd-style'; +import { createStaticStyles, cssVar, cx } from 'antd-style'; import { type CSSProperties, memo } from 'react'; -const fade = keyframes` - 0%, 100% { - opacity: 0.3; - } - 50% { - opacity: 1; - } -`; - -interface StyleParams { - color?: string; - gap: number; - size: number; -} - -const useStyles = createStyles(({ css, token }, { size, gap, color }: StyleParams) => ({ +const styles = createStaticStyles(({ css }) => ({ container: css` display: inline-flex; flex-direction: row; - gap: ${gap}px; + gap: var(--dots-loading-gap); align-items: center; `, dot: css` - width: ${size}px; - height: ${size}px; + width: var(--dots-loading-size); + height: var(--dots-loading-size); border-radius: 50%; - background-color: ${color || token.colorTextSecondary}; + background-color: var(--dots-loading-color); - animation: ${fade} 1.2s ease-in-out infinite; + animation: dots-loading-fade 1.2s ease-in-out infinite; + + @keyframes dots-loading-fade { + 0%, + 100% { + opacity: 0.3; + } + + 50% { + opacity: 1; + } + } `, })); @@ -46,12 +42,17 @@ interface DotsLoadingProps extends StyleArgs { } const DotsLoading = memo(({ size = 4, gap = 3, color, className, style }) => { - const { styles: s, cx } = useStyles({ color, gap, size }); + const cssVars = { + '--dots-loading-color': color || cssVar.colorTextSecondary, + '--dots-loading-gap': `${gap}px`, + '--dots-loading-size': `${size}px`, + } as CSSProperties; + return ( -
-
-
-
+
+
+
+
); }); diff --git a/src/components/InvalidAPIKey/__tests__/ComfyUIForm.test.tsx b/src/components/InvalidAPIKey/__tests__/ComfyUIForm.test.tsx index 58662b068e..0471910c0b 100644 --- a/src/components/InvalidAPIKey/__tests__/ComfyUIForm.test.tsx +++ b/src/components/InvalidAPIKey/__tests__/ComfyUIForm.test.tsx @@ -30,7 +30,6 @@ vi.mock('antd-style', async (importOriginal) => { cssVar: {}, }), ), - createStyles: vi.fn(() => () => ({ styles: {} })), useTheme: () => ({ colorTextSecondary: '#999', }), diff --git a/src/features/AgentSetting/AgentDocuments/index.tsx b/src/features/AgentSetting/AgentDocuments/index.tsx index e9dec645ee..f31a5f08bd 100644 --- a/src/features/AgentSetting/AgentDocuments/index.tsx +++ b/src/features/AgentSetting/AgentDocuments/index.tsx @@ -1,7 +1,8 @@ 'use client'; +import { confirmModal, Select } from '@lobehub/ui/base-ui'; import type { TableColumnsType } from 'antd'; -import { App, Button, Popconfirm, Select, Space, Table, Tag, Typography } from 'antd'; +import { App, Button, Popconfirm, Space, Table, Tag, Typography } from 'antd'; import { memo, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -28,7 +29,7 @@ const FILE_PREVIEW_LIMIT = 5; const AgentDocuments = memo(() => { const { t } = useTranslation(['setting', 'common']); - const { message, modal } = App.useApp(); + const { message } = App.useApp(); const agentId = useAgentStore((s) => s.activeAgentId); const [templateId, setTemplateId] = useState(DEFAULT_TEMPLATE_ID); const [isInitializingTemplate, setIsInitializingTemplate] = useState(false); @@ -160,7 +161,7 @@ const AgentDocuments = memo(() => { const previewFilenames = overwrittenFilenames.slice(0, FILE_PREVIEW_LIMIT); const remainingCount = overwrittenCount - previewFilenames.length; - modal.confirm({ + confirmModal({ content: ( diff --git a/src/features/AgentTasks/AgentTaskDetail/CommentCard.tsx b/src/features/AgentTasks/AgentTaskDetail/CommentCard.tsx index 78bee3bf80..66026aa531 100644 --- a/src/features/AgentTasks/AgentTaskDetail/CommentCard.tsx +++ b/src/features/AgentTasks/AgentTaskDetail/CommentCard.tsx @@ -12,7 +12,7 @@ import { Markdown, Text, } from '@lobehub/ui'; -import { App } from 'antd'; +import { confirmModal } from '@lobehub/ui/base-ui'; import { cssVar } from 'antd-style'; import { MessageCircle, MoreHorizontal, Pencil, Trash } from 'lucide-react'; import { memo, useCallback, useMemo, useState } from 'react'; @@ -29,7 +29,6 @@ interface CommentCardProps { const CommentCard = memo(({ activity }) => { const { t } = useTranslation('chat'); - const { modal } = App.useApp(); const deleteComment = useTaskStore((s) => s.deleteComment); const updateComment = useTaskStore((s) => s.updateComment); @@ -64,16 +63,14 @@ const CommentCard = memo(({ activity }) => { const handleDelete = useCallback(() => { if (!commentId) return; - modal.confirm({ - centered: true, + confirmModal({ content: t('taskDetail.comment.deleteConfirm.content'), okButtonProps: { danger: true }, okText: t('taskDetail.comment.deleteConfirm.ok'), onOk: () => deleteComment(commentId), title: t('taskDetail.comment.deleteConfirm.title'), - type: 'error', }); - }, [commentId, deleteComment, modal, t]); + }, [commentId, deleteComment, t]); const menuItems = useMemo( () => [ diff --git a/src/features/AgentTasks/AgentTaskDetail/TaskArtifacts.tsx b/src/features/AgentTasks/AgentTaskDetail/TaskArtifacts.tsx index 78c97a62fa..af082c2cac 100644 --- a/src/features/AgentTasks/AgentTaskDetail/TaskArtifacts.tsx +++ b/src/features/AgentTasks/AgentTaskDetail/TaskArtifacts.tsx @@ -9,7 +9,7 @@ import { Tag, Text, } from '@lobehub/ui'; -import { App } from 'antd'; +import { confirmModal } from '@lobehub/ui/base-ui'; import { cssVar } from 'antd-style'; import { FileTextIcon, MoreHorizontal, Package, Trash } from 'lucide-react'; import { memo, useCallback, useMemo, useState } from 'react'; @@ -30,7 +30,6 @@ const flattenWorkspace = (nodes: TaskDetailWorkspaceNode[]): TaskDetailWorkspace const ArtifactCard = memo<{ node: TaskDetailWorkspaceNode }>(({ node }) => { const { t } = useTranslation('chat'); - const { modal } = App.useApp(); const openDocumentPreview = useDocumentStore((s) => s.openDocumentPreview); const unpinDocument = useTaskStore((s) => s.unpinDocument); const activeTaskId = useTaskStore(taskDetailSelectors.activeTaskId); @@ -41,16 +40,14 @@ const ArtifactCard = memo<{ node: TaskDetailWorkspaceNode }>(({ node }) => { const handleDelete = useCallback(() => { const taskId = node.sourceTaskId ?? activeTaskId; if (!taskId) return; - modal.confirm({ - centered: true, + confirmModal({ content: t('taskDetail.artifactMenu.deleteConfirm.content'), okButtonProps: { danger: true }, okText: t('taskDetail.artifactMenu.deleteConfirm.ok'), onOk: () => unpinDocument(taskId, node.documentId), title: t('taskDetail.artifactMenu.deleteConfirm.title'), - type: 'error', }); - }, [activeTaskId, modal, node.documentId, node.sourceTaskId, t, unpinDocument]); + }, [activeTaskId, node.documentId, node.sourceTaskId, t, unpinDocument]); const menuItems = useMemo( () => [ diff --git a/src/features/AgentTasks/AgentTaskDetail/TaskBriefCard.tsx b/src/features/AgentTasks/AgentTaskDetail/TaskBriefCard.tsx index 3393ff3786..87726437bc 100644 --- a/src/features/AgentTasks/AgentTaskDetail/TaskBriefCard.tsx +++ b/src/features/AgentTasks/AgentTaskDetail/TaskBriefCard.tsx @@ -7,7 +7,7 @@ import { Icon, Text, } from '@lobehub/ui'; -import { App } from 'antd'; +import { confirmModal } from '@lobehub/ui/base-ui'; import { cssVar } from 'antd-style'; import { Check, ChevronDownIcon, ChevronUpIcon, MoreHorizontal, Trash } from 'lucide-react'; import { memo, useCallback, useMemo, useState } from 'react'; @@ -32,15 +32,13 @@ interface TaskBriefCardProps { const TaskBriefCard = memo( ({ brief, onAfterResolve, onAfterAddComment, onAfterDelete }) => { const { t } = useTranslation('home'); - const { modal } = App.useApp(); const deleteBrief = useBriefStore((s) => s.deleteBrief); const isResolved = Boolean(brief.resolvedAction); const [expanded, setExpanded] = useState(false); const showFull = !isResolved || expanded; const handleDelete = useCallback(() => { - modal.confirm({ - centered: true, + confirmModal({ content: t('brief.deleteConfirm.content'), okButtonProps: { danger: true }, okText: t('brief.deleteConfirm.ok'), @@ -49,9 +47,8 @@ const TaskBriefCard = memo( await onAfterDelete?.(); }, title: t('brief.deleteConfirm.title'), - type: 'error', }); - }, [brief.id, deleteBrief, modal, onAfterDelete, t]); + }, [brief.id, deleteBrief, onAfterDelete, t]); const menuItems = useMemo( () => [ diff --git a/src/features/AgentTasks/AgentTaskDetail/TaskDetailHeaderActions.tsx b/src/features/AgentTasks/AgentTaskDetail/TaskDetailHeaderActions.tsx index 4c83d8e61f..cd7c6e1514 100644 --- a/src/features/AgentTasks/AgentTaskDetail/TaskDetailHeaderActions.tsx +++ b/src/features/AgentTasks/AgentTaskDetail/TaskDetailHeaderActions.tsx @@ -1,4 +1,5 @@ import { ActionIcon, copyToClipboard, type DropdownItem, DropdownMenu, Icon } from '@lobehub/ui'; +import { confirmModal } from '@lobehub/ui/base-ui'; import { App } from 'antd'; import { CopyIcon, LinkIcon, MoreHorizontal, Trash } from 'lucide-react'; import { memo, useCallback, useMemo } from 'react'; @@ -13,7 +14,7 @@ import { taskDetailPath } from '../shared/taskDetailPath'; const TaskDetailHeaderActions = memo(() => { const { t } = useTranslation(['chat', 'common']); - const { modal, message } = App.useApp(); + const { message } = App.useApp(); const navigate = useNavigate(); const appOrigin = useAppOrigin(); const taskId = useTaskStore(taskDetailSelectors.activeTaskId); @@ -22,8 +23,7 @@ const TaskDetailHeaderActions = memo(() => { const triggerDelete = useCallback(() => { if (!taskId) return; - modal.confirm({ - centered: true, + confirmModal({ content: t('taskDetail.deleteConfirm.content'), okButtonProps: { danger: true }, okText: t('taskDetail.deleteConfirm.ok'), @@ -32,9 +32,8 @@ const TaskDetailHeaderActions = memo(() => { navigate('/tasks'); }, title: t('taskDetail.deleteConfirm.title'), - type: 'error', }); - }, [taskId, modal, t, deleteTask, navigate]); + }, [taskId, t, deleteTask, navigate]); const menuItems = useMemo(() => { if (!taskId) return []; diff --git a/src/features/AgentTasks/AgentTaskDetail/TaskSubtasks.test.tsx b/src/features/AgentTasks/AgentTaskDetail/TaskSubtasks.test.tsx index 85694b0a42..09869d43ae 100644 --- a/src/features/AgentTasks/AgentTaskDetail/TaskSubtasks.test.tsx +++ b/src/features/AgentTasks/AgentTaskDetail/TaskSubtasks.test.tsx @@ -81,6 +81,10 @@ vi.mock('antd-style', () => ({ }, })); +vi.mock('@lobehub/ui/base-ui', () => ({ + confirmModal: vi.fn(), +})); + vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string) => key }), })); diff --git a/src/features/AgentTasks/AgentTaskDetail/TaskSubtasks.tsx b/src/features/AgentTasks/AgentTaskDetail/TaskSubtasks.tsx index 41e5682a09..daa67d04b7 100644 --- a/src/features/AgentTasks/AgentTaskDetail/TaskSubtasks.tsx +++ b/src/features/AgentTasks/AgentTaskDetail/TaskSubtasks.tsx @@ -1,5 +1,6 @@ import type { TaskDetailSubtask } from '@lobechat/types'; import { ActionIcon, Block, Flexbox, Icon, showContextMenu, Text } from '@lobehub/ui'; +import { confirmModal } from '@lobehub/ui/base-ui'; import { App, ConfigProvider, Tree } from 'antd'; import type { DataNode } from 'antd/es/tree'; import { cssVar } from 'antd-style'; @@ -127,7 +128,7 @@ const toTreeData = (tree: TaskTreeNode[]): DataNode[] => { const TaskSubtasks = memo(() => { const { t } = useTranslation('chat'); - const { message, modal } = App.useApp(); + const { message } = App.useApp(); const navigate = useNavigate(); const agentId = useTaskStore(taskDetailSelectors.activeTaskAgentId); const subtasks = useTaskStore(taskDetailSelectors.activeTaskSubtasks); @@ -210,9 +211,8 @@ const TaskSubtasks = memo(() => { } const canRun = plan.totalRunnable > 0; - modal.confirm({ + confirmModal({ cancelText: t('taskDetail.runAll.cancel'), - centered: true, content: , okButtonProps: canRun ? undefined : { disabled: true }, okText: t('taskDetail.runAll.confirm', { count: plan.totalRunnable }), @@ -234,7 +234,6 @@ const TaskSubtasks = memo(() => { } }, title: t('taskDetail.runAll.title'), - width: 520, }); } catch (error) { console.error('[TaskSubtasks] Failed to plan subtasks:', error); @@ -242,7 +241,7 @@ const TaskSubtasks = memo(() => { } finally { setIsPlanning(false); } - }, [taskId, isPlanning, message, modal, t, runReadySubtasks]); + }, [taskId, isPlanning, message, t, runReadySubtasks]); if (!taskId) return null; diff --git a/src/features/AgentTasks/features/useTaskItemContextMenu.tsx b/src/features/AgentTasks/features/useTaskItemContextMenu.tsx index 89c248948e..27dc66e437 100644 --- a/src/features/AgentTasks/features/useTaskItemContextMenu.tsx +++ b/src/features/AgentTasks/features/useTaskItemContextMenu.tsx @@ -7,6 +7,7 @@ import { Icon, type MenuInfo, } from '@lobehub/ui'; +import { confirmModal } from '@lobehub/ui/base-ui'; import { App } from 'antd'; import { cssVar } from 'antd-style'; import { @@ -55,7 +56,7 @@ export interface TaskContextMenuActions { export const useTaskContextMenuActions = (): TaskContextMenuActions => { const { t } = useTranslation(['chat', 'common']); - const { modal, message } = App.useApp(); + const { message } = App.useApp(); const appOrigin = useAppOrigin(); const updateTaskStatus = useTaskStore((s) => s.updateTaskStatus); @@ -72,8 +73,7 @@ export const useTaskContextMenuActions = (): TaskContextMenuActions => { return useMemo(() => { const triggerDelete = (identifier: string) => { - modal.confirm({ - centered: true, + confirmModal({ content: t('taskDetail.deleteConfirm.content'), okButtonProps: { danger: true }, okText: t('taskDetail.deleteConfirm.ok'), @@ -81,7 +81,6 @@ export const useTaskContextMenuActions = (): TaskContextMenuActions => { await deleteTask(identifier); }, title: t('taskDetail.deleteConfirm.title'), - type: 'error', }); }; @@ -277,7 +276,6 @@ export const useTaskContextMenuActions = (): TaskContextMenuActions => { return { buildItems, installKeyboardHandlers }; }, [ - modal, message, t, appOrigin, diff --git a/src/features/AgentTopicManager/BulkActionBar.tsx b/src/features/AgentTopicManager/BulkActionBar.tsx index c519af2c8c..906c89eb5c 100644 --- a/src/features/AgentTopicManager/BulkActionBar.tsx +++ b/src/features/AgentTopicManager/BulkActionBar.tsx @@ -1,7 +1,7 @@ 'use client'; import { ActionIcon, Flexbox, Text } from '@lobehub/ui'; -import { App } from 'antd'; +import { confirmModal } from '@lobehub/ui/base-ui'; import { createStaticStyles, cssVar } from 'antd-style'; import { Archive, Star, Trash2, X } from 'lucide-react'; import { memo, useCallback } from 'react'; @@ -44,7 +44,6 @@ const styles = createStaticStyles(({ css }) => ({ const BulkActionBar = memo(() => { const { t } = useTranslation('topic'); - const { modal } = App.useApp(); const selectedIds = useTopicsViewStore((s) => s.selectedIds); const exitSelectMode = useTopicsViewStore((s) => s.exitSelectMode); @@ -68,7 +67,7 @@ const BulkActionBar = memo(() => { }, [selectedIds, updateTopicStatus, exitSelectMode]); const handleBatchDelete = useCallback(() => { - modal.confirm({ + confirmModal({ content: t('management.bulk.deleteConfirm', { count: selectedIds.length }), okButtonProps: { danger: true }, okText: t('management.bulk.delete'), @@ -82,7 +81,7 @@ const BulkActionBar = memo(() => { }, title: t('management.bulk.deleteTitle'), }); - }, [selectedIds, modal, t, removeTopic, exitSelectMode]); + }, [selectedIds, t, removeTopic, exitSelectMode]); if (selectedIds.length === 0) return null; diff --git a/src/features/AgentTopicManager/ToolbarActions.tsx b/src/features/AgentTopicManager/ToolbarActions.tsx index fbc4e8b58c..61467aa719 100644 --- a/src/features/AgentTopicManager/ToolbarActions.tsx +++ b/src/features/AgentTopicManager/ToolbarActions.tsx @@ -1,6 +1,7 @@ 'use client'; import { ActionIcon, type DropdownItem, DropdownMenu } from '@lobehub/ui'; +import { confirmModal } from '@lobehub/ui/base-ui'; import { App } from 'antd'; import { Archive, MoreHorizontal } from 'lucide-react'; import { memo, useCallback, useMemo } from 'react'; @@ -13,7 +14,7 @@ const THREE_MONTHS_MS = 90 * 24 * 60 * 60 * 1000; const ToolbarActions = memo(() => { const { t } = useTranslation('topic'); - const { modal, message } = App.useApp(); + const { message } = App.useApp(); // Operate on the management page's own bucket — not the sidebar's — since // the management view is the one the user is acting on here. @@ -34,7 +35,7 @@ const ToolbarActions = memo(() => { return; } - modal.confirm({ + confirmModal({ content: t('management.actionsMenu.archiveStale.confirm', { count: stale.length }), okText: t('management.actionsMenu.archiveStale.confirmOk'), onOk: async () => { @@ -47,7 +48,7 @@ const ToolbarActions = memo(() => { }, title: t('management.actionsMenu.archiveStale.title'), }); - }, [topics, updateTopicStatus, modal, message, t]); + }, [topics, updateTopicStatus, message, t]); const items: DropdownItem[] = useMemo( () => [ diff --git a/src/features/Conversation/Messages/AssistantGroup/Tool/Detail/Intervention/Fallback.tsx b/src/features/Conversation/Messages/AssistantGroup/Tool/Detail/Intervention/Fallback.tsx index 26eaf3d9b0..021f68a561 100644 --- a/src/features/Conversation/Messages/AssistantGroup/Tool/Detail/Intervention/Fallback.tsx +++ b/src/features/Conversation/Messages/AssistantGroup/Tool/Detail/Intervention/Fallback.tsx @@ -50,6 +50,7 @@ const styles = createStaticStyles(({ css, cssVar }) => ({ margin-block-start: -4px; padding-block-end: 8px; padding-inline: 16px; + font-size: ${cssVar.fontSizeSM}; line-height: 1.45; color: ${cssVar.colorTextSecondary}; diff --git a/src/features/Conversation/Messages/CompressedGroup/index.tsx b/src/features/Conversation/Messages/CompressedGroup/index.tsx index 9e773a1a68..749902c36a 100644 --- a/src/features/Conversation/Messages/CompressedGroup/index.tsx +++ b/src/features/Conversation/Messages/CompressedGroup/index.tsx @@ -10,7 +10,7 @@ import { Tabs, type TabsProps, } from '@lobehub/ui'; -import { App } from 'antd'; +import { confirmModal } from '@lobehub/ui/base-ui'; import { createStaticStyles, cx } from 'antd-style'; import isEqual from 'fast-deep-equal'; import { ChevronDown, ChevronUp, History, Sparkles, Undo2 } from 'lucide-react'; @@ -68,7 +68,6 @@ export interface CompressedGroupMessageProps { const CompressedGroupMessage = memo(({ id }) => { const { t } = useTranslation('chat'); - const { modal } = App.useApp(); const [activeTab, setActiveTab] = useState(() => getStoredTab(id)); const handleTabChange = useCallback( @@ -86,13 +85,12 @@ const CompressedGroupMessage = memo(({ id }) => { const cancelCompression = useConversationStore((s) => s.cancelCompression); const handleCancelCompression = useCallback(() => { - modal.confirm({ - centered: true, + confirmModal({ content: t('compression.cancelConfirm'), onOk: () => cancelCompression(id), title: t('compression.cancel'), }); - }, [id, cancelCompression, modal, t]); + }, [id, cancelCompression, t]); const content = message?.content; const rawCompressedMessages = (message as UIChatMessage)?.compressedMessages; diff --git a/src/features/CreatePlatformAgent/index.tsx b/src/features/CreatePlatformAgent/index.tsx index 0ea93ab3bb..deba085395 100644 --- a/src/features/CreatePlatformAgent/index.tsx +++ b/src/features/CreatePlatformAgent/index.tsx @@ -5,8 +5,9 @@ import { type RemoteHeterogeneousAgentType, } from '@lobechat/heterogeneous-agents'; import { Button, Flexbox, Icon } from '@lobehub/ui'; -import { Alert, Input, Modal, Select, Steps, Tag, Typography } from 'antd'; -import { createStyles } from 'antd-style'; +import { Select } from '@lobehub/ui/base-ui'; +import { Alert, Input, Modal, Steps, Tag, Typography } from 'antd'; +import { createStaticStyles, cssVar } from 'antd-style'; import { BotIcon, CheckCircle2, @@ -23,7 +24,7 @@ import { lambdaClient, lambdaQuery } from '@/libs/trpc/client'; import { useAgentStore } from '@/store/agent'; import { useHomeStore } from '@/store/home'; -const useStyles = createStyles(({ css, token }) => ({ +const styles = createStaticStyles(({ css }) => ({ avatarPreview: css` display: flex; align-items: center; @@ -31,12 +32,12 @@ const useStyles = createStyles(({ css, token }) => ({ width: 48px; height: 48px; - border-radius: ${token.borderRadiusLG}px; + border-radius: ${cssVar.borderRadiusLG}; font-size: 28px; line-height: 1; - background: ${token.colorFillSecondary}; + background: ${cssVar.colorFillSecondary}; `, deviceItem: css` display: flex; @@ -53,20 +54,20 @@ const useStyles = createStyles(({ css, token }) => ({ padding-block: 12px; padding-inline: 16px; - border: 1.5px solid ${token.colorBorderSecondary}; - border-radius: ${token.borderRadiusLG}px; + border: 1.5px solid ${cssVar.colorBorderSecondary}; + border-radius: ${cssVar.borderRadiusLG}; - background: ${token.colorBgContainer}; + background: ${cssVar.colorBgContainer}; transition: border-color 0.2s; &:hover { - border-color: ${token.colorPrimary}; + border-color: ${cssVar.colorPrimary}; } &[data-selected='true'] { - border-color: ${token.colorPrimary}; - background: ${token.colorPrimaryBg}; + border-color: ${cssVar.colorPrimary}; + background: ${cssVar.colorPrimaryBg}; } &[data-disabled='true'] { @@ -74,18 +75,18 @@ const useStyles = createStyles(({ css, token }) => ({ opacity: 0.5; &:hover { - border-color: ${token.colorBorderSecondary}; + border-color: ${cssVar.colorBorderSecondary}; } } `, platformDesc: css` font-size: 13px; - color: ${token.colorTextSecondary}; + color: ${cssVar.colorTextSecondary}; `, platformName: css` font-size: 15px; font-weight: 500; - color: ${token.colorText}; + color: ${cssVar.colorText}; `, })); @@ -104,7 +105,6 @@ interface CreatePlatformAgentModalProps { const CreatePlatformAgentModal = memo( ({ open, onClose, groupId }) => { const { t } = useTranslation('chat'); - const { styles } = useStyles(); const navigate = useNavigate(); const storeCreateAgent = useAgentStore((s) => s.createAgent); const refreshAgentList = useHomeStore((s) => s.refreshAgentList); diff --git a/src/features/EditorModal/index.tsx b/src/features/EditorModal/index.tsx index b6528c7018..8d8bb82794 100644 --- a/src/features/EditorModal/index.tsx +++ b/src/features/EditorModal/index.tsx @@ -1,12 +1,11 @@ import { useEditor } from '@lobehub/editor/react'; -import { type ModalProps } from '@lobehub/ui'; -import { createRawModal, Modal } from '@lobehub/ui'; +import { Modal, type ModalComponentProps } from '@lobehub/ui/base-ui'; import { memo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import EditorCanvas from './EditorCanvas'; -interface EditorModalProps extends ModalProps { +interface EditorModalProps extends ModalComponentProps { editorData?: unknown; onConfirm?: (value: string, editorData?: unknown) => Promise; value?: string; @@ -47,5 +46,3 @@ export const EditorModal = memo( ); }, ); - -export const createEditorModal = (props: EditorModalProps) => createRawModal(EditorModal, props); diff --git a/src/features/Messenger/AgentSelect.tsx b/src/features/Messenger/AgentSelect.tsx index 42d1aca6fe..dc58cac7f5 100644 --- a/src/features/Messenger/AgentSelect.tsx +++ b/src/features/Messenger/AgentSelect.tsx @@ -1,6 +1,7 @@ 'use client'; -import { Avatar, Flexbox, Select, type SelectProps, Text } from '@lobehub/ui'; +import { Avatar, Flexbox, Text } from '@lobehub/ui'; +import { Select, type SelectProps } from '@lobehub/ui/base-ui'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import useSWR from 'swr'; @@ -8,21 +9,11 @@ import useSWR from 'swr'; import { DEFAULT_AVATAR } from '@/const/meta'; import { messengerService } from '@/services/messenger'; -interface AgentSelectProps extends Omit< - SelectProps, - 'options' | 'showSearch' | 'optionFilterProp' | 'value' | 'onChange' -> { +interface AgentSelectProps extends Omit, 'options' | 'value' | 'onChange'> { onChange?: (agentId: string | undefined) => void; value?: string; } -/** - * Shared agent picker used wherever the messenger feature asks the user to - * pick which agent receives messages. Single source of truth for the option - * shape (avatar + title, locale-aware fallback) so verify-im and the Settings - * panel render identically. Fetches `messenger.listAgentsForBinding` (which - * already pins LobeAI to the top and matches the bot's `/agents` ordering). - */ const AgentSelect = memo(({ value, onChange, ...rest }) => { const { t: tCommon } = useTranslation('common'); const agentsSWR = useSWR('messenger:agentsForBinding', () => @@ -36,7 +27,7 @@ const AgentSelect = memo(({ value, onChange, ...rest }) => { const title = agent.title || defaultAgentTitle; return { label: ( - + (({ value, onChange, ...rest }) => { {title} ), - searchValue: title, title, value: agent.id, }; @@ -55,10 +45,10 @@ const AgentSelect = memo(({ value, onChange, ...rest }) => { return ( + + + + + + + + + + ); +}; + +export default ApiKeyModalContent; diff --git a/src/routes/(main)/settings/apikey/features/ApiKeyModal/index.tsx b/src/routes/(main)/settings/apikey/features/ApiKeyModal/index.tsx index 313fe19bc4..fc94ef0752 100644 --- a/src/routes/(main)/settings/apikey/features/ApiKeyModal/index.tsx +++ b/src/routes/(main)/settings/apikey/features/ApiKeyModal/index.tsx @@ -1,58 +1,18 @@ -import { FormModal, Input } from '@lobehub/ui'; -import { type Dayjs } from 'dayjs'; -import { type FC } from 'react'; -import { useTranslation } from 'react-i18next'; +'use client'; -import { type CreateApiKeyParams } from '@/types/apiKey'; +import { createModal, type ModalInstance } from '@lobehub/ui/base-ui'; +import { t } from 'i18next'; -import ApiKeyDatePicker from '../ApiKeyDatePicker'; +import ApiKeyModalContent, { type ApiKeyModalContentProps } from './Content'; -interface ApiKeyModalProps { - onCancel: () => void; - onOk: (values: CreateApiKeyParams) => void; - open: boolean; - submitLoading?: boolean; -} - -type FormValues = Omit & { - expiresAt: Dayjs | null; -}; - -const ApiKeyModal: FC = ({ open, onCancel, onOk, submitLoading }) => { - const { t } = useTranslation('auth'); - - return ( - , - label: t('apikey.form.fields.name.label'), - name: 'name', - rules: [{ required: true }], - }, - { - children: , - label: t('apikey.form.fields.expiresAt.label'), - name: 'expiresAt', - }, - ]} - onCancel={onCancel} - onFinish={(values: FormValues) => { - onOk({ - ...values, - expiresAt: values.expiresAt ? values.expiresAt.toDate() : null, - } satisfies CreateApiKeyParams); - }} - /> - ); -}; - -export default ApiKeyModal; +export const createApiKeyModal = (props: ApiKeyModalContentProps): ModalInstance => + createModal({ + content: , + footer: null, + maskClosable: true, + styles: { + content: { paddingBlock: 16, paddingInline: 24 }, + }, + title: t('apikey.form.title', { ns: 'auth' }), + width: 'min(90vw, 560px)', + }); diff --git a/src/routes/(main)/settings/apikey/features/index.ts b/src/routes/(main)/settings/apikey/features/index.ts index 9f4cee876d..7ff4155b93 100644 --- a/src/routes/(main)/settings/apikey/features/index.ts +++ b/src/routes/(main)/settings/apikey/features/index.ts @@ -1,3 +1,3 @@ export { default as ApiKeyDisplay } from './ApiKeyDisplay'; -export { default as ApiKeyModal } from './ApiKeyModal'; +export { createApiKeyModal } from './ApiKeyModal'; export { default as EditableCell } from './EditableCell'; diff --git a/src/routes/(main)/settings/creds/features/CreateCredModal/Content.tsx b/src/routes/(main)/settings/creds/features/CreateCredModal/Content.tsx new file mode 100644 index 0000000000..363fc19d75 --- /dev/null +++ b/src/routes/(main)/settings/creds/features/CreateCredModal/Content.tsx @@ -0,0 +1,81 @@ +'use client'; + +import { type CredType } from '@lobechat/types'; +import { useModalContext } from '@lobehub/ui/base-ui'; +import { Steps } from 'antd'; +import { createStaticStyles } from 'antd-style'; +import { type FC, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import CredTypeSelector from './CredTypeSelector'; +import FileCredForm from './FileCredForm'; +import KVCredForm from './KVCredForm'; +import OAuthCredForm from './OAuthCredForm'; + +const styles = createStaticStyles(({ css }) => ({ + steps: css` + margin-block-end: 24px; + `, +})); + +export interface CreateCredModalContentProps { + onSuccess?: () => void; +} + +const CreateCredModalContent: FC = ({ onSuccess }) => { + const { t } = useTranslation('setting'); + const { close } = useModalContext(); + const [step, setStep] = useState(0); + const [credType, setCredType] = useState(null); + + const handleTypeSelect = (type: CredType) => { + setCredType(type); + setStep(1); + }; + + const handleBack = () => { + setStep(0); + setCredType(null); + }; + + const handleSuccess = () => { + onSuccess?.(); + close(); + }; + + const renderForm = () => { + switch (credType) { + case 'kv-env': + case 'kv-header': { + return ; + } + case 'oauth': { + return ; + } + case 'file': { + return ; + } + default: { + return null; + } + } + }; + + return ( + <> + + + {step === 0 ? : renderForm()} + + ); +}; + +export default CreateCredModalContent; diff --git a/src/routes/(main)/settings/creds/features/CreateCredModal/index.tsx b/src/routes/(main)/settings/creds/features/CreateCredModal/index.tsx index bf718ff0e8..ee01caaaab 100644 --- a/src/routes/(main)/settings/creds/features/CreateCredModal/index.tsx +++ b/src/routes/(main)/settings/creds/features/CreateCredModal/index.tsx @@ -1,100 +1,18 @@ 'use client'; -import { type CredType } from '@lobechat/types'; -import { Modal } from '@lobehub/ui'; -import { Steps } from 'antd'; -import { createStaticStyles } from 'antd-style'; -import { type FC, useState } from 'react'; -import { useTranslation } from 'react-i18next'; +import { createModal, type ModalInstance } from '@lobehub/ui/base-ui'; +import { t } from 'i18next'; -import CredTypeSelector from './CredTypeSelector'; -import FileCredForm from './FileCredForm'; -import KVCredForm from './KVCredForm'; -import OAuthCredForm from './OAuthCredForm'; +import CreateCredModalContent, { type CreateCredModalContentProps } from './Content'; -const styles = createStaticStyles(({ css }) => ({ - content: css` - padding-block: 24px; - `, - steps: css` - margin-block-end: 24px; - `, -})); - -interface CreateCredModalProps { - onCancel: () => void; - onSuccess: () => void; - open: boolean; -} - -const CreateCredModal: FC = ({ open, onCancel, onSuccess }) => { - const { t } = useTranslation('setting'); - const [step, setStep] = useState(0); - const [credType, setCredType] = useState(null); - - const handleTypeSelect = (type: CredType) => { - setCredType(type); - setStep(1); - }; - - const handleBack = () => { - setStep(0); - setCredType(null); - }; - - const handleClose = () => { - setStep(0); - setCredType(null); - onCancel(); - }; - - const handleSuccess = () => { - setStep(0); - setCredType(null); - onSuccess(); - }; - - const renderForm = () => { - switch (credType) { - case 'kv-env': - case 'kv-header': { - return ; - } - case 'oauth': { - return ; - } - case 'file': { - return ; - } - default: { - return null; - } - } - }; - - return ( - -
- - - {step === 0 ? : renderForm()} -
-
- ); -}; - -export default CreateCredModal; +export const createCreateCredModal = (props?: CreateCredModalContentProps): ModalInstance => + createModal({ + content: , + footer: null, + maskClosable: true, + styles: { + content: { paddingBlock: 16, paddingInline: 24 }, + }, + title: t('creds.createModal.title', { ns: 'setting' }), + width: 'min(90vw, 640px)', + }); diff --git a/src/routes/(main)/settings/creds/features/CredItem.tsx b/src/routes/(main)/settings/creds/features/CredItem.tsx index dfdcd065f7..34ee97856e 100644 --- a/src/routes/(main)/settings/creds/features/CredItem.tsx +++ b/src/routes/(main)/settings/creds/features/CredItem.tsx @@ -2,7 +2,8 @@ import { type UserCredSummary } from '@lobechat/types'; import { Avatar, Button, DropdownMenu, Flexbox, Icon, stopPropagation } from '@lobehub/ui'; -import { App, Tag } from 'antd'; +import { confirmModal } from '@lobehub/ui/base-ui'; +import { Tag } from 'antd'; import { Eye, File, @@ -41,17 +42,14 @@ const typeColors: Record = { const CredItem: FC = memo(({ cred, onEdit, onDelete, onView }) => { const { t } = useTranslation('setting'); - const { modal } = App.useApp(); const handleDelete = () => { - modal.confirm({ - centered: true, + confirmModal({ content: t('creds.actions.deleteConfirm.content'), okButtonProps: { danger: true }, okText: t('creds.actions.deleteConfirm.ok'), onOk: () => onDelete(cred.id), title: t('creds.actions.deleteConfirm.title'), - type: 'error', }); }; diff --git a/src/routes/(main)/settings/creds/features/CredsList.tsx b/src/routes/(main)/settings/creds/features/CredsList.tsx index cadb010fae..fd146ce732 100644 --- a/src/routes/(main)/settings/creds/features/CredsList.tsx +++ b/src/routes/(main)/settings/creds/features/CredsList.tsx @@ -6,15 +6,15 @@ import { useMutation } from '@tanstack/react-query'; import { Empty, Spin } from 'antd'; import { createStaticStyles } from 'antd-style'; import { LogIn } from 'lucide-react'; -import { type FC, useState } from 'react'; +import { type FC } from 'react'; import { useTranslation } from 'react-i18next'; import { useMarketAuth } from '@/layout/AuthProvider/MarketAuth'; import { lambdaClient, lambdaQuery } from '@/libs/trpc/client'; import CredItem from './CredItem'; -import EditCredModal from './EditCredModal'; -import ViewCredModal from './ViewCredModal'; +import { createEditCredModal } from './EditCredModal'; +import { createViewCredModal } from './ViewCredModal'; const styles = createStaticStyles(({ css }) => ({ container: css` @@ -39,8 +39,6 @@ const styles = createStaticStyles(({ css }) => ({ const CredsList: FC = () => { const { t } = useTranslation('setting'); - const [editingCred, setEditingCred] = useState(null); - const [viewingCred, setViewingCred] = useState(null); const { isAuthenticated, isLoading: isAuthLoading, signIn } = useMarketAuth(); const { data, isLoading, refetch } = lambdaQuery.market.creds.list.useQuery(undefined, { @@ -56,26 +54,30 @@ const CredsList: FC = () => { const credentials = data?.data ?? []; - const handleEditSuccess = () => { - setEditingCred(null); - refetch(); + const handleEdit = (cred: UserCredSummary) => { + createEditCredModal({ + cred, + onSuccess: () => refetch(), + }); + }; + + const handleView = (cred: UserCredSummary) => { + createViewCredModal({ cred }); }; - // Show loading while checking auth status if (isAuthLoading) { return ( - + ); } - // Show sign-in prompt if not authenticated if (!isAuthenticated) { return (
-
@@ -85,7 +87,7 @@ const CredsList: FC = () => { return (
{isLoading ? ( - + ) : credentials.length === 0 ? ( @@ -97,20 +99,12 @@ const CredsList: FC = () => { cred={cred} key={cred.id} onDelete={(id) => deleteMutation.mutate(id)} - onEdit={setEditingCred} - onView={setViewingCred} + onEdit={handleEdit} + onView={handleView} /> ))} )} - - setEditingCred(null)} - onSuccess={handleEditSuccess} - /> - setViewingCred(null)} />
); }; diff --git a/src/routes/(main)/settings/creds/features/EditCredModal/Content.tsx b/src/routes/(main)/settings/creds/features/EditCredModal/Content.tsx new file mode 100644 index 0000000000..962902d067 --- /dev/null +++ b/src/routes/(main)/settings/creds/features/EditCredModal/Content.tsx @@ -0,0 +1,32 @@ +'use client'; + +import { type UserCredSummary } from '@lobechat/types'; +import { useModalContext } from '@lobehub/ui/base-ui'; +import { type FC } from 'react'; + +import EditKVForm from './EditKVForm'; +import EditMetaForm from './EditMetaForm'; + +export interface EditCredModalContentProps { + cred: UserCredSummary; + onSuccess?: () => void; +} + +const EditCredModalContent: FC = ({ cred, onSuccess }) => { + const { close } = useModalContext(); + + const isKVType = cred.type === 'kv-env' || cred.type === 'kv-header'; + + const handleSuccess = () => { + onSuccess?.(); + close(); + }; + + return isKVType ? ( + + ) : ( + + ); +}; + +export default EditCredModalContent; diff --git a/src/routes/(main)/settings/creds/features/EditCredModal/index.tsx b/src/routes/(main)/settings/creds/features/EditCredModal/index.tsx index e97e072924..c795e8c2d5 100644 --- a/src/routes/(main)/settings/creds/features/EditCredModal/index.tsx +++ b/src/routes/(main)/settings/creds/features/EditCredModal/index.tsx @@ -1,48 +1,18 @@ 'use client'; -import { type UserCredSummary } from '@lobechat/types'; -import { Modal } from '@lobehub/ui'; -import { type FC } from 'react'; -import { useTranslation } from 'react-i18next'; +import { createModal, type ModalInstance } from '@lobehub/ui/base-ui'; +import { t } from 'i18next'; -import EditKVForm from './EditKVForm'; -import EditMetaForm from './EditMetaForm'; +import EditCredModalContent, { type EditCredModalContentProps } from './Content'; -interface EditCredModalProps { - cred: UserCredSummary | null; - onClose: () => void; - onSuccess: () => void; - open: boolean; -} - -const EditCredModal: FC = ({ open, onClose, onSuccess, cred }) => { - const { t } = useTranslation('setting'); - - if (!cred) return null; - - const isKVType = cred.type === 'kv-env' || cred.type === 'kv-header'; - - const handleSuccess = () => { - onSuccess(); - onClose(); - }; - - return ( - - {isKVType ? ( - - ) : ( - - )} - - ); -}; - -export default EditCredModal; +export const createEditCredModal = (props: EditCredModalContentProps): ModalInstance => + createModal({ + content: , + footer: null, + maskClosable: true, + styles: { + content: { paddingBlock: 16, paddingInline: 24 }, + }, + title: t('creds.edit.title', { ns: 'setting' }), + width: 'min(90vw, 560px)', + }); diff --git a/src/routes/(main)/settings/creds/features/ViewCredModal.tsx b/src/routes/(main)/settings/creds/features/ViewCredModal/Content.tsx similarity index 60% rename from src/routes/(main)/settings/creds/features/ViewCredModal.tsx rename to src/routes/(main)/settings/creds/features/ViewCredModal/Content.tsx index 4bd86ec5bb..79479d6ce9 100644 --- a/src/routes/(main)/settings/creds/features/ViewCredModal.tsx +++ b/src/routes/(main)/settings/creds/features/ViewCredModal/Content.tsx @@ -3,7 +3,7 @@ import { type UserCredSummary } from '@lobechat/types'; import { CopyButton, Flexbox } from '@lobehub/ui'; import { useQuery } from '@tanstack/react-query'; -import { Alert, Descriptions, Modal, Skeleton, Typography } from 'antd'; +import { Alert, Descriptions, Skeleton, Typography } from 'antd'; import { createStaticStyles, cx } from 'antd-style'; import { Eye, EyeOff } from 'lucide-react'; import { type FC, useState } from 'react'; @@ -84,7 +84,6 @@ const styles = createStaticStyles(({ css, cssVar }) => ({ `, })); -// Mask value like "sk-****xxxx" const maskValue = (value: string): string => { if (value.length <= 4) return '••••••••'; return '••••••••' + value.slice(-4); @@ -113,96 +112,89 @@ const KVRow: FC = ({ keyName, value }) => { > {visible ? value : maskValue(value)} - +
setVisible(!visible)}> {visible ? : }
- +
); }; -interface ViewCredModalProps { - cred: UserCredSummary | null; - onClose: () => void; - open: boolean; +export interface ViewCredModalContentProps { + cred: UserCredSummary; } -const ViewCredModal: FC = ({ cred, open, onClose }) => { +const ViewCredModalContent: FC = ({ cred }) => { const { t } = useTranslation('setting'); const { data, isLoading, error } = useQuery({ - enabled: open && !!cred, queryFn: () => lambdaClient.market.creds.get.query({ decrypt: true, - id: cred!.id, + id: cred.id, }), - queryKey: ['cred-plaintext', cred?.id], + queryKey: ['cred-plaintext', cred.id], }); const values = (data as any)?.plaintext || {}; const valueEntries = Object.entries(values); + if (isLoading) { + return ; + } + + if (error) { + return ( + + ); + } + return ( - - {isLoading ? ( - - ) : error ? ( + <> + + + {cred.name} + + {cred.key} + + + {cred.type ? t(`creds.types.${cred.type}` as any) : '-'} + + + + {valueEntries.length > 0 && ( +
+
{t('creds.view.values')}
+ {valueEntries.map(([key, value]) => ( + + ))} +
+ )} + + {valueEntries.length === 0 && cred.type === 'oauth' && ( - ) : ( - <> - - - {cred?.name} - - {cred?.key} - - - {cred?.type ? t(`creds.types.${cred.type}` as any) : '-'} - - - - {valueEntries.length > 0 && ( -
-
{t('creds.view.values')}
- {valueEntries.map(([key, value]) => ( - - ))} -
- )} - - {valueEntries.length === 0 && cred?.type === 'oauth' && ( - - )} - )} -
+ ); }; -export default ViewCredModal; +export default ViewCredModalContent; diff --git a/src/routes/(main)/settings/creds/features/ViewCredModal/index.tsx b/src/routes/(main)/settings/creds/features/ViewCredModal/index.tsx new file mode 100644 index 0000000000..372282394f --- /dev/null +++ b/src/routes/(main)/settings/creds/features/ViewCredModal/index.tsx @@ -0,0 +1,18 @@ +'use client'; + +import { createModal, type ModalInstance } from '@lobehub/ui/base-ui'; +import { t } from 'i18next'; + +import ViewCredModalContent, { type ViewCredModalContentProps } from './Content'; + +export const createViewCredModal = (props: ViewCredModalContentProps): ModalInstance => + createModal({ + content: , + footer: null, + maskClosable: true, + styles: { + content: { paddingBlock: 16, paddingInline: 24 }, + }, + title: t('creds.view.title', { name: props.cred.name, ns: 'setting' }), + width: 'min(90vw, 600px)', + }); diff --git a/src/routes/(main)/settings/creds/features/index.ts b/src/routes/(main)/settings/creds/features/index.ts index 151122293a..3f20678726 100644 --- a/src/routes/(main)/settings/creds/features/index.ts +++ b/src/routes/(main)/settings/creds/features/index.ts @@ -1,6 +1,6 @@ -export { default as CreateCredModal } from './CreateCredModal'; +export { createCreateCredModal } from './CreateCredModal'; export { default as CredDisplay } from './CredDisplay'; export { default as CredItem } from './CredItem'; export { default as CredsList } from './CredsList'; -export { default as EditCredModal } from './EditCredModal'; -export { default as ViewCredModal } from './ViewCredModal'; +export { createEditCredModal } from './EditCredModal'; +export { createViewCredModal } from './ViewCredModal'; diff --git a/src/routes/(main)/settings/creds/index.tsx b/src/routes/(main)/settings/creds/index.tsx index ae5c4bcbc4..17df92ecbc 100644 --- a/src/routes/(main)/settings/creds/index.tsx +++ b/src/routes/(main)/settings/creds/index.tsx @@ -7,17 +7,17 @@ import { useTranslation } from 'react-i18next'; import SettingHeader from '@/routes/(main)/settings/features/SettingHeader'; -import CreateCredModal from './features/CreateCredModal'; +import { createCreateCredModal } from './features/CreateCredModal'; import CredsList from './features/CredsList'; const Page = () => { const { t } = useTranslation('setting'); - const [createModalOpen, setCreateModalOpen] = useState(false); const [refreshKey, setRefreshKey] = useState(0); - const handleCreateSuccess = () => { - setCreateModalOpen(false); - setRefreshKey((k) => k + 1); + const handleCreate = () => { + createCreateCredModal({ + onSuccess: () => setRefreshKey((k) => k + 1), + }); }; return ( @@ -25,17 +25,12 @@ const Page = () => { } size="large" onClick={() => setCreateModalOpen(true)}> + } /> - setCreateModalOpen(false)} - onSuccess={handleCreateSuccess} - /> ); }; diff --git a/src/routes/(main)/settings/profile/features/KlavisAuthorizationList/index.tsx b/src/routes/(main)/settings/profile/features/KlavisAuthorizationList/index.tsx index 7dce214cfb..9aa058c564 100644 --- a/src/routes/(main)/settings/profile/features/KlavisAuthorizationList/index.tsx +++ b/src/routes/(main)/settings/profile/features/KlavisAuthorizationList/index.tsx @@ -1,9 +1,9 @@ import { KLAVIS_SERVER_TYPES } from '@lobechat/const'; import { Avatar, Flexbox, Tag } from '@lobehub/ui'; +import { confirmModal } from '@lobehub/ui/base-ui'; import { memo, useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { modal } from '@/components/AntdStaticMethods'; import { useToolStore } from '@/store/tool'; import { type KlavisServer } from '@/store/tool/slices/klavisStore'; @@ -22,7 +22,7 @@ const KlavisAuthItem = memo(({ server }) => { // Handle deauthorization const handleRevoke = useCallback(() => { - modal.confirm({ + confirmModal({ content: t('profile.authorizations.revoke.description'), okButtonProps: { danger: true }, onOk: async () => { diff --git a/src/routes/(main)/settings/profile/features/SSOProvidersList/index.tsx b/src/routes/(main)/settings/profile/features/SSOProvidersList/index.tsx index 9f18ff9d50..eca48745e9 100644 --- a/src/routes/(main)/settings/profile/features/SSOProvidersList/index.tsx +++ b/src/routes/(main)/settings/profile/features/SSOProvidersList/index.tsx @@ -1,12 +1,13 @@ import { isDesktop } from '@lobechat/const'; import { type MenuProps } from '@lobehub/ui'; import { ActionIcon, DropdownMenu, Flexbox, Text } from '@lobehub/ui'; +import { confirmModal } from '@lobehub/ui/base-ui'; import { ArrowRight, Plus, Unlink } from 'lucide-react'; import { type CSSProperties } from 'react'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { modal, notification } from '@/components/AntdStaticMethods'; +import { notification } from '@/components/AntdStaticMethods'; import AuthIcons from '@/components/AuthIcons'; import { isBuiltinProvider, normalizeProviderId } from '@/libs/better-auth/utils/client'; import { useServerConfigStore } from '@/store/serverConfig'; @@ -54,7 +55,7 @@ export const SSOProvidersList = memo(() => { }); return; } - modal.confirm({ + confirmModal({ content: t('profile.sso.unlink.description', { provider }), okButtonProps: { danger: true, diff --git a/src/routes/(main)/settings/provider/ProviderMenu/AddNew.tsx b/src/routes/(main)/settings/provider/ProviderMenu/AddNew.tsx index 2ed9da8e60..fe84b0237d 100644 --- a/src/routes/(main)/settings/provider/ProviderMenu/AddNew.tsx +++ b/src/routes/(main)/settings/provider/ProviderMenu/AddNew.tsx @@ -2,25 +2,22 @@ import { ActionIcon } from '@lobehub/ui'; import { PlusIcon } from 'lucide-react'; -import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import CreateNewProvider from '../features/CreateNewProvider'; +import { DESKTOP_HEADER_ICON_SMALL_SIZE } from '@/const/layoutTokens'; + +import { createCreateNewProviderModal } from '../features/CreateNewProvider'; const AddNewProvider = () => { const { t } = useTranslation('modelProvider'); - const [open, setOpen] = useState(false); return ( - <> - setOpen(true)} - /> - setOpen(false)} /> - + createCreateNewProviderModal()} + /> ); }; diff --git a/src/routes/(main)/settings/provider/features/CreateNewProvider/Content.tsx b/src/routes/(main)/settings/provider/features/CreateNewProvider/Content.tsx new file mode 100644 index 0000000000..98d226c008 --- /dev/null +++ b/src/routes/(main)/settings/provider/features/CreateNewProvider/Content.tsx @@ -0,0 +1,182 @@ +'use client'; + +import { ProviderIcon } from '@lobehub/icons'; +import { Button, Flexbox, Input, InputPassword, Text, TextArea } from '@lobehub/ui'; +import { Select, useModalContext } from '@lobehub/ui/base-ui'; +import { App, Form } from 'antd'; +import { memo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; + +import { useAiInfraStore } from '@/store/aiInfra/store'; +import { type CreateAiProviderParams } from '@/types/aiProvider'; + +import { KeyVaultsConfigKey, LLMProviderApiTokenKey, LLMProviderBaseUrlKey } from '../../const'; +import { CUSTOM_PROVIDER_SDK_OPTIONS } from '../customProviderSdkOptions'; +import { normalizeProviderSettings } from '../providerSettings'; + +const SectionTitle = memo<{ children: React.ReactNode }>(({ children }) => ( + + {children} + +)); + +const CreateNewProviderContent = memo(() => { + const { t } = useTranslation('modelProvider'); + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + const createNewAiProvider = useAiInfraStore((s) => s.createNewAiProvider); + const { message } = App.useApp(); + const navigate = useNavigate(); + const { close } = useModalContext(); + + const onFinish = async (values: CreateAiProviderParams) => { + setLoading(true); + + try { + const finalValues = { + ...values, + name: values.name || values.id, + settings: normalizeProviderSettings({ + nextSettings: values.settings, + }) as CreateAiProviderParams['settings'], + }; + + await createNewAiProvider(finalValues); + setLoading(false); + navigate(`/settings/provider/${values.id}`); + message.success(t('createNewAiProvider.createSuccess')); + close(); + } catch (e) { + console.error(e); + setLoading(false); + } + }; + + const itemStyle = { marginBottom: 0 }; + + return ( +
+ + {t('createNewAiProvider.basicTitle')} + + { + const list = useAiInfraStore.getState().aiProviderList; + if (value && list.some((p) => p.id === value)) { + return Promise.reject(); + } + return Promise.resolve(); + }, + }, + ]} + > + + + + + + + + +