From 35f76cb7aeebe585de55a469e01d14df6da483c0 Mon Sep 17 00:00:00 2001 From: "Junyan Qin (Chin)" Date: Sat, 28 Jun 2025 21:50:51 +0800 Subject: [PATCH] Perf/combine entity dialogs (#1555) * feat: combine bot settings and bot log dialogs * perf: dialog style when creating bot * perf: bot creation dialog * feat: combine pipeline dialogs * perf: ui * perf: move buttons * perf: ui layout in pipeline detail dialog * perf: remove debug button from pipeline card * perf: open pipeline dialog after creating * perf: placeholder in send input * perf: no close dialog when save done * fix: linter errors --- web/package.json | 6 +- web/src/app/home/bots/BotDetailDialog.tsx | 262 +++++++ web/src/app/home/bots/ICreateBotField.ts | 0 .../home/bots/components/bot-card/BotCard.tsx | 25 - .../home/bots/components/bot-form/BotForm.tsx | 71 +- .../{ => components}/bot-log/BotLogManager.ts | 0 .../bot-log/view/BotLogCard.tsx | 0 .../bot-log/view/BotLogListComponent.tsx | 9 +- .../bot-log/view/botLog.module.css | 0 web/src/app/home/bots/page.tsx | 99 +-- .../home/pipelines/PipelineDetailDialog.tsx | 214 ++++++ .../{ => components}/debug-dialog/AtBadge.tsx | 0 .../components/debug-dialog/DebugDialog.tsx | 376 +++++++++ .../components/pipeline-card/PipelineCard.tsx | 30 +- .../pipeline-form/PipelineFormComponent.tsx | 332 ++++---- .../pipelines/debug-dialog/DebugDialog.tsx | 422 ---------- web/src/app/home/pipelines/page.tsx | 114 ++- web/src/components/ui/breadcrumb.tsx | 109 +++ web/src/components/ui/separator.tsx | 28 + web/src/components/ui/sheet.tsx | 139 ++++ web/src/components/ui/sidebar.tsx | 726 ++++++++++++++++++ web/src/components/ui/skeleton.tsx | 13 + web/src/components/ui/tooltip.tsx | 61 ++ web/src/hooks/use-mobile.ts | 21 + web/src/i18n/locales/en-US.ts | 8 +- web/src/i18n/locales/ja-JP.ts | 8 +- web/src/i18n/locales/zh-Hans.ts | 10 +- 27 files changed, 2271 insertions(+), 812 deletions(-) create mode 100644 web/src/app/home/bots/BotDetailDialog.tsx delete mode 100644 web/src/app/home/bots/ICreateBotField.ts rename web/src/app/home/bots/{ => components}/bot-log/BotLogManager.ts (100%) rename web/src/app/home/bots/{ => components}/bot-log/view/BotLogCard.tsx (100%) rename web/src/app/home/bots/{ => components}/bot-log/view/BotLogListComponent.tsx (93%) rename web/src/app/home/bots/{ => components}/bot-log/view/botLog.module.css (100%) create mode 100644 web/src/app/home/pipelines/PipelineDetailDialog.tsx rename web/src/app/home/pipelines/{ => components}/debug-dialog/AtBadge.tsx (100%) create mode 100644 web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx delete mode 100644 web/src/app/home/pipelines/debug-dialog/DebugDialog.tsx create mode 100644 web/src/components/ui/breadcrumb.tsx create mode 100644 web/src/components/ui/separator.tsx create mode 100644 web/src/components/ui/sheet.tsx create mode 100644 web/src/components/ui/sidebar.tsx create mode 100644 web/src/components/ui/skeleton.tsx create mode 100644 web/src/components/ui/tooltip.tsx create mode 100644 web/src/hooks/use-mobile.ts diff --git a/web/package.json b/web/package.json index 6d1fca11..17516ac4 100644 --- a/web/package.json +++ b/web/package.json @@ -21,17 +21,19 @@ "@dnd-kit/sortable": "^10.0.0", "@hookform/resolvers": "^5.0.1", "@radix-ui/react-checkbox": "^1.3.1", - "@radix-ui/react-dialog": "^1.1.13", + "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-hover-card": "^1.1.13", "@radix-ui/react-label": "^2.1.6", "@radix-ui/react-popover": "^1.1.14", "@radix-ui/react-scroll-area": "^1.2.9", "@radix-ui/react-select": "^2.2.4", - "@radix-ui/react-slot": "^1.2.2", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.4", "@radix-ui/react-tabs": "^1.1.11", "@radix-ui/react-toggle": "^1.1.8", "@radix-ui/react-toggle-group": "^1.1.9", + "@radix-ui/react-tooltip": "^1.2.7", "@tailwindcss/postcss": "^4.1.5", "axios": "^1.8.4", "class-variance-authority": "^0.7.1", diff --git a/web/src/app/home/bots/BotDetailDialog.tsx b/web/src/app/home/bots/BotDetailDialog.tsx new file mode 100644 index 00000000..1c4a2403 --- /dev/null +++ b/web/src/app/home/bots/BotDetailDialog.tsx @@ -0,0 +1,262 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from '@/components/ui/dialog'; +import { + Sidebar, + SidebarContent, + SidebarGroup, + SidebarGroupContent, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarProvider, +} from '@/components/ui/sidebar'; +import { Button } from '@/components/ui/button'; +import BotForm from '@/app/home/bots/components/bot-form/BotForm'; +import { BotLogListComponent } from '@/app/home/bots/components/bot-log/view/BotLogListComponent'; +import { useTranslation } from 'react-i18next'; +import { z } from 'zod'; +import { httpClient } from '@/app/infra/http/HttpClient'; + +interface BotDetailDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + botId?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onFormSubmit: (value: z.infer) => void; + onFormCancel: () => void; + onBotDeleted: () => void; + onNewBotCreated: (botId: string) => void; +} + +export default function BotDetailDialog({ + open, + onOpenChange, + botId: propBotId, + onFormSubmit, + onFormCancel, + onBotDeleted, + onNewBotCreated, +}: BotDetailDialogProps) { + const { t } = useTranslation(); + const [botId, setBotId] = useState(propBotId); + const [activeMenu, setActiveMenu] = useState('config'); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + + useEffect(() => { + setBotId(propBotId); + setActiveMenu('config'); + }, [propBotId, open]); + + const menu = [ + { + key: 'config', + label: t('bots.configuration'), + icon: ( + + + + ), + }, + { + key: 'logs', + label: t('bots.logs'), + icon: ( + + + + ), + }, + ]; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handleFormSubmit = (value: any) => { + onFormSubmit(value); + }; + + const handleFormCancel = () => { + onFormCancel(); + }; + + const handleBotDeleted = () => { + httpClient.deleteBot(botId ?? '').then(() => { + onBotDeleted(); + }); + }; + + const handleNewBotCreated = (newBotId: string) => { + setBotId(newBotId); + setActiveMenu('config'); + onNewBotCreated(newBotId); + }; + + const handleDelete = () => { + setShowDeleteConfirm(true); + }; + + const confirmDelete = () => { + handleBotDeleted(); + setShowDeleteConfirm(false); + }; + + if (!botId) { + return ( + <> + + +
+ + {t('bots.createBot')} + +
+ +
+ +
+ + +
+
+
+
+
+ + ); + } + + return ( + <> + + + + + + + + + {menu.map((item) => ( + + setActiveMenu(item.key)} + > + + {item.icon} + {item.label} + + + + ))} + + + + + +
+ + + {activeMenu === 'config' + ? t('bots.editBot') + : t('bots.botLogTitle')} + + +
+ {activeMenu === 'config' && ( + + )} + {activeMenu === 'logs' && botId && ( + + )} +
+ {activeMenu === 'config' && ( + +
+ + + +
+
+ )} +
+
+
+
+ + {/* 删除确认对话框 */} + + + + {t('common.confirmDelete')} + +
{t('bots.deleteConfirmation')}
+ + + + +
+
+ + ); +} diff --git a/web/src/app/home/bots/ICreateBotField.ts b/web/src/app/home/bots/ICreateBotField.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/web/src/app/home/bots/components/bot-card/BotCard.tsx b/web/src/app/home/bots/components/bot-card/BotCard.tsx index 5d44a8d6..3551ed66 100644 --- a/web/src/app/home/bots/components/bot-card/BotCard.tsx +++ b/web/src/app/home/bots/components/bot-card/BotCard.tsx @@ -4,21 +4,15 @@ import { httpClient } from '@/app/infra/http/HttpClient'; import { Switch } from '@/components/ui/switch'; import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; -import { Button } from '@/components/ui/button'; export default function BotCard({ botCardVO, - clickLogIconCallback, setBotEnableCallback, }: { botCardVO: BotCardVO; - clickLogIconCallback: (id: string) => void; setBotEnableCallback: (id: string, enable: boolean) => void; }) { const { t } = useTranslation(); - function onClickLogIcon() { - clickLogIconCallback(botCardVO.id); - } function setBotEnable(enable: boolean) { return httpClient.updateBot(botCardVO.id, { @@ -93,25 +87,6 @@ export default function BotCard({ e.stopPropagation(); }} /> - diff --git a/web/src/app/home/bots/components/bot-form/BotForm.tsx b/web/src/app/home/bots/components/bot-form/BotForm.tsx index c2c79e41..40a902c2 100644 --- a/web/src/app/home/bots/components/bot-form/BotForm.tsx +++ b/web/src/app/home/bots/components/bot-form/BotForm.tsx @@ -67,12 +67,14 @@ export default function BotForm({ onFormCancel, onBotDeleted, onNewBotCreated, + hideButtons = false, }: { initBotId?: string; onFormSubmit: (value: z.infer>) => void; onFormCancel: () => void; onBotDeleted: () => void; onNewBotCreated: (botId: string) => void; + hideButtons?: boolean; }) { const { t } = useTranslation(); const formSchema = getFormSchema(t); @@ -282,7 +284,7 @@ export default function BotForm({ }) .finally(() => { setIsLoading(false); - form.reset(); + // form.reset(); // dynamicForm.resetFields(); }); } else { @@ -314,8 +316,6 @@ export default function BotForm({ // dynamicForm.resetFields(); }); } - setShowDynamicForm(false); - console.log('set loading', false); } function deleteBot() { @@ -365,6 +365,7 @@ export default function BotForm({
@@ -527,42 +528,44 @@ export default function BotForm({ )} -
-
- {!initBotId && ( - - )} - {initBotId && ( - <> + {!hideButtons && ( +
+
+ {!initBotId && ( - - - )} - + )} + {initBotId && ( + <> + + + + )} + +
-
+ )}
diff --git a/web/src/app/home/bots/bot-log/BotLogManager.ts b/web/src/app/home/bots/components/bot-log/BotLogManager.ts similarity index 100% rename from web/src/app/home/bots/bot-log/BotLogManager.ts rename to web/src/app/home/bots/components/bot-log/BotLogManager.ts diff --git a/web/src/app/home/bots/bot-log/view/BotLogCard.tsx b/web/src/app/home/bots/components/bot-log/view/BotLogCard.tsx similarity index 100% rename from web/src/app/home/bots/bot-log/view/BotLogCard.tsx rename to web/src/app/home/bots/components/bot-log/view/BotLogCard.tsx diff --git a/web/src/app/home/bots/bot-log/view/BotLogListComponent.tsx b/web/src/app/home/bots/components/bot-log/view/BotLogListComponent.tsx similarity index 93% rename from web/src/app/home/bots/bot-log/view/BotLogListComponent.tsx rename to web/src/app/home/bots/components/bot-log/view/BotLogListComponent.tsx index 5c8ea0ed..368df61c 100644 --- a/web/src/app/home/bots/bot-log/view/BotLogListComponent.tsx +++ b/web/src/app/home/bots/components/bot-log/view/BotLogListComponent.tsx @@ -1,9 +1,9 @@ 'use client'; -import { BotLogManager } from '@/app/home/bots/bot-log/BotLogManager'; +import { BotLogManager } from '@/app/home/bots/components/bot-log/BotLogManager'; import { useCallback, useEffect, useRef, useState } from 'react'; import { BotLog } from '@/app/infra/http/requestParam/bots/GetBotLogsResponse'; -import { BotLogCard } from '@/app/home/bots/bot-log/view/BotLogCard'; +import { BotLogCard } from '@/app/home/bots/components/bot-log/view/BotLogCard'; import styles from './botLog.module.css'; import { Switch } from '@/components/ui/switch'; import { debounce } from 'lodash'; @@ -112,10 +112,7 @@ export function BotLogListComponent({ botId }: { botId: string }) { ); return ( -
+
{t('bots.enableAutoRefresh')}
setAutoFlush(e)} /> diff --git a/web/src/app/home/bots/bot-log/view/botLog.module.css b/web/src/app/home/bots/components/bot-log/view/botLog.module.css similarity index 100% rename from web/src/app/home/bots/bot-log/view/botLog.module.css rename to web/src/app/home/bots/components/bot-log/view/botLog.module.css diff --git a/web/src/app/home/bots/page.tsx b/web/src/app/home/bots/page.tsx index 33d55e61..d4305898 100644 --- a/web/src/app/home/bots/page.tsx +++ b/web/src/app/home/bots/page.tsx @@ -3,32 +3,21 @@ import { useEffect, useState } from 'react'; import styles from './botConfig.module.css'; import { BotCardVO } from '@/app/home/bots/components/bot-card/BotCardVO'; -import BotForm from '@/app/home/bots/components/bot-form/BotForm'; import BotCard from '@/app/home/bots/components/bot-card/BotCard'; import CreateCardComponent from '@/app/infra/basic-component/create-card-component/CreateCardComponent'; import { httpClient } from '@/app/infra/http/HttpClient'; import { Bot, Adapter } from '@/app/infra/entities/api'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; import { toast } from 'sonner'; import { useTranslation } from 'react-i18next'; import { i18nObj } from '@/i18n/I18nProvider'; -import { BotLogListComponent } from '@/app/home/bots/bot-log/view/BotLogListComponent'; +import BotDetailDialog from '@/app/home/bots/BotDetailDialog'; export default function BotConfigPage() { const { t } = useTranslation(); - // 编辑机器人的modal - const [modalOpen, setModalOpen] = useState(false); - // 机器人日志的modal - const [logModalOpen, setLogModalOpen] = useState(false); + // 机器人详情dialog + const [detailDialogOpen, setDetailDialogOpen] = useState(false); const [botList, setBotList] = useState([]); - const [isEditForm, setIsEditForm] = useState(false); - const [nowSelectedBotUUID, setNowSelectedBotUUID] = useState(); - const [nowSelectedBotLog, setNowSelectedBotLog] = useState(); + const [selectedBotId, setSelectedBotId] = useState(''); useEffect(() => { getBotList(); @@ -73,61 +62,46 @@ export default function BotConfigPage() { } function handleCreateBotClick() { - setIsEditForm(false); - setNowSelectedBotUUID(''); - setModalOpen(true); + setSelectedBotId(''); + setDetailDialogOpen(true); } function selectBot(botUUID: string) { - setNowSelectedBotUUID(botUUID); - setIsEditForm(true); - setModalOpen(true); + setSelectedBotId(botUUID); + setDetailDialogOpen(true); } - function onClickLogIcon(botId: string) { - setNowSelectedBotLog(botId); - setLogModalOpen(true); + function handleFormSubmit() { + getBotList(); + // setDetailDialogOpen(false); + } + + function handleFormCancel() { + setDetailDialogOpen(false); + } + + function handleBotDeleted() { + getBotList(); + setDetailDialogOpen(false); + } + + function handleNewBotCreated(botId: string) { + console.log('new bot created', botId); + getBotList(); + setSelectedBotId(botId); } return (
- - - - - {isEditForm ? t('bots.editBot') : t('bots.createBot')} - - -
- { - getBotList(); - setModalOpen(false); - }} - onFormCancel={() => setModalOpen(false)} - onBotDeleted={() => { - getBotList(); - setModalOpen(false); - }} - onNewBotCreated={(botId) => { - console.log('new bot created', botId); - getBotList(); - selectBot(botId); - }} - /> -
-
-
- - - - - {t('bots.botLogTitle')} - - - - + {/* 注意:其余的返回内容需要保持在Spin组件外部 */}
@@ -147,9 +121,6 @@ export default function BotConfigPage() { > { - onClickLogIcon(id); - }} setBotEnableCallback={(id, enable) => { setBotList( botList.map((bot) => { diff --git a/web/src/app/home/pipelines/PipelineDetailDialog.tsx b/web/src/app/home/pipelines/PipelineDetailDialog.tsx new file mode 100644 index 00000000..72b4ac76 --- /dev/null +++ b/web/src/app/home/pipelines/PipelineDetailDialog.tsx @@ -0,0 +1,214 @@ +import React, { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { + Sidebar, + SidebarContent, + SidebarGroup, + SidebarGroupContent, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarProvider, +} from '@/components/ui/sidebar'; +import PipelineFormComponent from './components/pipeline-form/PipelineFormComponent'; +import DebugDialog from './components/debug-dialog/DebugDialog'; +import { PipelineFormEntity } from '@/app/infra/entities/pipeline'; + +interface PipelineDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + pipelineId?: string; + isEditMode?: boolean; + isDefaultPipeline?: boolean; + initValues?: PipelineFormEntity; + onFinish: () => void; + onNewPipelineCreated?: (pipelineId: string) => void; + onDeletePipeline: () => void; + onCancel: () => void; +} + +type DialogMode = 'config' | 'debug'; + +export default function PipelineDialog({ + open, + onOpenChange, + pipelineId: propPipelineId, + isEditMode = false, + isDefaultPipeline = false, + initValues, + onFinish, + onNewPipelineCreated, + onDeletePipeline, + onCancel, +}: PipelineDialogProps) { + const { t } = useTranslation(); + const [pipelineId, setPipelineId] = useState( + propPipelineId, + ); + const [currentMode, setCurrentMode] = useState('config'); + + useEffect(() => { + setPipelineId(propPipelineId); + setCurrentMode('config'); + }, [propPipelineId, open]); + + const handleFinish = () => { + onFinish(); + }; + + const handleNewPipelineCreated = (newPipelineId: string) => { + setPipelineId(newPipelineId); + setCurrentMode('config'); + if (onNewPipelineCreated) { + onNewPipelineCreated(newPipelineId); + } + }; + + const menu = [ + { + key: 'config', + label: t('pipelines.configuration'), + icon: ( + + + + ), + }, + { + key: 'debug', + label: t('pipelines.debugChat'), + icon: ( + + + + ), + }, + ]; + + const getDialogTitle = () => { + if (currentMode === 'config') { + return isEditMode + ? t('pipelines.editPipeline') + : t('pipelines.createPipeline'); + } + return t('pipelines.debugDialog.title'); + }; + + // 创建新流水线时的对话框 + if (!isEditMode) { + return ( + + +
+ + {t('pipelines.createPipeline')} + +
+ { + onCancel(); + }} + /> +
+
+
+
+ ); + } + + // 编辑流水线时的对话框 + return ( + + + + + + + + + {menu.map((item) => ( + + setCurrentMode(item.key as DialogMode)} + > + + {item.icon} + {item.label} + + + + ))} + + + + + +
+ + {getDialogTitle()} + +
+ {currentMode === 'config' && ( + { + onCancel(); + }} + /> + )} + {currentMode === 'debug' && pipelineId && ( + + )} +
+
+
+
+
+ ); +} diff --git a/web/src/app/home/pipelines/debug-dialog/AtBadge.tsx b/web/src/app/home/pipelines/components/debug-dialog/AtBadge.tsx similarity index 100% rename from web/src/app/home/pipelines/debug-dialog/AtBadge.tsx rename to web/src/app/home/pipelines/components/debug-dialog/AtBadge.tsx diff --git a/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx new file mode 100644 index 00000000..a84389e0 --- /dev/null +++ b/web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx @@ -0,0 +1,376 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { httpClient } from '@/app/infra/http/HttpClient'; +import { DialogContent } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; + +import { ScrollArea } from '@/components/ui/scroll-area'; +import { cn } from '@/lib/utils'; +import { Message } from '@/app/infra/entities/message'; +import { toast } from 'sonner'; +import AtBadge from './AtBadge'; + +interface MessageComponent { + type: 'At' | 'Plain'; + target?: string; + text?: string; +} + +interface DebugDialogProps { + open: boolean; + pipelineId: string; + isEmbedded?: boolean; +} + +export default function DebugDialog({ + open, + pipelineId, + isEmbedded = false, +}: DebugDialogProps) { + const { t } = useTranslation(); + const [selectedPipelineId, setSelectedPipelineId] = useState(pipelineId); + const [sessionType, setSessionType] = useState<'person' | 'group'>('person'); + const [messages, setMessages] = useState([]); + const [inputValue, setInputValue] = useState(''); + const [showAtPopover, setShowAtPopover] = useState(false); + const [hasAt, setHasAt] = useState(false); + const [isHovering, setIsHovering] = useState(false); + const messagesEndRef = useRef(null); + const inputRef = useRef(null); + const popoverRef = useRef(null); + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }; + + useEffect(() => { + scrollToBottom(); + }, [messages]); + + useEffect(() => { + if (open) { + setSelectedPipelineId(pipelineId); + loadMessages(pipelineId); + } + }, [open, pipelineId]); + + useEffect(() => { + if (open) { + loadMessages(selectedPipelineId); + } + }, [sessionType, selectedPipelineId]); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + popoverRef.current && + !popoverRef.current.contains(event.target as Node) && + !inputRef.current?.contains(event.target as Node) + ) { + setShowAtPopover(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + useEffect(() => { + if (showAtPopover) { + setIsHovering(true); + } + }, [showAtPopover]); + + const loadMessages = async (pipelineId: string) => { + try { + const response = await httpClient.getWebChatHistoryMessages( + pipelineId, + sessionType, + ); + setMessages(response.messages); + } catch (error) { + console.error('Failed to load messages:', error); + } + }; + + const handleInputChange = (e: React.ChangeEvent) => { + const value = e.target.value; + if (sessionType === 'group') { + if (value.endsWith('@')) { + setShowAtPopover(true); + } else if (showAtPopover && (!value.includes('@') || value.length > 1)) { + setShowAtPopover(false); + } + } + setInputValue(value); + }; + + const handleAtSelect = () => { + setHasAt(true); + setShowAtPopover(false); + setInputValue(inputValue.slice(0, -1)); + }; + + const handleAtRemove = () => { + setHasAt(false); + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + if (showAtPopover) { + handleAtSelect(); + } else { + sendMessage(); + } + } else if (e.key === 'Backspace' && hasAt && inputValue === '') { + handleAtRemove(); + } + }; + + const sendMessage = async () => { + if (!inputValue.trim() && !hasAt) return; + + try { + const messageChain = []; + + let text_content = inputValue.trim(); + if (hasAt) { + text_content = ' ' + text_content; + } + + if (hasAt) { + messageChain.push({ + type: 'At', + target: 'webchatbot', + }); + } + messageChain.push({ + type: 'Plain', + text: text_content, + }); + + if (hasAt) { + // for showing + text_content = '@webchatbot' + text_content; + } + + const userMessage: Message = { + id: -1, + role: 'user', + content: text_content, + timestamp: new Date().toISOString(), + message_chain: messageChain, + }; + + setMessages((prevMessages) => [...prevMessages, userMessage]); + setInputValue(''); + setHasAt(false); + + const response = await httpClient.sendWebChatMessage( + sessionType, + messageChain, + selectedPipelineId, + 120000, + ); + + setMessages((prevMessages) => [...prevMessages, response.message]); + } catch ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + error: any + ) { + console.log(error, 'type of error', typeof error); + console.error('Failed to send message:', error); + + if (!error.message.includes('timeout') && sessionType === 'person') { + toast.error(t('pipelines.debugDialog.sendFailed')); + } + } finally { + inputRef.current?.focus(); + } + }; + + const renderMessageContent = (message: Message) => { + return ( + + {(message.message_chain as MessageComponent[]).map( + (component, index) => { + if (component.type === 'At') { + return ( + + ); + } else if (component.type === 'Plain') { + return {component.text}; + } + return null; + }, + )} + + ); + }; + + const renderContent = () => ( +
+
+ + +
+
+ +
+ +
+ {messages.length === 0 ? ( +
+ {t('pipelines.debugDialog.noMessages')} +
+ ) : ( + messages.map((message) => ( +
+
+ {renderMessageContent(message)} +
+ {message.role === 'user' + ? t('pipelines.debugDialog.userMessage') + : t('pipelines.debugDialog.botMessage')} +
+
+
+ )) + )} +
+
+ + +
+
+ {hasAt && ( + + )} +
+ + {showAtPopover && ( +
+
setIsHovering(true)} + onMouseLeave={() => setIsHovering(false)} + > + + @webchatbot - {t('pipelines.debugDialog.atTips')} + +
+
+ )} +
+
+ +
+
+
+ ); + + // 如果是嵌入模式,直接返回内容 + if (isEmbedded) { + return ( +
+
{renderContent()}
+
+ ); + } + + // 原有的Dialog包装 + return ( + + {renderContent()} + + ); +} diff --git a/web/src/app/home/pipelines/components/pipeline-card/PipelineCard.tsx b/web/src/app/home/pipelines/components/pipeline-card/PipelineCard.tsx index 05bf2470..52b3aef9 100644 --- a/web/src/app/home/pipelines/components/pipeline-card/PipelineCard.tsx +++ b/web/src/app/home/pipelines/components/pipeline-card/PipelineCard.tsx @@ -1,22 +1,10 @@ import styles from './pipelineCard.module.css'; import { PipelineCardVO } from '@/app/home/pipelines/components/pipeline-card/PipelineCardVO'; import { useTranslation } from 'react-i18next'; -import { Button } from '@/components/ui/button'; -export default function PipelineCard({ - cardVO, - onDebug, -}: { - cardVO: PipelineCardVO; - onDebug: (pipelineId: string) => void; -}) { +export default function PipelineCard({ cardVO }: { cardVO: PipelineCardVO }) { const { t } = useTranslation(); - const handleDebugClick = (e: React.MouseEvent) => { - e.stopPropagation(); - onDebug(cardVO.id); - }; - return (
@@ -61,22 +49,6 @@ export default function PipelineCard({
)} -
); diff --git a/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx b/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx index 7497f64a..c92b553d 100644 --- a/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx +++ b/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx @@ -22,15 +22,14 @@ import { FormLabel, FormMessage, } from '@/components/ui/form'; -import { toast } from 'sonner'; import { Dialog, DialogContent, DialogHeader, DialogTitle, - DialogDescription, DialogFooter, } from '@/components/ui/dialog'; +import { toast } from 'sonner'; import { useTranslation } from 'react-i18next'; import { i18nObj } from '@/i18n/I18nProvider'; @@ -41,17 +40,25 @@ export default function PipelineFormComponent({ onNewPipelineCreated, isEditMode, pipelineId, + showButtons = true, + onDeletePipeline, + onCancel, }: { pipelineId?: string; isDefaultPipeline: boolean; isEditMode: boolean; disableForm: boolean; + showButtons?: boolean; // 这里的写法很不安全不规范,未来流水线需要重新整理 initValues?: PipelineFormEntity; onFinish: () => void; onNewPipelineCreated: (pipelineId: string) => void; + onDeletePipeline: () => void; + onCancel: () => void; }) { const { t } = useTranslation(); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const formSchema = isEditMode ? z.object({ basic: z.object({ @@ -98,7 +105,6 @@ export default function PipelineFormComponent({ useState(); const [outputConfigTabSchema, setOutputConfigTabSchema] = useState(); - const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false); const form = useForm({ resolver: zodResolver(formSchema), @@ -306,187 +312,191 @@ export default function PipelineFormComponent({ ); } - function deletePipeline() { - httpClient - .deletePipeline(pipelineId || '') - .then(() => { - onFinish(); - toast.success(t('common.deleteSuccess')); - }) - .catch((err) => { - toast.error(t('common.deleteError') + err.message); - }); - } + const handleDelete = () => { + setShowDeleteConfirm(true); + }; + + const confirmDelete = () => { + if (pipelineId) { + httpClient + .deletePipeline(pipelineId) + .then(() => { + onDeletePipeline(); + setShowDeleteConfirm(false); + toast.success(t('pipelines.deleteSuccess')); + }) + .catch((err) => { + toast.error(t('pipelines.deleteError') + err.message); + }); + } + }; return ( -
- - - - {t('common.confirmDelete')} - - - {t('pipelines.deleteConfirmation')} - - - - - - - - -
- - - - {formLabelList.map((formLabel) => ( - - {formLabel.label} - - ))} - - - {formLabelList.map((formLabel) => ( - +
+ + +
+ -

{formLabel.label}

+ + {formLabelList.map((formLabel) => ( + + {formLabel.label} + + ))} + - {formLabel.name === 'basic' && ( -
- ( - - - {t('common.name')} - * - - - - - - +
+ {formLabelList.map((formLabel) => ( + + {formLabel.name === 'basic' && ( +
+ ( + + + {t('common.name')} + * + + + + + + + )} + /> + + ( + + + {t('common.description')} + * + + + + + + + )} + /> +
)} - /> - ( - - - {t('common.description')} - * - - - - - - + {isEditMode && ( + <> + {formLabel.name === 'ai' && aiConfigTabSchema && ( +
+ {aiConfigTabSchema.stages.map((stage) => + renderDynamicForms(stage, 'ai'), + )} +
+ )} + + {formLabel.name === 'trigger' && + triggerConfigTabSchema && ( +
+ {triggerConfigTabSchema.stages.map((stage) => + renderDynamicForms(stage, 'trigger'), + )} +
+ )} + + {formLabel.name === 'safety' && + safetyConfigTabSchema && ( +
+ {safetyConfigTabSchema.stages.map((stage) => + renderDynamicForms(stage, 'safety'), + )} +
+ )} + + {formLabel.name === 'output' && + outputConfigTabSchema && ( +
+ {outputConfigTabSchema.stages.map((stage) => + renderDynamicForms(stage, 'output'), + )} +
+ )} + )} - /> -
- )} - - {isEditMode && ( - <> - {formLabel.name === 'ai' && aiConfigTabSchema && ( -
- {aiConfigTabSchema.stages.map((stage) => - renderDynamicForms(stage, 'ai'), - )} -
- )} - - {formLabel.name === 'trigger' && triggerConfigTabSchema && ( -
- {triggerConfigTabSchema.stages.map((stage) => - renderDynamicForms(stage, 'trigger'), - )} -
- )} - - {formLabel.name === 'safety' && safetyConfigTabSchema && ( -
- {safetyConfigTabSchema.stages.map((stage) => - renderDynamicForms(stage, 'safety'), - )} -
- )} - - {formLabel.name === 'output' && outputConfigTabSchema && ( -
- {outputConfigTabSchema.stages.map((stage) => - renderDynamicForms(stage, 'output'), - )} -
- )} - - )} - - ))} - - -
-
- {isEditMode && isDefaultPipeline && ( - - {t('pipelines.defaultPipelineCannotDelete')} - - )} - + + ))} +
+ +
+ + {/* 按钮栏移到 Tabs 外部,始终固定底部 */} + {showButtons && ( +
{isEditMode && !isDefaultPipeline && ( )} - -
-
- - -
+ )} + +
+ + {/* 删除确认对话框 */} + + + + {t('common.confirmDelete')} + +
{t('pipelines.deleteConfirmation')}
+ + + + +
+
+ ); } - interface FormLabel { label: string; name: string; diff --git a/web/src/app/home/pipelines/debug-dialog/DebugDialog.tsx b/web/src/app/home/pipelines/debug-dialog/DebugDialog.tsx deleted file mode 100644 index 6e428635..00000000 --- a/web/src/app/home/pipelines/debug-dialog/DebugDialog.tsx +++ /dev/null @@ -1,422 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react'; -import { useTranslation } from 'react-i18next'; -import { httpClient } from '@/app/infra/http/HttpClient'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select'; -import { ScrollArea } from '@/components/ui/scroll-area'; -import { cn } from '@/lib/utils'; -import { Pipeline } from '@/app/infra/entities/api'; -import { Message } from '@/app/infra/entities/message'; -import { toast } from 'sonner'; -import AtBadge from './AtBadge'; - -interface MessageComponent { - type: 'At' | 'Plain'; - target?: string; - text?: string; -} - -interface DebugDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - pipelineId: string; -} - -export default function DebugDialog({ - open, - onOpenChange, - pipelineId, -}: DebugDialogProps) { - const { t } = useTranslation(); - const [selectedPipelineId, setSelectedPipelineId] = useState(pipelineId); - const [sessionType, setSessionType] = useState<'person' | 'group'>('person'); - const [messages, setMessages] = useState([]); - const [inputValue, setInputValue] = useState(''); - const [pipelines, setPipelines] = useState([]); - const [showAtPopover, setShowAtPopover] = useState(false); - const [hasAt, setHasAt] = useState(false); - const [isHovering, setIsHovering] = useState(false); - const messagesEndRef = useRef(null); - const inputRef = useRef(null); - const popoverRef = useRef(null); - - const scrollToBottom = () => { - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }; - - useEffect(() => { - scrollToBottom(); - }, [messages]); - - useEffect(() => { - if (open) { - setSelectedPipelineId(pipelineId); - loadPipelines(); - loadMessages(pipelineId); - } - }, [open, pipelineId]); - - useEffect(() => { - if (open) { - loadMessages(selectedPipelineId); - } - }, [sessionType, selectedPipelineId]); - - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if ( - popoverRef.current && - !popoverRef.current.contains(event.target as Node) && - !inputRef.current?.contains(event.target as Node) - ) { - setShowAtPopover(false); - } - }; - - document.addEventListener('mousedown', handleClickOutside); - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, []); - - useEffect(() => { - if (showAtPopover) { - setIsHovering(true); - } - }, [showAtPopover]); - - const loadPipelines = async () => { - try { - const response = await httpClient.getPipelines(); - setPipelines(response.pipelines); - } catch (error) { - console.error('Failed to load pipelines:', error); - } - }; - - const loadMessages = async (pipelineId: string) => { - try { - const response = await httpClient.getWebChatHistoryMessages( - pipelineId, - sessionType, - ); - setMessages(response.messages); - } catch (error) { - console.error('Failed to load messages:', error); - } - }; - - const handleInputChange = (e: React.ChangeEvent) => { - const value = e.target.value; - if (sessionType === 'group') { - if (value.endsWith('@')) { - setShowAtPopover(true); - } else if (showAtPopover && (!value.includes('@') || value.length > 1)) { - setShowAtPopover(false); - } - } - setInputValue(value); - }; - - const handleAtSelect = () => { - setHasAt(true); - setShowAtPopover(false); - setInputValue(inputValue.slice(0, -1)); - }; - - const handleAtRemove = () => { - setHasAt(false); - }; - - const handleKeyPress = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - if (showAtPopover) { - handleAtSelect(); - } else { - sendMessage(); - } - } else if (e.key === 'Backspace' && hasAt && inputValue === '') { - handleAtRemove(); - } - }; - - const sendMessage = async () => { - if (!inputValue.trim() && !hasAt) return; - - try { - const messageChain = []; - - let text_content = inputValue.trim(); - if (hasAt) { - text_content = ' ' + text_content; - } - - if (hasAt) { - messageChain.push({ - type: 'At', - target: 'webchatbot', - }); - } - messageChain.push({ - type: 'Plain', - text: text_content, - }); - - if (hasAt) { - // for showing - text_content = '@webchatbot' + text_content; - } - - const userMessage: Message = { - id: -1, - role: 'user', - content: text_content, - timestamp: new Date().toISOString(), - message_chain: messageChain, - }; - - setMessages((prevMessages) => [...prevMessages, userMessage]); - setInputValue(''); - setHasAt(false); - - const response = await httpClient.sendWebChatMessage( - sessionType, - messageChain, - selectedPipelineId, - 120000, - ); - - setMessages((prevMessages) => [...prevMessages, response.message]); - } catch ( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - error: any - ) { - console.log(error, 'type of error', typeof error); - console.error('Failed to send message:', error); - - if (!error.message.includes('timeout') && sessionType === 'person') { - toast.error(t('pipelines.debugDialog.sendFailed')); - } - } finally { - inputRef.current?.focus(); - } - }; - - // const resetSession = async () => { - // try { - // await httpClient.resetWebChatSession(selectedPipelineId, sessionType); - // setMessages([]); - // } catch (error) { - // console.error('Failed to reset session:', error); - // } - // }; - - const renderMessageContent = (message: Message) => { - return ( - - {(message.message_chain as MessageComponent[]).map( - (component, index) => { - if (component.type === 'At') { - return ( - - ); - } else if (component.type === 'Plain') { - return {component.text}; - } - return null; - }, - )} - - ); - }; - - return ( - - - -
- - {t('pipelines.debugDialog.title')} - - -
-
-
-
-
- - -
-
-
- -
- -
- {messages.length === 0 ? ( -
- {t('pipelines.debugDialog.noMessages')} -
- ) : ( - messages.map((message) => ( -
-
- {renderMessageContent(message)} -
- {message.role === 'user' - ? t('pipelines.debugDialog.userMessage') - : t('pipelines.debugDialog.botMessage')} -
-
-
- )) - )} -
-
- - -
-
- {hasAt && ( - - )} -
- - {showAtPopover && ( -
-
setIsHovering(true)} - onMouseLeave={() => setIsHovering(false)} - > - - @webchatbot - {t('pipelines.debugDialog.atTips')} - -
-
- )} -
-
- -
-
-
- -
- ); -} diff --git a/web/src/app/home/pipelines/page.tsx b/web/src/app/home/pipelines/page.tsx index fb17e6e6..40875f6e 100644 --- a/web/src/app/home/pipelines/page.tsx +++ b/web/src/app/home/pipelines/page.tsx @@ -1,25 +1,18 @@ 'use client'; import { useState, useEffect } from 'react'; import CreateCardComponent from '@/app/infra/basic-component/create-card-component/CreateCardComponent'; -import PipelineFormComponent from './components/pipeline-form/PipelineFormComponent'; import { httpClient } from '@/app/infra/http/HttpClient'; import { PipelineCardVO } from '@/app/home/pipelines/components/pipeline-card/PipelineCardVO'; import PipelineCard from '@/app/home/pipelines/components/pipeline-card/PipelineCard'; import { PipelineFormEntity } from '@/app/infra/entities/pipeline'; import styles from './pipelineConfig.module.css'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; import { toast } from 'sonner'; import { useTranslation } from 'react-i18next'; -import DebugDialog from './debug-dialog/DebugDialog'; +import PipelineDialog from './PipelineDetailDialog'; export default function PluginConfigPage() { const { t } = useTranslation(); - const [modalOpen, setModalOpen] = useState(false); + const [dialogOpen, setDialogOpen] = useState(false); const [isEditForm, setIsEditForm] = useState(false); const [pipelineList, setPipelineList] = useState([]); const [selectedPipelineId, setSelectedPipelineId] = useState(''); @@ -31,11 +24,8 @@ export default function PluginConfigPage() { safety: {}, output: {}, }); - const [disableForm, setDisableForm] = useState(false); const [selectedPipelineIsDefault, setSelectedPipelineIsDefault] = useState(false); - const [debugDialogOpen, setDebugDialogOpen] = useState(false); - const [debugPipelineId, setDebugPipelineId] = useState(''); useEffect(() => { getPipelines(); @@ -92,83 +82,77 @@ export default function PluginConfigPage() { trigger: value.pipeline.config.trigger, }); setSelectedPipelineIsDefault(value.pipeline.is_default ?? false); - setDisableForm(false); }); } - const handleDebug = (pipelineId: string) => { - setDebugPipelineId(pipelineId); - setDebugDialogOpen(true); + const handlePipelineClick = (pipelineId: string) => { + setSelectedPipelineId(pipelineId); + setIsEditForm(true); + setDialogOpen(true); + getSelectedPipelineForm(pipelineId); + }; + + const handleCreateNew = () => { + setIsEditForm(false); + setSelectedPipelineId(''); + setSelectedPipelineFormValue({ + basic: {}, + ai: {}, + trigger: {}, + safety: {}, + output: {}, + }); + setSelectedPipelineIsDefault(false); + setDialogOpen(true); }; return (
- - - - - {isEditForm - ? t('pipelines.editPipeline') - : t('pipelines.createPipeline')} - - -
- { - setDisableForm(true); - setIsEditForm(true); - setModalOpen(true); - setSelectedPipelineId(pipelineId); - getSelectedPipelineForm(pipelineId); - }} - onFinish={() => { - getPipelines(); - setModalOpen(false); - }} - isEditMode={isEditForm} - pipelineId={selectedPipelineId} - disableForm={disableForm} - initValues={selectedPipelineFormValue} - isDefaultPipeline={selectedPipelineIsDefault} - /> -
-
-
+ { + getPipelines(); + }} + onNewPipelineCreated={(pipelineId) => { + getPipelines(); + setSelectedPipelineId(pipelineId); + setIsEditForm(true); + setDialogOpen(true); + getSelectedPipelineForm(pipelineId); + }} + onDeletePipeline={() => { + getPipelines(); + setDialogOpen(false); + }} + onCancel={() => { + setDialogOpen(false); + }} + />
{ - setIsEditForm(false); - setModalOpen(true); - }} + onClick={handleCreateNew} /> {pipelineList.map((pipeline) => { return (
{ - setDisableForm(true); - setIsEditForm(true); - setModalOpen(true); - setSelectedPipelineId(pipeline.id); - getSelectedPipelineForm(pipeline.id); - }} + onClick={() => handlePipelineClick(pipeline.id)} > - +
); })}
- -
); } diff --git a/web/src/components/ui/breadcrumb.tsx b/web/src/components/ui/breadcrumb.tsx new file mode 100644 index 00000000..74fa1f4d --- /dev/null +++ b/web/src/components/ui/breadcrumb.tsx @@ -0,0 +1,109 @@ +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { ChevronRight, MoreHorizontal } from 'lucide-react'; + +import { cn } from '@/lib/utils'; + +function Breadcrumb({ ...props }: React.ComponentProps<'nav'>) { + return