mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-26 12:17:14 +00:00
feat(web): edit entity details from page titles
This commit is contained in:
@@ -692,6 +692,17 @@ class BotService:
|
|||||||
)
|
)
|
||||||
if getattr(result, 'rowcount', None) == 0:
|
if getattr(result, 'rowcount', None) == 0:
|
||||||
raise WorkspaceNotFoundError('Bot not found')
|
raise WorkspaceNotFoundError('Bot not found')
|
||||||
|
|
||||||
|
runtime_fields = {'adapter', 'adapter_config', 'enable', 'event_bindings'}
|
||||||
|
if not runtime_fields.intersection(update_data):
|
||||||
|
runtime_bot = await self.ap.platform_mgr.get_bot_by_uuid(context, bot_uuid)
|
||||||
|
if runtime_bot is not None:
|
||||||
|
if 'name' in update_data:
|
||||||
|
runtime_bot.bot_entity.name = update_data['name']
|
||||||
|
if 'description' in update_data:
|
||||||
|
runtime_bot.bot_entity.description = update_data['description']
|
||||||
|
return
|
||||||
|
|
||||||
await self.ap.platform_mgr.remove_bot(context, bot_uuid)
|
await self.ap.platform_mgr.remove_bot(context, bot_uuid)
|
||||||
|
|
||||||
# select from db
|
# select from db
|
||||||
|
|||||||
@@ -443,6 +443,7 @@ class TestBotServiceUpdateBot:
|
|||||||
ap.persistence_mgr = SimpleNamespace()
|
ap.persistence_mgr = SimpleNamespace()
|
||||||
ap.platform_mgr = SimpleNamespace()
|
ap.platform_mgr = SimpleNamespace()
|
||||||
ap.platform_mgr.remove_bot = AsyncMock()
|
ap.platform_mgr.remove_bot = AsyncMock()
|
||||||
|
ap.platform_mgr.get_bot_by_uuid = AsyncMock(return_value=None)
|
||||||
|
|
||||||
# Mock pipeline query - not updating pipeline
|
# Mock pipeline query - not updating pipeline
|
||||||
ap.persistence_mgr.execute_async = AsyncMock()
|
ap.persistence_mgr.execute_async = AsyncMock()
|
||||||
@@ -473,6 +474,7 @@ class TestBotServiceUpdateBot:
|
|||||||
|
|
||||||
ap.persistence_mgr.execute_async = AsyncMock(return_value=Mock())
|
ap.persistence_mgr.execute_async = AsyncMock(return_value=Mock())
|
||||||
ap.platform_mgr = SimpleNamespace(
|
ap.platform_mgr = SimpleNamespace(
|
||||||
|
get_bot_by_uuid=AsyncMock(return_value=None),
|
||||||
remove_bot=AsyncMock(),
|
remove_bot=AsyncMock(),
|
||||||
load_bot=AsyncMock(return_value=SimpleNamespace(enable=False)),
|
load_bot=AsyncMock(return_value=SimpleNamespace(enable=False)),
|
||||||
)
|
)
|
||||||
@@ -496,6 +498,29 @@ class TestBotServiceUpdateBot:
|
|||||||
assert 'use_pipeline_uuid' not in update_params
|
assert 'use_pipeline_uuid' not in update_params
|
||||||
assert 'use_pipeline_name' not in update_params
|
assert 'use_pipeline_name' not in update_params
|
||||||
|
|
||||||
|
async def test_basic_info_update_does_not_restart_platform_adapter(self):
|
||||||
|
ap = SimpleNamespace()
|
||||||
|
ap.persistence_mgr = SimpleNamespace(execute_async=AsyncMock(return_value=SimpleNamespace(rowcount=1)))
|
||||||
|
runtime_entity = SimpleNamespace(name='Old name', description='Old description')
|
||||||
|
runtime_bot = SimpleNamespace(bot_entity=runtime_entity)
|
||||||
|
ap.platform_mgr = SimpleNamespace(
|
||||||
|
get_bot_by_uuid=AsyncMock(return_value=runtime_bot),
|
||||||
|
remove_bot=AsyncMock(),
|
||||||
|
load_bot=AsyncMock(),
|
||||||
|
)
|
||||||
|
|
||||||
|
service = BotService(ap)
|
||||||
|
await service.update_bot(
|
||||||
|
WORKSPACE_UUID,
|
||||||
|
'test-uuid',
|
||||||
|
{'name': 'New name', 'description': 'New description'},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert runtime_entity.name == 'New name'
|
||||||
|
assert runtime_entity.description == 'New description'
|
||||||
|
ap.platform_mgr.remove_bot.assert_not_awaited()
|
||||||
|
ap.platform_mgr.load_bot.assert_not_awaited()
|
||||||
|
|
||||||
|
|
||||||
class TestBotServiceDeleteBot:
|
class TestBotServiceDeleteBot:
|
||||||
"""Tests for delete_bot method."""
|
"""Tests for delete_bot method."""
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { toast } from 'sonner';
|
||||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
import { useCurrentWorkspace } from '@/app/infra/http';
|
import { useCurrentWorkspace } from '@/app/infra/http';
|
||||||
import { Agent } from '@/app/infra/entities/api';
|
import { Agent } from '@/app/infra/entities/api';
|
||||||
import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext';
|
import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext';
|
||||||
import ProcessorDetailWorkbench from '@/app/home/components/processor-detail/ProcessorDetailWorkbench';
|
import ProcessorDetailWorkbench from '@/app/home/components/processor-detail/ProcessorDetailWorkbench';
|
||||||
|
import EntityBasicInfoDialog, {
|
||||||
|
EntityBasicInfoValues,
|
||||||
|
} from '@/app/home/components/entity-basic-info/EntityBasicInfoDialog';
|
||||||
|
import EntityTitleEditButton from '@/app/home/components/entity-basic-info/EntityTitleEditButton';
|
||||||
import PipelineDetailContent from '@/app/home/pipelines/PipelineDetailContent';
|
import PipelineDetailContent from '@/app/home/pipelines/PipelineDetailContent';
|
||||||
import AgentCreateContent from './components/AgentCreateContent';
|
import AgentCreateContent from './components/AgentCreateContent';
|
||||||
import AgentDebugPanel from './components/AgentDebugPanel';
|
import AgentDebugPanel from './components/AgentDebugPanel';
|
||||||
@@ -28,6 +33,7 @@ export default function AgentDetailContent({ id }: { id: string }) {
|
|||||||
const [loading, setLoading] = useState(!isCreateMode);
|
const [loading, setLoading] = useState(!isCreateMode);
|
||||||
const [formDirty, setFormDirty] = useState(false);
|
const [formDirty, setFormDirty] = useState(false);
|
||||||
const [formSaving, setFormSaving] = useState(false);
|
const [formSaving, setFormSaving] = useState(false);
|
||||||
|
const [basicInfoOpen, setBasicInfoOpen] = useState(false);
|
||||||
const [runnerStatus, setRunnerStatus] = useState<AgentRunnerStatus | null>(
|
const [runnerStatus, setRunnerStatus] = useState<AgentRunnerStatus | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
@@ -88,58 +94,93 @@ export default function AgentDetailContent({ id }: { id: string }) {
|
|||||||
return <PipelineDetailContent id={id} routeBase="/home/agents" />;
|
return <PipelineDetailContent id={id} routeBase="/home/agents" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveBasicInfo(values: EntityBasicInfoValues) {
|
||||||
|
try {
|
||||||
|
await httpClient.updateAgent(id, values);
|
||||||
|
setAgent((current) => (current ? { ...current, ...values } : current));
|
||||||
|
agentFormRef.current?.syncBasicInfo(values);
|
||||||
|
await refreshPipelines();
|
||||||
|
toast.success(t('agents.saveSuccess'));
|
||||||
|
} catch (error) {
|
||||||
|
const message =
|
||||||
|
typeof error === 'object' && error && 'msg' in error
|
||||||
|
? String((error as { msg?: string }).msg || '')
|
||||||
|
: '';
|
||||||
|
toast.error(t('agents.saveError') + message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProcessorDetailWorkbench
|
<>
|
||||||
key={id}
|
<ProcessorDetailWorkbench
|
||||||
title={`${agent.emoji || '🤖'} ${agent.name}`}
|
key={id}
|
||||||
status={runnerStatus}
|
title={`${agent.emoji || '🤖'} ${agent.name}`}
|
||||||
saveLabel={t('common.save')}
|
titleAction={
|
||||||
saveFormId="agent-form"
|
canManage ? (
|
||||||
canSave={canManage}
|
<EntityTitleEditButton onClick={() => setBasicInfoOpen(true)} />
|
||||||
isDirty={formDirty}
|
) : undefined
|
||||||
isSaving={formSaving}
|
}
|
||||||
configTitle={t('pipelines.configuration')}
|
status={runnerStatus}
|
||||||
configContent={
|
saveLabel={t('common.save')}
|
||||||
<fieldset className="contents" disabled={!canManage}>
|
saveFormId="agent-form"
|
||||||
<AgentFormComponent
|
canSave={canManage}
|
||||||
ref={agentFormRef}
|
isDirty={formDirty}
|
||||||
agentId={id}
|
isSaving={formSaving}
|
||||||
onFinish={(updatedAgent) => {
|
configTitle={t('pipelines.configuration')}
|
||||||
if (updatedAgent) {
|
configContent={
|
||||||
setAgent((current) =>
|
<fieldset className="contents" disabled={!canManage}>
|
||||||
current ? { ...current, ...updatedAgent } : current,
|
<AgentFormComponent
|
||||||
);
|
ref={agentFormRef}
|
||||||
|
agentId={id}
|
||||||
|
onFinish={(updatedAgent) => {
|
||||||
|
if (updatedAgent) {
|
||||||
|
setAgent((current) =>
|
||||||
|
current ? { ...current, ...updatedAgent } : current,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
refreshPipelines();
|
||||||
|
}}
|
||||||
|
onDeleted={() => {
|
||||||
|
refreshPipelines();
|
||||||
|
navigate('/home/agents');
|
||||||
|
}}
|
||||||
|
onDirtyChange={setFormDirty}
|
||||||
|
onSavingChange={setFormSaving}
|
||||||
|
onRunnerStatusChange={setRunnerStatus}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
}
|
||||||
|
debugTitle={canOperate ? t('agents.debugTab') : undefined}
|
||||||
|
debugContent={
|
||||||
|
canOperate ? (
|
||||||
|
<AgentDebugPanel
|
||||||
|
agentId={id}
|
||||||
|
hasUnsavedChanges={formDirty}
|
||||||
|
beforeRun={async () => agentFormRef.current?.save() ?? false}
|
||||||
|
onOpenRunnerConfig={() =>
|
||||||
|
agentFormRef.current?.openSection('runner_config')
|
||||||
}
|
}
|
||||||
refreshPipelines();
|
supportedEventPatterns={
|
||||||
}}
|
agent.supported_event_patterns ??
|
||||||
onDeleted={() => {
|
agent.capability?.supported_event_patterns ?? ['*']
|
||||||
refreshPipelines();
|
}
|
||||||
navigate('/home/agents');
|
/>
|
||||||
}}
|
) : undefined
|
||||||
onDirtyChange={setFormDirty}
|
}
|
||||||
onSavingChange={setFormSaving}
|
unsavedLabel={t('pipelines.unsavedChanges')}
|
||||||
onRunnerStatusChange={setRunnerStatus}
|
/>
|
||||||
/>
|
<EntityBasicInfoDialog
|
||||||
</fieldset>
|
open={basicInfoOpen}
|
||||||
}
|
onOpenChange={setBasicInfoOpen}
|
||||||
debugTitle={canOperate ? t('agents.debugTab') : undefined}
|
values={{
|
||||||
debugContent={
|
name: agent.name,
|
||||||
canOperate ? (
|
description: agent.description || '',
|
||||||
<AgentDebugPanel
|
emoji: agent.emoji || '🤖',
|
||||||
agentId={id}
|
}}
|
||||||
hasUnsavedChanges={formDirty}
|
defaultEmoji="🤖"
|
||||||
beforeRun={async () => agentFormRef.current?.save() ?? false}
|
onSave={saveBasicInfo}
|
||||||
onOpenRunnerConfig={() =>
|
/>
|
||||||
agentFormRef.current?.openSection('runner_config')
|
</>
|
||||||
}
|
|
||||||
supportedEventPatterns={
|
|
||||||
agent.supported_event_patterns ??
|
|
||||||
agent.capability?.supported_event_patterns ?? ['*']
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : undefined
|
|
||||||
}
|
|
||||||
unsavedLabel={t('pipelines.unsavedChanges')}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Bot, Info, Power, SlidersHorizontal, Trash2, Zap } from 'lucide-react';
|
import { Bot, Power, SlidersHorizontal, Trash2, Zap } from 'lucide-react';
|
||||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
import { Agent, ApiRespPluginSystemStatus } from '@/app/infra/entities/api';
|
import { Agent, ApiRespPluginSystemStatus } from '@/app/infra/entities/api';
|
||||||
import {
|
import {
|
||||||
@@ -25,9 +25,7 @@ import { extractI18nObject } from '@/i18n/I18nProvider';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import EmojiPicker from '@/components/ui/emoji-picker';
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -68,11 +66,19 @@ interface AgentFormComponentProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type AgentConfigSection =
|
export type AgentConfigSection =
|
||||||
'events' | 'runner' | 'runner_config' | 'basic';
|
| 'events'
|
||||||
|
| 'runner'
|
||||||
|
| 'runner_config'
|
||||||
|
| 'basic';
|
||||||
|
|
||||||
export interface AgentFormHandle {
|
export interface AgentFormHandle {
|
||||||
openSection: (section: AgentConfigSection) => void;
|
openSection: (section: AgentConfigSection) => void;
|
||||||
save: () => Promise<boolean>;
|
save: () => Promise<boolean>;
|
||||||
|
syncBasicInfo: (values: {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
emoji?: string;
|
||||||
|
}) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isRequiredRunnerValueMissing(value: unknown): boolean {
|
function isRequiredRunnerValueMissing(value: unknown): boolean {
|
||||||
@@ -266,8 +272,8 @@ function AgentFormComponent(
|
|||||||
}> = [
|
}> = [
|
||||||
{
|
{
|
||||||
name: 'basic',
|
name: 'basic',
|
||||||
label: t('agents.basicInfo'),
|
label: t('common.management'),
|
||||||
icon: Info,
|
icon: Power,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'events',
|
name: 'events',
|
||||||
@@ -503,6 +509,24 @@ function AgentFormComponent(
|
|||||||
ref,
|
ref,
|
||||||
() => ({
|
() => ({
|
||||||
openSection: setActiveSection,
|
openSection: setActiveSection,
|
||||||
|
syncBasicInfo(values) {
|
||||||
|
form.setValue('basic', {
|
||||||
|
...form.getValues('basic'),
|
||||||
|
name: values.name,
|
||||||
|
description: values.description,
|
||||||
|
emoji: values.emoji || '🤖',
|
||||||
|
});
|
||||||
|
if (savedSnapshotRef.current) {
|
||||||
|
const snapshot = JSON.parse(savedSnapshotRef.current) as FormValues;
|
||||||
|
snapshot.basic = {
|
||||||
|
...snapshot.basic,
|
||||||
|
name: values.name,
|
||||||
|
description: values.description,
|
||||||
|
emoji: values.emoji || '🤖',
|
||||||
|
};
|
||||||
|
savedSnapshotRef.current = JSON.stringify(snapshot);
|
||||||
|
}
|
||||||
|
},
|
||||||
async save() {
|
async save() {
|
||||||
if (!hasUnsavedChangesRef.current) return true;
|
if (!hasUnsavedChangesRef.current) return true;
|
||||||
if (isSavingRef.current) return false;
|
if (isSavingRef.current) return false;
|
||||||
@@ -634,62 +658,12 @@ function AgentFormComponent(
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{t('agents.basicInfo')}</CardTitle>
|
<CardTitle>{t('agents.availability')}</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
{t('agents.basicInfoDescription')}
|
{t('agents.availabilityDescription')}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent>
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="basic.name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex-1">
|
|
||||||
<FormLabel>
|
|
||||||
{t('common.name')}
|
|
||||||
<span className="text-destructive">*</span>
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} value={field.value ?? ''} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="basic.emoji"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('common.icon')}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<EmojiPicker
|
|
||||||
value={field.value}
|
|
||||||
onChange={field.onChange}
|
|
||||||
ariaLabel={t('common.icon')}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="basic.description"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('common.description')}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} value={field.value ?? ''} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="basic.enabled"
|
name="basic.enabled"
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ import {
|
|||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import BotForm from '@/app/home/bots/components/bot-form/BotForm';
|
import BotForm, {
|
||||||
|
BotFormHandle,
|
||||||
|
} from '@/app/home/bots/components/bot-form/BotForm';
|
||||||
import { BotLogListComponent } from '@/app/home/bots/components/bot-log/view/BotLogListComponent';
|
import { BotLogListComponent } from '@/app/home/bots/components/bot-log/view/BotLogListComponent';
|
||||||
import BotSessionMonitor from '@/app/home/bots/components/bot-session/BotSessionMonitor';
|
import BotSessionMonitor from '@/app/home/bots/components/bot-session/BotSessionMonitor';
|
||||||
import type { BotSessionMonitorHandle } from '@/app/home/bots/components/bot-session/BotSessionMonitor';
|
import type { BotSessionMonitorHandle } from '@/app/home/bots/components/bot-session/BotSessionMonitor';
|
||||||
@@ -30,6 +32,11 @@ import { Settings, FileText, Users, RefreshCw, Trash2 } from 'lucide-react';
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useCurrentWorkspace } from '@/app/infra/http';
|
import { useCurrentWorkspace } from '@/app/infra/http';
|
||||||
|
import { Bot } from '@/app/infra/entities/api';
|
||||||
|
import EntityBasicInfoDialog, {
|
||||||
|
EntityBasicInfoValues,
|
||||||
|
} from '@/app/home/components/entity-basic-info/EntityBasicInfoDialog';
|
||||||
|
import EntityTitleEditButton from '@/app/home/components/entity-basic-info/EntityTitleEditButton';
|
||||||
|
|
||||||
export default function BotDetailContent({ id }: { id: string }) {
|
export default function BotDetailContent({ id }: { id: string }) {
|
||||||
const isCreateMode = id === 'new';
|
const isCreateMode = id === 'new';
|
||||||
@@ -55,8 +62,11 @@ export default function BotDetailContent({ id }: { id: string }) {
|
|||||||
|
|
||||||
const [activeTab, setActiveTab] = useState('config');
|
const [activeTab, setActiveTab] = useState('config');
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
|
const [basicInfoOpen, setBasicInfoOpen] = useState(false);
|
||||||
|
const [bot, setBot] = useState<Bot | null>(null);
|
||||||
const [isRefreshingSessions, setIsRefreshingSessions] = useState(false);
|
const [isRefreshingSessions, setIsRefreshingSessions] = useState(false);
|
||||||
const sessionMonitorRef = useRef<BotSessionMonitorHandle>(null);
|
const sessionMonitorRef = useRef<BotSessionMonitorHandle>(null);
|
||||||
|
const botFormRef = useRef<BotFormHandle>(null);
|
||||||
|
|
||||||
// Track whether the form has unsaved changes
|
// Track whether the form has unsaved changes
|
||||||
const [formDirty, setFormDirty] = useState(false);
|
const [formDirty, setFormDirty] = useState(false);
|
||||||
@@ -69,6 +79,7 @@ export default function BotDetailContent({ id }: { id: string }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isCreateMode) {
|
if (!isCreateMode) {
|
||||||
httpClient.getBot(id).then((res) => {
|
httpClient.getBot(id).then((res) => {
|
||||||
|
setBot(res.bot);
|
||||||
setBotEnabled(res.bot.enable ?? true);
|
setBotEnabled(res.bot.enable ?? true);
|
||||||
setEnableLoaded(true);
|
setEnableLoaded(true);
|
||||||
});
|
});
|
||||||
@@ -80,16 +91,10 @@ export default function BotDetailContent({ id }: { id: string }) {
|
|||||||
const prev = botEnabled;
|
const prev = botEnabled;
|
||||||
setBotEnabled(checked);
|
setBotEnabled(checked);
|
||||||
try {
|
try {
|
||||||
// Fetch current bot data to send a complete update
|
await httpClient.updateBot(id, { enable: checked });
|
||||||
const res = await httpClient.getBot(id);
|
setBot((current) =>
|
||||||
const bot = res.bot;
|
current ? { ...current, enable: checked } : current,
|
||||||
await httpClient.updateBot(id, {
|
);
|
||||||
name: bot.name,
|
|
||||||
description: bot.description,
|
|
||||||
adapter: bot.adapter,
|
|
||||||
adapter_config: bot.adapter_config,
|
|
||||||
enable: checked,
|
|
||||||
});
|
|
||||||
refreshBots();
|
refreshBots();
|
||||||
} catch {
|
} catch {
|
||||||
setBotEnabled(prev);
|
setBotEnabled(prev);
|
||||||
@@ -102,6 +107,7 @@ export default function BotDetailContent({ id }: { id: string }) {
|
|||||||
function handleFormSubmit() {
|
function handleFormSubmit() {
|
||||||
// Re-sync enable state after form save (form may update enable too)
|
// Re-sync enable state after form save (form may update enable too)
|
||||||
httpClient.getBot(id).then((res) => {
|
httpClient.getBot(id).then((res) => {
|
||||||
|
setBot(res.bot);
|
||||||
setBotEnabled(res.bot.enable ?? true);
|
setBotEnabled(res.bot.enable ?? true);
|
||||||
});
|
});
|
||||||
refreshBots();
|
refreshBots();
|
||||||
@@ -117,6 +123,26 @@ export default function BotDetailContent({ id }: { id: string }) {
|
|||||||
navigate(`/home/bots?id=${encodeURIComponent(newBotId)}`);
|
navigate(`/home/bots?id=${encodeURIComponent(newBotId)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveBasicInfo(values: EntityBasicInfoValues) {
|
||||||
|
try {
|
||||||
|
await httpClient.updateBot(id, {
|
||||||
|
name: values.name,
|
||||||
|
description: values.description,
|
||||||
|
});
|
||||||
|
setBot((current) => (current ? { ...current, ...values } : current));
|
||||||
|
botFormRef.current?.syncBasicInfo(values);
|
||||||
|
await refreshBots();
|
||||||
|
toast.success(t('bots.saveSuccess'));
|
||||||
|
} catch (error) {
|
||||||
|
const message =
|
||||||
|
typeof error === 'object' && error && 'msg' in error
|
||||||
|
? String((error as { msg?: string }).msg || '')
|
||||||
|
: '';
|
||||||
|
toast.error(t('bots.saveError') + message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function confirmDelete() {
|
function confirmDelete() {
|
||||||
httpClient
|
httpClient
|
||||||
.deleteBot(id)
|
.deleteBot(id)
|
||||||
@@ -166,8 +192,15 @@ export default function BotDetailContent({ id }: { id: string }) {
|
|||||||
<div className="flex h-full min-w-0 flex-col">
|
<div className="flex h-full min-w-0 flex-col">
|
||||||
{/* Sticky Header: title + enable switch + save button */}
|
{/* Sticky Header: title + enable switch + save button */}
|
||||||
<div className="flex items-center justify-between pb-4 shrink-0">
|
<div className="flex items-center justify-between pb-4 shrink-0">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex min-w-0 items-center gap-4">
|
||||||
<h1 className="text-xl font-semibold">{t('bots.editBot')}</h1>
|
<div className="flex min-w-0 items-center gap-1">
|
||||||
|
<h1 className="truncate text-xl font-semibold">
|
||||||
|
{bot?.name || t('bots.editBot')}
|
||||||
|
</h1>
|
||||||
|
{canManage && (
|
||||||
|
<EntityTitleEditButton onClick={() => setBasicInfoOpen(true)} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{enableLoaded && (
|
{enableLoaded && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Switch
|
<Switch
|
||||||
@@ -258,6 +291,7 @@ export default function BotDetailContent({ id }: { id: string }) {
|
|||||||
<div className="mx-auto flex w-full min-w-0 max-w-3xl flex-col gap-6 pb-8">
|
<div className="mx-auto flex w-full min-w-0 max-w-3xl flex-col gap-6 pb-8">
|
||||||
<fieldset className="contents" disabled={!canManage}>
|
<fieldset className="contents" disabled={!canManage}>
|
||||||
<BotForm
|
<BotForm
|
||||||
|
ref={botFormRef}
|
||||||
initBotId={id}
|
initBotId={id}
|
||||||
onFormSubmit={handleFormSubmit}
|
onFormSubmit={handleFormSubmit}
|
||||||
onNewBotCreated={handleNewBotCreated}
|
onNewBotCreated={handleNewBotCreated}
|
||||||
@@ -344,6 +378,17 @@ export default function BotDetailContent({ id }: { id: string }) {
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<EntityBasicInfoDialog
|
||||||
|
open={basicInfoOpen}
|
||||||
|
onOpenChange={setBasicInfoOpen}
|
||||||
|
values={{
|
||||||
|
name: bot?.name || '',
|
||||||
|
description: bot?.description || '',
|
||||||
|
}}
|
||||||
|
showEmoji={false}
|
||||||
|
onSave={saveBasicInfo}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
import React, {
|
||||||
|
forwardRef,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import i18n from 'i18next';
|
import i18n from 'i18next';
|
||||||
import { IChooseAdapterEntity } from '@/app/home/bots/components/bot-form/ChooseEntity';
|
import { IChooseAdapterEntity } from '@/app/home/bots/components/bot-form/ChooseEntity';
|
||||||
import {
|
import {
|
||||||
@@ -79,17 +86,21 @@ const getFormSchema = (t: (key: string) => string) =>
|
|||||||
.optional(),
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function BotForm({
|
export interface BotFormHandle {
|
||||||
initBotId,
|
syncBasicInfo: (values: { name: string; description: string }) => void;
|
||||||
onFormSubmit,
|
}
|
||||||
onNewBotCreated,
|
|
||||||
onDirtyChange,
|
interface BotFormProps {
|
||||||
}: {
|
|
||||||
initBotId?: string;
|
initBotId?: string;
|
||||||
onFormSubmit: (value: z.infer<ReturnType<typeof getFormSchema>>) => void;
|
onFormSubmit: (value: z.infer<ReturnType<typeof getFormSchema>>) => void;
|
||||||
onNewBotCreated: (botId: string) => void;
|
onNewBotCreated: (botId: string) => void;
|
||||||
onDirtyChange?: (dirty: boolean) => void;
|
onDirtyChange?: (dirty: boolean) => void;
|
||||||
}) {
|
}
|
||||||
|
|
||||||
|
const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
|
||||||
|
{ initBotId, onFormSubmit, onNewBotCreated, onDirtyChange },
|
||||||
|
ref,
|
||||||
|
) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const formSchema = getFormSchema(t);
|
const formSchema = getFormSchema(t);
|
||||||
|
|
||||||
@@ -174,6 +185,19 @@ export default function BotForm({
|
|||||||
onDirtyChange?.(isDirty);
|
onDirtyChange?.(isDirty);
|
||||||
}, [isDirty, onDirtyChange]);
|
}, [isDirty, onDirtyChange]);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
syncBasicInfo(values) {
|
||||||
|
form.reset(
|
||||||
|
{
|
||||||
|
...form.getValues(),
|
||||||
|
name: values.name,
|
||||||
|
description: values.description,
|
||||||
|
},
|
||||||
|
{ keepDirtyValues: true },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setBotFormValues();
|
setBotFormValues();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -416,46 +440,47 @@ export default function BotForm({
|
|||||||
className="w-full min-w-0 max-w-full space-y-6"
|
className="w-full min-w-0 max-w-full space-y-6"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
{/* Card 1: Basic Information */}
|
{!initBotId && (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{t('bots.basicInfo')}</CardTitle>
|
<CardTitle>{t('bots.basicInfo')}</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
{t('bots.basicInfoDescription')}
|
{t('bots.basicInfoDescription')}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t('bots.botName')}
|
{t('bots.botName')}
|
||||||
<span className="text-destructive">*</span>
|
<span className="text-destructive">*</span>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="description"
|
name="description"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('bots.botDescription')}</FormLabel>
|
<FormLabel>{t('bots.botDescription')}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Card 2: Adapter Configuration */}
|
{/* Card 2: Adapter Configuration */}
|
||||||
<Card>
|
<Card>
|
||||||
@@ -688,4 +713,6 @@ export default function BotForm({
|
|||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
export default BotForm;
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import { FormEvent, useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import EmojiPicker from '@/components/ui/emoji-picker';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
|
||||||
|
export interface EntityBasicInfoValues {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
emoji?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EntityBasicInfoDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
values: EntityBasicInfoValues;
|
||||||
|
defaultEmoji?: string;
|
||||||
|
showEmoji?: boolean;
|
||||||
|
onSave: (values: EntityBasicInfoValues) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EntityBasicInfoDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
values,
|
||||||
|
defaultEmoji,
|
||||||
|
showEmoji = true,
|
||||||
|
onSave,
|
||||||
|
}: EntityBasicInfoDialogProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [draft, setDraft] = useState(values);
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [nameError, setNameError] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
setDraft({
|
||||||
|
name: values.name,
|
||||||
|
description: values.description,
|
||||||
|
emoji: values.emoji || defaultEmoji,
|
||||||
|
});
|
||||||
|
setNameError(false);
|
||||||
|
}, [defaultEmoji, open, values.description, values.emoji, values.name]);
|
||||||
|
|
||||||
|
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
const name = draft.name.trim();
|
||||||
|
if (!name) {
|
||||||
|
setNameError(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSaving(true);
|
||||||
|
try {
|
||||||
|
await onSave({
|
||||||
|
name,
|
||||||
|
description: draft.description.trim(),
|
||||||
|
emoji: showEmoji ? draft.emoji || defaultEmoji : undefined,
|
||||||
|
});
|
||||||
|
onOpenChange(false);
|
||||||
|
} catch {
|
||||||
|
// The caller presents the entity-specific error message.
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t('common.editBasicInfo')}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{t(
|
||||||
|
showEmoji
|
||||||
|
? 'common.editBasicInfoDescription'
|
||||||
|
: 'common.editBasicInfoDescriptionNoIcon',
|
||||||
|
)}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4 py-5">
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div className="min-w-0 flex-1 space-y-2">
|
||||||
|
<Label htmlFor="entity-basic-name">{t('common.name')}</Label>
|
||||||
|
<Input
|
||||||
|
id="entity-basic-name"
|
||||||
|
value={draft.name}
|
||||||
|
aria-invalid={nameError}
|
||||||
|
onChange={(event) => {
|
||||||
|
setDraft((current) => ({
|
||||||
|
...current,
|
||||||
|
name: event.target.value,
|
||||||
|
}));
|
||||||
|
if (event.target.value.trim()) setNameError(false);
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
{nameError && (
|
||||||
|
<p className="text-sm text-destructive">
|
||||||
|
{t('common.fieldRequired')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showEmoji && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>{t('common.icon')}</Label>
|
||||||
|
<EmojiPicker
|
||||||
|
value={draft.emoji || defaultEmoji}
|
||||||
|
onChange={(emoji) =>
|
||||||
|
setDraft((current) => ({ ...current, emoji }))
|
||||||
|
}
|
||||||
|
ariaLabel={t('common.icon')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="entity-basic-description">
|
||||||
|
{t('common.description')}
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="entity-basic-description"
|
||||||
|
value={draft.description}
|
||||||
|
onChange={(event) =>
|
||||||
|
setDraft((current) => ({
|
||||||
|
...current,
|
||||||
|
description: event.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
disabled={isSaving}
|
||||||
|
>
|
||||||
|
{t('common.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={isSaving}>
|
||||||
|
{isSaving ? t('common.saving') : t('common.save')}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Pencil } from 'lucide-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/ui/tooltip';
|
||||||
|
|
||||||
|
export default function EntityTitleEditButton({
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
onClick: () => void;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="size-8 shrink-0 text-muted-foreground"
|
||||||
|
aria-label={t('common.editBasicInfo')}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<Pencil className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{t('common.editBasicInfo')}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ export interface ProcessorDetailStatus {
|
|||||||
|
|
||||||
interface ProcessorDetailWorkbenchProps {
|
interface ProcessorDetailWorkbenchProps {
|
||||||
title: string;
|
title: string;
|
||||||
|
titleAction?: ReactNode;
|
||||||
status?: ProcessorDetailStatus | null;
|
status?: ProcessorDetailStatus | null;
|
||||||
saveLabel: string;
|
saveLabel: string;
|
||||||
saveFormId: string;
|
saveFormId: string;
|
||||||
@@ -41,6 +42,7 @@ interface ProcessorDetailWorkbenchProps {
|
|||||||
|
|
||||||
export default function ProcessorDetailWorkbench({
|
export default function ProcessorDetailWorkbench({
|
||||||
title,
|
title,
|
||||||
|
titleAction,
|
||||||
status,
|
status,
|
||||||
saveLabel,
|
saveLabel,
|
||||||
saveFormId,
|
saveFormId,
|
||||||
@@ -67,6 +69,7 @@ export default function ProcessorDetailWorkbench({
|
|||||||
<div className="flex shrink-0 flex-wrap items-center justify-between gap-3 pb-4">
|
<div className="flex shrink-0 flex-wrap items-center justify-between gap-3 pb-4">
|
||||||
<div className="flex min-w-0 items-center gap-2">
|
<div className="flex min-w-0 items-center gap-2">
|
||||||
<h1 className="truncate text-xl font-semibold">{title}</h1>
|
<h1 className="truncate text-xl font-semibold">{title}</h1>
|
||||||
|
{titleAction}
|
||||||
{status && (
|
{status && (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
@@ -139,7 +142,10 @@ export default function ProcessorDetailWorkbench({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activeView === 'monitoring' && monitoring ? (
|
{activeView === 'monitoring' && monitoring ? (
|
||||||
<section className="min-h-0 flex-1 overflow-y-auto rounded-xl border bg-card p-4">
|
<section
|
||||||
|
aria-label={monitoring.label}
|
||||||
|
className="min-h-0 flex-1 overflow-y-auto rounded-xl border bg-card p-4"
|
||||||
|
>
|
||||||
{monitoring.content}
|
{monitoring.content}
|
||||||
</section>
|
</section>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { toast } from 'sonner';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import PipelineFormComponent, {
|
import PipelineFormComponent, {
|
||||||
PipelineFormHandle,
|
PipelineFormHandle,
|
||||||
@@ -7,9 +8,15 @@ import PipelineFormComponent, {
|
|||||||
import DebugDialog from '@/app/home/pipelines/components/debug-dialog/DebugDialog';
|
import DebugDialog from '@/app/home/pipelines/components/debug-dialog/DebugDialog';
|
||||||
import PipelineMonitoringTab from '@/app/home/pipelines/components/monitoring-tab/PipelineMonitoringTab';
|
import PipelineMonitoringTab from '@/app/home/pipelines/components/monitoring-tab/PipelineMonitoringTab';
|
||||||
import ProcessorDetailWorkbench from '@/app/home/components/processor-detail/ProcessorDetailWorkbench';
|
import ProcessorDetailWorkbench from '@/app/home/components/processor-detail/ProcessorDetailWorkbench';
|
||||||
|
import EntityBasicInfoDialog, {
|
||||||
|
EntityBasicInfoValues,
|
||||||
|
} from '@/app/home/components/entity-basic-info/EntityBasicInfoDialog';
|
||||||
|
import EntityTitleEditButton from '@/app/home/components/entity-basic-info/EntityTitleEditButton';
|
||||||
import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext';
|
import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useCurrentWorkspace } from '@/app/infra/http';
|
import { useCurrentWorkspace } from '@/app/infra/http';
|
||||||
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
|
import { Pipeline } from '@/app/infra/entities/api';
|
||||||
|
|
||||||
export default function PipelineDetailContent({
|
export default function PipelineDetailContent({
|
||||||
id,
|
id,
|
||||||
@@ -44,13 +51,47 @@ export default function PipelineDetailContent({
|
|||||||
const [isWebSocketConnected, setIsWebSocketConnected] = useState(false);
|
const [isWebSocketConnected, setIsWebSocketConnected] = useState(false);
|
||||||
const [formDirty, setFormDirty] = useState(false);
|
const [formDirty, setFormDirty] = useState(false);
|
||||||
const [formSaving, setFormSaving] = useState(false);
|
const [formSaving, setFormSaving] = useState(false);
|
||||||
|
const [basicInfoOpen, setBasicInfoOpen] = useState(false);
|
||||||
|
const [pipelineDetails, setPipelineDetails] = useState<Pipeline | null>(null);
|
||||||
const pipelineFormRef = useRef<PipelineFormHandle>(null);
|
const pipelineFormRef = useRef<PipelineFormHandle>(null);
|
||||||
const pipeline = pipelines.find((item) => item.id === id);
|
const sidebarPipeline = pipelines.find((item) => item.id === id);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isCreateMode) return;
|
||||||
|
let cancelled = false;
|
||||||
|
httpClient.getPipeline(id).then((response) => {
|
||||||
|
if (!cancelled) setPipelineDetails(response.pipeline);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [id, isCreateMode]);
|
||||||
|
|
||||||
function handleFinish() {
|
function handleFinish() {
|
||||||
refreshPipelines();
|
refreshPipelines();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveBasicInfo(values: EntityBasicInfoValues) {
|
||||||
|
try {
|
||||||
|
await httpClient.updatePipeline(id, values);
|
||||||
|
setPipelineDetails((current) =>
|
||||||
|
current
|
||||||
|
? { ...current, ...values }
|
||||||
|
: ({ ...values, config: {} } as Pipeline),
|
||||||
|
);
|
||||||
|
pipelineFormRef.current?.syncBasicInfo(values);
|
||||||
|
await refreshPipelines();
|
||||||
|
toast.success(t('pipelines.saveSuccess'));
|
||||||
|
} catch (error) {
|
||||||
|
const message =
|
||||||
|
typeof error === 'object' && error && 'msg' in error
|
||||||
|
? String((error as { msg?: string }).msg || '')
|
||||||
|
: '';
|
||||||
|
toast.error(t('pipelines.saveError') + message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleNewPipelineCreated(newPipelineId: string) {
|
function handleNewPipelineCreated(newPipelineId: string) {
|
||||||
refreshPipelines();
|
refreshPipelines();
|
||||||
navigate(`${routeBase}?id=${encodeURIComponent(newPipelineId)}`);
|
navigate(`${routeBase}?id=${encodeURIComponent(newPipelineId)}`);
|
||||||
@@ -97,66 +138,91 @@ export default function PipelineDetailContent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Edit Mode ====================
|
// ==================== Edit Mode ====================
|
||||||
|
const pipelineName =
|
||||||
|
pipelineDetails?.name ||
|
||||||
|
sidebarPipeline?.name ||
|
||||||
|
t('pipelines.editPipeline');
|
||||||
|
const pipelineEmoji =
|
||||||
|
pipelineDetails?.emoji || sidebarPipeline?.emoji || '⚙️';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProcessorDetailWorkbench
|
<>
|
||||||
key={id}
|
<ProcessorDetailWorkbench
|
||||||
title={`${pipeline?.emoji || '⚙️'} ${pipeline?.name || t('pipelines.editPipeline')}`}
|
key={id}
|
||||||
saveLabel={t('common.save')}
|
title={`${pipelineEmoji} ${pipelineName}`}
|
||||||
saveFormId="pipeline-form"
|
titleAction={
|
||||||
canSave={canManage}
|
canManage ? (
|
||||||
isDirty={formDirty}
|
<EntityTitleEditButton onClick={() => setBasicInfoOpen(true)} />
|
||||||
isSaving={formSaving}
|
) : undefined
|
||||||
configTitle={t('pipelines.configuration')}
|
}
|
||||||
configContent={
|
saveLabel={t('common.save')}
|
||||||
<fieldset className="contents" disabled={!canManage}>
|
saveFormId="pipeline-form"
|
||||||
<PipelineFormComponent
|
canSave={canManage}
|
||||||
ref={pipelineFormRef}
|
isDirty={formDirty}
|
||||||
pipelineId={id}
|
isSaving={formSaving}
|
||||||
isEditMode={true}
|
configTitle={t('pipelines.configuration')}
|
||||||
disableForm={!canManage}
|
configContent={
|
||||||
showButtons={false}
|
<fieldset className="contents" disabled={!canManage}>
|
||||||
onFinish={handleFinish}
|
<PipelineFormComponent
|
||||||
onNewPipelineCreated={handleNewPipelineCreated}
|
ref={pipelineFormRef}
|
||||||
onDeletePipeline={handleDeletePipeline}
|
pipelineId={id}
|
||||||
onCancel={() => navigate(routeBase)}
|
isEditMode={true}
|
||||||
onDirtyChange={setFormDirty}
|
disableForm={!canManage}
|
||||||
onSavingChange={setFormSaving}
|
showButtons={false}
|
||||||
/>
|
onFinish={handleFinish}
|
||||||
</fieldset>
|
onNewPipelineCreated={handleNewPipelineCreated}
|
||||||
}
|
onDeletePipeline={handleDeletePipeline}
|
||||||
debugTitle={canOperate ? t('pipelines.debugChat') : undefined}
|
onCancel={() => navigate(routeBase)}
|
||||||
debugConnected={canOperate ? isWebSocketConnected : undefined}
|
onDirtyChange={setFormDirty}
|
||||||
debugConnectedLabel={t('pipelines.debugDialog.connected')}
|
onSavingChange={setFormSaving}
|
||||||
debugDisconnectedLabel={t('pipelines.debugDialog.disconnected')}
|
/>
|
||||||
debugContent={
|
</fieldset>
|
||||||
canOperate ? (
|
}
|
||||||
<DebugDialog
|
debugTitle={canOperate ? t('pipelines.debugChat') : undefined}
|
||||||
open={true}
|
debugConnected={canOperate ? isWebSocketConnected : undefined}
|
||||||
pipelineId={id}
|
debugConnectedLabel={t('pipelines.debugDialog.connected')}
|
||||||
isEmbedded={true}
|
debugDisconnectedLabel={t('pipelines.debugDialog.disconnected')}
|
||||||
compact={true}
|
debugContent={
|
||||||
hasUnsavedChanges={formDirty}
|
canOperate ? (
|
||||||
beforeSend={async () => pipelineFormRef.current?.save() ?? false}
|
<DebugDialog
|
||||||
onConnectionStatusChange={setIsWebSocketConnected}
|
open={true}
|
||||||
/>
|
pipelineId={id}
|
||||||
) : undefined
|
isEmbedded={true}
|
||||||
}
|
compact={true}
|
||||||
unsavedLabel={t('pipelines.unsavedChanges')}
|
hasUnsavedChanges={formDirty}
|
||||||
monitoring={
|
beforeSend={async () => pipelineFormRef.current?.save() ?? false}
|
||||||
canViewMonitoring
|
onConnectionStatusChange={setIsWebSocketConnected}
|
||||||
? {
|
/>
|
||||||
label: t('pipelines.monitoring.title'),
|
) : undefined
|
||||||
content: (
|
}
|
||||||
<PipelineMonitoringTab
|
unsavedLabel={t('pipelines.unsavedChanges')}
|
||||||
pipelineId={id}
|
monitoring={
|
||||||
onNavigateToMonitoring={() => {
|
canViewMonitoring
|
||||||
navigate('/home/monitoring');
|
? {
|
||||||
}}
|
label: t('pipelines.monitoring.title'),
|
||||||
/>
|
content: (
|
||||||
),
|
<PipelineMonitoringTab
|
||||||
}
|
pipelineId={id}
|
||||||
: undefined
|
onNavigateToMonitoring={() => {
|
||||||
}
|
navigate('/home/monitoring');
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<EntityBasicInfoDialog
|
||||||
|
open={basicInfoOpen}
|
||||||
|
onOpenChange={setBasicInfoOpen}
|
||||||
|
values={{
|
||||||
|
name: pipelineName,
|
||||||
|
description: pipelineDetails?.description || '',
|
||||||
|
emoji: pipelineEmoji,
|
||||||
|
}}
|
||||||
|
defaultEmoji="⚙️"
|
||||||
|
onSave={saveBasicInfo}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,11 @@ interface PipelineFormComponentProps {
|
|||||||
|
|
||||||
export interface PipelineFormHandle {
|
export interface PipelineFormHandle {
|
||||||
save: () => Promise<boolean>;
|
save: () => Promise<boolean>;
|
||||||
|
syncBasicInfo: (values: {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
emoji?: string;
|
||||||
|
}) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PipelineFormComponent = forwardRef<
|
const PipelineFormComponent = forwardRef<
|
||||||
@@ -137,7 +142,7 @@ const PipelineFormComponent = forwardRef<
|
|||||||
const formLabelList: SectionItem[] = isEditMode
|
const formLabelList: SectionItem[] = isEditMode
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
label: t('pipelines.basicInfo'),
|
label: t('common.management'),
|
||||||
name: 'basic',
|
name: 'basic',
|
||||||
icon: SECTION_ICONS.basic,
|
icon: SECTION_ICONS.basic,
|
||||||
},
|
},
|
||||||
@@ -367,6 +372,24 @@ const PipelineFormComponent = forwardRef<
|
|||||||
}
|
}
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
|
syncBasicInfo(values) {
|
||||||
|
form.setValue('basic', {
|
||||||
|
...form.getValues('basic'),
|
||||||
|
name: values.name,
|
||||||
|
description: values.description,
|
||||||
|
emoji: values.emoji || '⚙️',
|
||||||
|
});
|
||||||
|
if (savedSnapshotRef.current) {
|
||||||
|
const snapshot = JSON.parse(savedSnapshotRef.current) as FormValues;
|
||||||
|
snapshot.basic = {
|
||||||
|
...snapshot.basic,
|
||||||
|
name: values.name,
|
||||||
|
description: values.description,
|
||||||
|
emoji: values.emoji || '⚙️',
|
||||||
|
};
|
||||||
|
savedSnapshotRef.current = JSON.stringify(snapshot);
|
||||||
|
}
|
||||||
|
},
|
||||||
async save() {
|
async save() {
|
||||||
if (!hasUnsavedChangesRef.current) return true;
|
if (!hasUnsavedChangesRef.current) return true;
|
||||||
if (isSavingRef.current || !isEditMode) return false;
|
if (isSavingRef.current || !isEditMode) return false;
|
||||||
@@ -656,69 +679,85 @@ const PipelineFormComponent = forwardRef<
|
|||||||
|
|
||||||
{/* Content panel */}
|
{/* Content panel */}
|
||||||
<div className="flex-1 overflow-y-auto min-h-0">
|
<div className="flex-1 overflow-y-auto min-h-0">
|
||||||
{/* Basic info section */}
|
|
||||||
{activeSection === 'basic' && (
|
{activeSection === 'basic' && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Basic Information Card */}
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{t('pipelines.basicInfo')}</CardTitle>
|
<CardTitle>
|
||||||
|
{isEditMode
|
||||||
|
? t('common.management')
|
||||||
|
: t('pipelines.basicInfo')}
|
||||||
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
{t('pipelines.basicInfoDescription')}
|
{isEditMode
|
||||||
|
? t('pipelines.managementDescription')
|
||||||
|
: t('pipelines.basicInfoDescription')}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
{/* Name and Emoji in same row */}
|
{!isEditMode && (
|
||||||
<div className="flex gap-4 items-start">
|
<>
|
||||||
<FormField
|
<div className="flex gap-4 items-start">
|
||||||
control={form.control}
|
<FormField
|
||||||
name="basic.name"
|
control={form.control}
|
||||||
render={({ field }) => (
|
name="basic.name"
|
||||||
<FormItem className="flex-1">
|
render={({ field }) => (
|
||||||
<FormLabel>
|
<FormItem className="flex-1">
|
||||||
{t('common.name')}
|
<FormLabel>
|
||||||
<span className="text-destructive">*</span>
|
{t('common.name')}
|
||||||
</FormLabel>
|
<span className="text-destructive">
|
||||||
<FormControl>
|
*
|
||||||
<Input {...field} value={field.value ?? ''} />
|
</span>
|
||||||
</FormControl>
|
</FormLabel>
|
||||||
<FormMessage />
|
<FormControl>
|
||||||
</FormItem>
|
<Input
|
||||||
)}
|
{...field}
|
||||||
/>
|
value={field.value ?? ''}
|
||||||
<FormField
|
/>
|
||||||
control={form.control}
|
</FormControl>
|
||||||
name="basic.emoji"
|
<FormMessage />
|
||||||
render={({ field }) => (
|
</FormItem>
|
||||||
<FormItem>
|
)}
|
||||||
<FormLabel>{t('common.icon')}</FormLabel>
|
/>
|
||||||
<FormControl>
|
<FormField
|
||||||
<EmojiPicker
|
control={form.control}
|
||||||
value={field.value}
|
name="basic.emoji"
|
||||||
onChange={field.onChange}
|
render={({ field }) => (
|
||||||
/>
|
<FormItem>
|
||||||
</FormControl>
|
<FormLabel>{t('common.icon')}</FormLabel>
|
||||||
<FormMessage />
|
<FormControl>
|
||||||
</FormItem>
|
<EmojiPicker
|
||||||
)}
|
value={field.value}
|
||||||
/>
|
onChange={field.onChange}
|
||||||
</div>
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="basic.description"
|
name="basic.description"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('common.description')}</FormLabel>
|
<FormLabel>
|
||||||
<FormControl>
|
{t('common.description')}
|
||||||
<Input {...field} value={field.value ?? ''} />
|
</FormLabel>
|
||||||
</FormControl>
|
<FormControl>
|
||||||
<FormMessage />
|
<Input
|
||||||
</FormItem>
|
{...field}
|
||||||
)}
|
value={field.value ?? ''}
|
||||||
/>
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Copy pipeline (edit mode only) */}
|
|
||||||
{isEditMode && (
|
{isEditMode && (
|
||||||
<div className="flex items-center justify-between rounded-lg border p-4">
|
<div className="flex items-center justify-between rounded-lg border p-4">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
|
|||||||
@@ -329,7 +329,10 @@ export class BackendClient extends BaseHttpClient {
|
|||||||
return this.post('/api/v1/pipelines', pipeline);
|
return this.post('/api/v1/pipelines', pipeline);
|
||||||
}
|
}
|
||||||
|
|
||||||
public updatePipeline(uuid: string, pipeline: Pipeline): Promise<object> {
|
public updatePipeline(
|
||||||
|
uuid: string,
|
||||||
|
pipeline: Partial<Pipeline>,
|
||||||
|
): Promise<object> {
|
||||||
return this.put(`/api/v1/pipelines/${uuid}`, pipeline);
|
return this.put(`/api/v1/pipelines/${uuid}`, pipeline);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -489,7 +492,7 @@ export class BackendClient extends BaseHttpClient {
|
|||||||
return this.post('/api/v1/platform/bots', bot);
|
return this.post('/api/v1/platform/bots', bot);
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateBot(uuid: string, bot: Bot): Promise<object> {
|
public updateBot(uuid: string, bot: Partial<Bot>): Promise<object> {
|
||||||
return this.put(`/api/v1/platform/bots/${uuid}`, bot);
|
return this.put(`/api/v1/platform/bots/${uuid}`, bot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ const enUS = {
|
|||||||
joinDiscord: 'Join our Discord',
|
joinDiscord: 'Join our Discord',
|
||||||
create: 'Create',
|
create: 'Create',
|
||||||
edit: 'Edit',
|
edit: 'Edit',
|
||||||
|
editBasicInfo: 'Edit basic information',
|
||||||
|
editBasicInfoDescription: 'Change the name, description, and icon.',
|
||||||
|
editBasicInfoDescriptionNoIcon: 'Change the name and description.',
|
||||||
|
management: 'Management',
|
||||||
delete: 'Delete',
|
delete: 'Delete',
|
||||||
add: 'Add',
|
add: 'Add',
|
||||||
select: 'Select',
|
select: 'Select',
|
||||||
@@ -688,6 +692,9 @@ const enUS = {
|
|||||||
messageEventsOnly: 'Message events only',
|
messageEventsOnly: 'Message events only',
|
||||||
basicInfo: 'Basic Information',
|
basicInfo: 'Basic Information',
|
||||||
basicInfoDescription: 'Set the name, icon, description and enabled state',
|
basicInfoDescription: 'Set the name, icon, description and enabled state',
|
||||||
|
availability: 'Availability',
|
||||||
|
availabilityDescription:
|
||||||
|
'Control whether this Agent can receive and process events.',
|
||||||
runnerSettings: 'Runner',
|
runnerSettings: 'Runner',
|
||||||
advanced: 'Advanced',
|
advanced: 'Advanced',
|
||||||
bindableEvents: 'Bindable Event Range',
|
bindableEvents: 'Bindable Event Range',
|
||||||
@@ -1233,6 +1240,7 @@ const enUS = {
|
|||||||
earliestEdited: 'Earliest Edited',
|
earliestEdited: 'Earliest Edited',
|
||||||
basicInfo: 'Basic Information',
|
basicInfo: 'Basic Information',
|
||||||
basicInfoDescription: 'Set the pipeline name, icon and description',
|
basicInfoDescription: 'Set the pipeline name, icon and description',
|
||||||
|
managementDescription: 'Copy or delete this pipeline.',
|
||||||
aiCapabilities: 'AI',
|
aiCapabilities: 'AI',
|
||||||
triggerConditions: 'Trigger',
|
triggerConditions: 'Trigger',
|
||||||
safetyControls: 'Safety',
|
safetyControls: 'Safety',
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ const jaJP = {
|
|||||||
joinDiscord: 'Discord に参加',
|
joinDiscord: 'Discord に参加',
|
||||||
create: '作成',
|
create: '作成',
|
||||||
edit: '編集',
|
edit: '編集',
|
||||||
|
editBasicInfo: '基本情報を編集',
|
||||||
|
editBasicInfoDescription: '名前、説明、アイコンを変更します。',
|
||||||
|
editBasicInfoDescriptionNoIcon: '名前と説明を変更します。',
|
||||||
|
management: '管理',
|
||||||
delete: '削除',
|
delete: '削除',
|
||||||
add: '追加',
|
add: '追加',
|
||||||
select: '選択してください',
|
select: '選択してください',
|
||||||
@@ -703,6 +707,9 @@ const jaJP = {
|
|||||||
messageEventsOnly: 'メッセージイベントのみ',
|
messageEventsOnly: 'メッセージイベントのみ',
|
||||||
basicInfo: '基本情報',
|
basicInfo: '基本情報',
|
||||||
basicInfoDescription: '名前、アイコン、説明、有効状態を設定します',
|
basicInfoDescription: '名前、アイコン、説明、有効状態を設定します',
|
||||||
|
availability: '有効状態',
|
||||||
|
availabilityDescription:
|
||||||
|
'この Agent がイベントを受信して処理できるかを制御します。',
|
||||||
runnerSettings: 'Runner',
|
runnerSettings: 'Runner',
|
||||||
advanced: '詳細',
|
advanced: '詳細',
|
||||||
bindableEvents: '紐付け可能なイベント範囲',
|
bindableEvents: '紐付け可能なイベント範囲',
|
||||||
@@ -1198,6 +1205,7 @@ const jaJP = {
|
|||||||
earliestEdited: '最古編集',
|
earliestEdited: '最古編集',
|
||||||
basicInfo: '基本情報',
|
basicInfo: '基本情報',
|
||||||
basicInfoDescription: 'パイプラインの名前、アイコン、説明を設定',
|
basicInfoDescription: 'パイプラインの名前、アイコン、説明を設定',
|
||||||
|
managementDescription: 'このパイプラインを複製または削除します。',
|
||||||
aiCapabilities: 'AI機能',
|
aiCapabilities: 'AI機能',
|
||||||
triggerConditions: 'トリガー条件',
|
triggerConditions: 'トリガー条件',
|
||||||
safetyControls: '安全制御',
|
safetyControls: '安全制御',
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ const zhHans = {
|
|||||||
joinDiscord: '加入 Discord 社区',
|
joinDiscord: '加入 Discord 社区',
|
||||||
create: '创建',
|
create: '创建',
|
||||||
edit: '编辑',
|
edit: '编辑',
|
||||||
|
editBasicInfo: '编辑基本信息',
|
||||||
|
editBasicInfoDescription: '修改名称、描述和图标。',
|
||||||
|
editBasicInfoDescriptionNoIcon: '修改名称和描述。',
|
||||||
|
management: '管理',
|
||||||
delete: '删除',
|
delete: '删除',
|
||||||
add: '添加',
|
add: '添加',
|
||||||
select: '请选择',
|
select: '请选择',
|
||||||
@@ -658,6 +662,8 @@ const zhHans = {
|
|||||||
messageEventsOnly: '仅支持消息事件',
|
messageEventsOnly: '仅支持消息事件',
|
||||||
basicInfo: '基础信息',
|
basicInfo: '基础信息',
|
||||||
basicInfoDescription: '设置名称、图标、描述和启用状态',
|
basicInfoDescription: '设置名称、图标、描述和启用状态',
|
||||||
|
availability: '启用状态',
|
||||||
|
availabilityDescription: '控制此 Agent 是否可以接收并处理事件。',
|
||||||
runnerSettings: '运行器',
|
runnerSettings: '运行器',
|
||||||
advanced: '高级',
|
advanced: '高级',
|
||||||
bindableEvents: '可绑定事件范围',
|
bindableEvents: '可绑定事件范围',
|
||||||
@@ -1175,6 +1181,7 @@ const zhHans = {
|
|||||||
earliestEdited: '最早编辑',
|
earliestEdited: '最早编辑',
|
||||||
basicInfo: '基础信息',
|
basicInfo: '基础信息',
|
||||||
basicInfoDescription: '设置流水线名称、图标和描述',
|
basicInfoDescription: '设置流水线名称、图标和描述',
|
||||||
|
managementDescription: '复制或删除此流水线。',
|
||||||
aiCapabilities: 'AI 能力',
|
aiCapabilities: 'AI 能力',
|
||||||
triggerConditions: '触发条件',
|
triggerConditions: '触发条件',
|
||||||
safetyControls: '安全控制',
|
safetyControls: '安全控制',
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ test.describe('frontend CRUD smoke flows', () => {
|
|||||||
await expect(page.getByText('No logs yet')).toBeVisible();
|
await expect(page.getByText('No logs yet')).toBeVisible();
|
||||||
|
|
||||||
await page.goto('/home/agents?id=pipeline-1');
|
await page.goto('/home/agents?id=pipeline-1');
|
||||||
await expect(page.getByRole('tab', { name: 'Dashboard' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Dashboard' })).toBeVisible();
|
||||||
await expect(page.getByRole('tab', { name: 'Debug Chat' })).toHaveCount(0);
|
await expect(page.getByRole('tab', { name: 'Debug Chat' })).toHaveCount(0);
|
||||||
await expect(page.getByRole('button', { name: /^Save$/ })).toHaveCount(0);
|
await expect(page.getByRole('button', { name: /^Save$/ })).toHaveCount(0);
|
||||||
|
|
||||||
@@ -144,15 +144,21 @@ test.describe('frontend CRUD smoke flows', () => {
|
|||||||
|
|
||||||
await expect(page).toHaveURL(/\/home\/bots\?id=bot-1$/);
|
await expect(page).toHaveURL(/\/home\/bots\?id=bot-1$/);
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await expect(page.locator('input[name="name"]')).toHaveValue('Support Bot');
|
await expect(
|
||||||
|
page.getByRole('heading', { name: 'Support Bot' }),
|
||||||
await page
|
).toBeVisible();
|
||||||
.locator('input[name="description"]')
|
await expect(page.locator('input[name="name"]')).toHaveCount(0);
|
||||||
|
await page.getByRole('button', { name: 'Edit basic information' }).click();
|
||||||
|
const botInfoDialog = page.getByRole('dialog');
|
||||||
|
await expect(botInfoDialog.getByLabel('Icon')).toHaveCount(0);
|
||||||
|
await botInfoDialog.getByLabel('Name').fill('Support Bot Updated');
|
||||||
|
await botInfoDialog
|
||||||
|
.getByLabel('Description')
|
||||||
.fill('Answers customer support questions with context.');
|
.fill('Answers customer support questions with context.');
|
||||||
await save(page);
|
await botInfoDialog.getByRole('button', { name: 'Save' }).click();
|
||||||
await expect(page.locator('input[name="description"]')).toHaveValue(
|
await expect(
|
||||||
'Answers customer support questions with context.',
|
page.getByRole('heading', { name: 'Support Bot Updated' }),
|
||||||
);
|
).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole('button', { name: /^Delete$/ }).click();
|
await page.getByRole('button', { name: /^Delete$/ }).click();
|
||||||
await confirmDelete(page);
|
await confirmDelete(page);
|
||||||
@@ -176,18 +182,18 @@ test.describe('frontend CRUD smoke flows', () => {
|
|||||||
|
|
||||||
await expect(page).toHaveURL(/\/home\/agents\?id=pipeline-1$/);
|
await expect(page).toHaveURL(/\/home\/agents\?id=pipeline-1$/);
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await expect(page.locator('input[name="basic.name"]')).toHaveValue(
|
await expect(
|
||||||
'Escalation Pipeline',
|
page.getByRole('heading', { name: /Escalation Pipeline/ }),
|
||||||
);
|
).toBeVisible();
|
||||||
|
await expect(page.locator('input[name="basic.name"]')).toHaveCount(0);
|
||||||
await page
|
await page.getByRole('button', { name: 'Edit basic information' }).click();
|
||||||
.locator('input[name="basic.description"]')
|
const pipelineInfoDialog = page.getByRole('dialog');
|
||||||
|
await pipelineInfoDialog
|
||||||
|
.getByLabel('Description')
|
||||||
.fill('Routes urgent customer issues to operators.');
|
.fill('Routes urgent customer issues to operators.');
|
||||||
await save(page);
|
await pipelineInfoDialog.getByRole('button', { name: 'Save' }).click();
|
||||||
await expect(page.locator('input[name="basic.description"]')).toHaveValue(
|
|
||||||
'Routes urgent customer issues to operators.',
|
|
||||||
);
|
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Management' }).click();
|
||||||
await page.getByRole('button', { name: /^Delete$/ }).click();
|
await page.getByRole('button', { name: /^Delete$/ }).click();
|
||||||
await confirmDelete(page);
|
await confirmDelete(page);
|
||||||
|
|
||||||
@@ -204,8 +210,10 @@ test.describe('frontend CRUD smoke flows', () => {
|
|||||||
|
|
||||||
await page.goto('/home/agents?id=pipeline-ai');
|
await page.goto('/home/agents?id=pipeline-ai');
|
||||||
|
|
||||||
await expect(page.locator('input[name="basic.name"]')).toBeVisible();
|
await expect(
|
||||||
await page.getByRole('button', { name: /^AI$/ }).click();
|
page.getByRole('heading', { name: /pipeline-ai/ }),
|
||||||
|
).toBeVisible();
|
||||||
|
await page.getByRole('tab', { name: /^AI$/ }).click();
|
||||||
|
|
||||||
await expect(page.getByText('Runtime')).toBeVisible();
|
await expect(page.getByText('Runtime')).toBeVisible();
|
||||||
await expect(
|
await expect(
|
||||||
@@ -512,7 +520,9 @@ test.describe('bot advanced flows', () => {
|
|||||||
await expect(
|
await expect(
|
||||||
page.getByRole('tab', { name: /Configuration/ }),
|
page.getByRole('tab', { name: /Configuration/ }),
|
||||||
).toHaveAttribute('data-state', 'active');
|
).toHaveAttribute('data-state', 'active');
|
||||||
await expect(page.locator('input[name="name"]')).toBeVisible();
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Edit basic information' }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
// Switch to Logs tab
|
// Switch to Logs tab
|
||||||
await page.getByRole('tab', { name: /Logs/ }).click();
|
await page.getByRole('tab', { name: /Logs/ }).click();
|
||||||
@@ -530,7 +540,9 @@ test.describe('bot advanced flows', () => {
|
|||||||
|
|
||||||
// Switch back to Configuration
|
// Switch back to Configuration
|
||||||
await page.getByRole('tab', { name: /Configuration/ }).click();
|
await page.getByRole('tab', { name: /Configuration/ }).click();
|
||||||
await expect(page.locator('input[name="name"]')).toBeVisible();
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Edit basic information' }),
|
||||||
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('save button is disabled when form is clean', async ({ page }) => {
|
test('save button is disabled when form is clean', async ({ page }) => {
|
||||||
@@ -541,23 +553,22 @@ test.describe('bot advanced flows', () => {
|
|||||||
await selectPlaywrightAdapter(page);
|
await selectPlaywrightAdapter(page);
|
||||||
await page.locator('input[name="name"]').fill('Clean Form Bot');
|
await page.locator('input[name="name"]').fill('Clean Form Bot');
|
||||||
await submit(page);
|
await submit(page);
|
||||||
|
await expect(page).toHaveURL(/\/home\/bots\?id=bot-1$/);
|
||||||
|
|
||||||
// Reload the persisted record so post-create initialization has completed.
|
// Reload the persisted record so post-create initialization has completed.
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await expect(page.locator('input[name="name"]')).toHaveValue(
|
await expect(
|
||||||
'Clean Form Bot',
|
page.getByRole('heading', { name: 'Clean Form Bot' }),
|
||||||
);
|
).toBeVisible();
|
||||||
|
|
||||||
// After loading, save button should be disabled (form is clean)
|
// After loading, save button should be disabled (form is clean)
|
||||||
const saveButton = page.getByRole('button', { name: /^Save$/ });
|
const saveButton = page.getByRole('button', { name: /^Save$/ });
|
||||||
await expect(saveButton).toBeDisabled();
|
await expect(saveButton).toBeDisabled();
|
||||||
|
|
||||||
// Edit the form
|
await page.getByRole('button', { name: 'Edit basic information' }).click();
|
||||||
await page.locator('input[name="description"]').fill('New description');
|
const infoDialog = page.getByRole('dialog');
|
||||||
await expect(saveButton).toBeEnabled();
|
await infoDialog.getByLabel('Description').fill('New description');
|
||||||
|
await infoDialog.getByRole('button', { name: 'Save' }).click();
|
||||||
// Save
|
|
||||||
await saveButton.click();
|
|
||||||
await expect(saveButton).toBeDisabled();
|
await expect(saveButton).toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -593,7 +604,7 @@ test.describe('pipeline advanced flows', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await page.goto('/home/agents?id=pipeline-scope');
|
await page.goto('/home/agents?id=pipeline-scope');
|
||||||
await page.getByRole('button', { name: /^AI$/ }).click();
|
await page.getByRole('tab', { name: /^AI$/ }).click();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Edit tools' }),
|
page.getByRole('button', { name: 'Edit tools' }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
@@ -621,22 +632,26 @@ test.describe('pipeline advanced flows', () => {
|
|||||||
await page.locator('input[name="name"]').fill('Tab Test Pipeline');
|
await page.locator('input[name="name"]').fill('Tab Test Pipeline');
|
||||||
await submit(page);
|
await submit(page);
|
||||||
|
|
||||||
// Verify we're on the Configuration tab
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('tab', { name: /Configuration/ }),
|
page.getByRole('region', { name: 'Configuration' }),
|
||||||
).toHaveAttribute('data-state', 'active');
|
).toBeVisible();
|
||||||
|
|
||||||
// Switch to Monitoring tab (labeled "Dashboard" in the pipeline context)
|
// Switch to Monitoring tab (labeled "Dashboard" in the pipeline context)
|
||||||
// Skip Debug tab as it requires WebSocket connection
|
// Skip Debug tab as it requires WebSocket connection
|
||||||
await page.getByRole('tab', { name: /Dashboard/ }).click();
|
await page
|
||||||
await expect(page.getByRole('tab', { name: /Dashboard/ })).toHaveAttribute(
|
.getByRole('button', { name: 'Dashboard', exact: true })
|
||||||
'data-state',
|
.last()
|
||||||
'active',
|
.click();
|
||||||
);
|
await expect(page.getByRole('region', { name: /Dashboard/ })).toBeVisible();
|
||||||
|
|
||||||
// Switch back to Configuration
|
// Switch back to Configuration
|
||||||
await page.getByRole('tab', { name: /Configuration/ }).click();
|
await page
|
||||||
await expect(page.locator('input[name="basic.name"]')).toBeVisible();
|
.getByRole('button', { name: 'Dashboard', exact: true })
|
||||||
|
.last()
|
||||||
|
.click();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('region', { name: 'Configuration' }),
|
||||||
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('save button reflects form dirty state', async ({ page }) => {
|
test('save button reflects form dirty state', async ({ page }) => {
|
||||||
@@ -648,20 +663,16 @@ test.describe('pipeline advanced flows', () => {
|
|||||||
await page.locator('input[name="name"]').fill('Dirty Form Pipeline');
|
await page.locator('input[name="name"]').fill('Dirty Form Pipeline');
|
||||||
await submit(page);
|
await submit(page);
|
||||||
|
|
||||||
// Wait for the page to fully load and form to reset
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
// Edit the form - use the name field which definitely triggers dirty state
|
|
||||||
await page
|
|
||||||
.locator('input[name="basic.name"]')
|
|
||||||
.fill('Dirty Form Pipeline Updated');
|
|
||||||
const saveButton = page.getByRole('button', { name: /^Save$/ });
|
const saveButton = page.getByRole('button', { name: /^Save$/ });
|
||||||
await expect(saveButton).toBeEnabled();
|
await expect(saveButton).toBeDisabled();
|
||||||
|
await page.getByRole('button', { name: 'Edit basic information' }).click();
|
||||||
// Save
|
const infoDialog = page.getByRole('dialog');
|
||||||
await saveButton.click();
|
await infoDialog.getByLabel('Name').fill('Dirty Form Pipeline Updated');
|
||||||
// Wait for save to complete
|
await infoDialog.getByRole('button', { name: 'Save' }).click();
|
||||||
await page.waitForTimeout(500);
|
await expect(
|
||||||
|
page.getByRole('heading', { name: /Dirty Form Pipeline Updated/ }),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(saveButton).toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('shows validation error when pipeline name is empty', async ({
|
test('shows validation error when pipeline name is empty', async ({
|
||||||
@@ -705,7 +716,8 @@ test.describe('agent runner resource selectors', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await page.goto('/home/agents?id=agent-scope');
|
await page.goto('/home/agents?id=agent-scope');
|
||||||
await page.getByRole('button', { name: /^Runner$/ }).click();
|
await page.getByRole('tab', { name: /^Runner$/ }).click();
|
||||||
|
await page.getByRole('tab', { name: 'Local Agent' }).click();
|
||||||
await page.getByRole('button', { name: 'Edit tools' }).click();
|
await page.getByRole('button', { name: 'Edit tools' }).click();
|
||||||
|
|
||||||
const dialog = page.getByRole('dialog');
|
const dialog = page.getByRole('dialog');
|
||||||
@@ -747,16 +759,16 @@ test.describe('agent and pipeline save concurrency', () => {
|
|||||||
|
|
||||||
await page.goto('/home/agents?id=agent-save-race');
|
await page.goto('/home/agents?id=agent-save-race');
|
||||||
const saveButton = page.getByRole('button', { name: /^Save$/ });
|
const saveButton = page.getByRole('button', { name: /^Save$/ });
|
||||||
const nameInput = page.locator('input[name="basic.name"]');
|
await page.getByRole('tab', { name: 'Bindable Event Range' }).click();
|
||||||
const descriptionInput = page.locator('input[name="basic.description"]');
|
const eventPatterns = page.getByLabel('Event Range');
|
||||||
await expect(nameInput).toBeVisible();
|
await expect(eventPatterns).toBeVisible();
|
||||||
|
|
||||||
await nameInput.fill('Submitted Agent');
|
await eventPatterns.fill('message.received');
|
||||||
await saveButton.click();
|
await saveButton.click();
|
||||||
await expect.poll(() => delayedSave.payloads.length).toBe(1);
|
await expect.poll(() => delayedSave.payloads.length).toBe(1);
|
||||||
await expect(saveButton).toBeDisabled();
|
await expect(saveButton).toBeDisabled();
|
||||||
|
|
||||||
await descriptionInput.fill('Edited while the agent save is pending');
|
await eventPatterns.fill('group.*');
|
||||||
await forceFormSubmit(page, '#agent-form');
|
await forceFormSubmit(page, '#agent-form');
|
||||||
expect(delayedSave.payloads).toHaveLength(1);
|
expect(delayedSave.payloads).toHaveLength(1);
|
||||||
await expect(saveButton).toBeDisabled();
|
await expect(saveButton).toBeDisabled();
|
||||||
@@ -764,15 +776,13 @@ test.describe('agent and pipeline save concurrency', () => {
|
|||||||
delayedSave.releaseFirstSave();
|
delayedSave.releaseFirstSave();
|
||||||
await expect(saveButton).toBeEnabled();
|
await expect(saveButton).toBeEnabled();
|
||||||
expect(delayedSave.payloads[0]).toMatchObject({
|
expect(delayedSave.payloads[0]).toMatchObject({
|
||||||
name: 'Submitted Agent',
|
supported_event_patterns: ['message.received'],
|
||||||
description: '',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await saveButton.click();
|
await saveButton.click();
|
||||||
await expect.poll(() => delayedSave.payloads.length).toBe(2);
|
await expect.poll(() => delayedSave.payloads.length).toBe(2);
|
||||||
expect(delayedSave.payloads[1]).toMatchObject({
|
expect(delayedSave.payloads[1]).toMatchObject({
|
||||||
name: 'Submitted Agent',
|
supported_event_patterns: ['group.*'],
|
||||||
description: 'Edited while the agent save is pending',
|
|
||||||
});
|
});
|
||||||
await expect(saveButton).toBeDisabled();
|
await expect(saveButton).toBeDisabled();
|
||||||
});
|
});
|
||||||
@@ -787,35 +797,35 @@ test.describe('agent and pipeline save concurrency', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await page.goto('/home/agents?id=pipeline-save-race');
|
await page.goto('/home/agents?id=pipeline-save-race');
|
||||||
const saveButton = page.getByRole('button', { name: /^Save$/ });
|
await page.getByRole('button', { name: 'Edit basic information' }).click();
|
||||||
const nameInput = page.locator('input[name="basic.name"]');
|
let infoDialog = page.getByRole('dialog');
|
||||||
const descriptionInput = page.locator('input[name="basic.description"]');
|
await infoDialog.getByLabel('Name').fill('Submitted Pipeline');
|
||||||
await expect(nameInput).toBeVisible();
|
const dialogSaveButton = infoDialog.getByRole('button', { name: 'Save' });
|
||||||
|
await dialogSaveButton.click();
|
||||||
await nameInput.fill('Submitted Pipeline');
|
|
||||||
await saveButton.click();
|
|
||||||
await expect.poll(() => delayedSave.payloads.length).toBe(1);
|
await expect.poll(() => delayedSave.payloads.length).toBe(1);
|
||||||
await expect(saveButton).toBeDisabled();
|
await expect(
|
||||||
|
infoDialog.getByRole('button', { name: 'Saving...' }),
|
||||||
await descriptionInput.fill('Edited while the pipeline save is pending');
|
).toBeDisabled();
|
||||||
await forceFormSubmit(page, '#pipeline-form');
|
|
||||||
expect(delayedSave.payloads).toHaveLength(1);
|
|
||||||
await expect(saveButton).toBeDisabled();
|
|
||||||
|
|
||||||
delayedSave.releaseFirstSave();
|
delayedSave.releaseFirstSave();
|
||||||
await expect(saveButton).toBeEnabled();
|
await expect(infoDialog).toHaveCount(0);
|
||||||
expect(delayedSave.payloads[0]).toMatchObject({
|
expect(delayedSave.payloads[0]).toMatchObject({
|
||||||
name: 'Submitted Pipeline',
|
name: 'Submitted Pipeline',
|
||||||
description: '',
|
description: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
await saveButton.click();
|
await page.getByRole('button', { name: 'Edit basic information' }).click();
|
||||||
|
infoDialog = page.getByRole('dialog');
|
||||||
|
await infoDialog
|
||||||
|
.getByLabel('Description')
|
||||||
|
.fill('Edited in the next basic information save');
|
||||||
|
await infoDialog.getByRole('button', { name: 'Save' }).click();
|
||||||
await expect.poll(() => delayedSave.payloads.length).toBe(2);
|
await expect.poll(() => delayedSave.payloads.length).toBe(2);
|
||||||
expect(delayedSave.payloads[1]).toMatchObject({
|
expect(delayedSave.payloads[1]).toMatchObject({
|
||||||
name: 'Submitted Pipeline',
|
name: 'Submitted Pipeline',
|
||||||
description: 'Edited while the pipeline save is pending',
|
description: 'Edited in the next basic information save',
|
||||||
});
|
});
|
||||||
await expect(saveButton).toBeDisabled();
|
await expect(infoDialog).toHaveCount(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -838,7 +848,9 @@ test.describe('cross-resource flows', () => {
|
|||||||
await expect(page).toHaveURL(/\/home\/bots\?id=bot-1$/);
|
await expect(page).toHaveURL(/\/home\/bots\?id=bot-1$/);
|
||||||
|
|
||||||
// Wait for form to fully load
|
// Wait for form to fully load
|
||||||
await expect(page.locator('input[name="name"]')).toHaveValue('Bound Bot');
|
await expect(
|
||||||
|
page.getByRole('heading', { name: 'Bound Bot' }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Add behavior' }).click();
|
await page.getByRole('button', { name: 'Add behavior' }).click();
|
||||||
await page.getByRole('menuitem', { name: /^Reply to messages/ }).click();
|
await page.getByRole('menuitem', { name: /^Reply to messages/ }).click();
|
||||||
|
|||||||
@@ -647,7 +647,12 @@ async function handleBackendApi(route: Route, state: LangBotApiMockState) {
|
|||||||
const botId = decodeURIComponent(botMatch[1]);
|
const botId = decodeURIComponent(botMatch[1]);
|
||||||
|
|
||||||
if (method === 'PUT') {
|
if (method === 'PUT') {
|
||||||
const bot = makeBot(state, parseJsonBody(route), botId);
|
const current = state.bots.find((item) => item.uuid === botId);
|
||||||
|
const bot = makeBot(
|
||||||
|
state,
|
||||||
|
{ ...(current || {}), ...parseJsonBody(route) },
|
||||||
|
botId,
|
||||||
|
);
|
||||||
state.bots = [...state.bots.filter((item) => item.uuid !== botId), bot];
|
state.bots = [...state.bots.filter((item) => item.uuid !== botId), bot];
|
||||||
return fulfillJson(route, {});
|
return fulfillJson(route, {});
|
||||||
}
|
}
|
||||||
@@ -729,7 +734,12 @@ async function handleBackendApi(route: Route, state: LangBotApiMockState) {
|
|||||||
const agentId = decodeURIComponent(agentMatch[1]);
|
const agentId = decodeURIComponent(agentMatch[1]);
|
||||||
|
|
||||||
if (method === 'PUT') {
|
if (method === 'PUT') {
|
||||||
const agent = makePipeline(state, parseJsonBody(route), agentId);
|
const current = state.pipelines.find((item) => item.uuid === agentId);
|
||||||
|
const agent = makePipeline(
|
||||||
|
state,
|
||||||
|
{ ...(current || {}), ...parseJsonBody(route) },
|
||||||
|
agentId,
|
||||||
|
);
|
||||||
state.pipelines = [
|
state.pipelines = [
|
||||||
...state.pipelines.filter((item) => item.uuid !== agentId),
|
...state.pipelines.filter((item) => item.uuid !== agentId),
|
||||||
agent,
|
agent,
|
||||||
@@ -789,7 +799,12 @@ async function handleBackendApi(route: Route, state: LangBotApiMockState) {
|
|||||||
const pipelineId = decodeURIComponent(pipelineMatch[1]);
|
const pipelineId = decodeURIComponent(pipelineMatch[1]);
|
||||||
|
|
||||||
if (method === 'PUT') {
|
if (method === 'PUT') {
|
||||||
const pipeline = makePipeline(state, parseJsonBody(route), pipelineId);
|
const current = state.pipelines.find((item) => item.uuid === pipelineId);
|
||||||
|
const pipeline = makePipeline(
|
||||||
|
state,
|
||||||
|
{ ...(current || {}), ...parseJsonBody(route) },
|
||||||
|
pipelineId,
|
||||||
|
);
|
||||||
state.pipelines = [
|
state.pipelines = [
|
||||||
...state.pipelines.filter((item) => item.uuid !== pipelineId),
|
...state.pipelines.filter((item) => item.uuid !== pipelineId),
|
||||||
pipeline,
|
pipeline,
|
||||||
|
|||||||
@@ -45,26 +45,32 @@ test.describe('processor detail workbench', () => {
|
|||||||
expect(debugBox!.y).toBeGreaterThanOrEqual(0);
|
expect(debugBox!.y).toBeGreaterThanOrEqual(0);
|
||||||
|
|
||||||
const flow = configPanel.getByRole('tablist');
|
const flow = configPanel.getByRole('tablist');
|
||||||
await expect(flow.getByRole('tab').nth(0)).toContainText(
|
await expect(flow.getByRole('tab').nth(0)).toContainText('Management');
|
||||||
'Basic Information',
|
|
||||||
);
|
|
||||||
await expect(flow.getByRole('tab').nth(1)).toContainText(
|
await expect(flow.getByRole('tab').nth(1)).toContainText(
|
||||||
'Bindable Event Range',
|
'Bindable Event Range',
|
||||||
);
|
);
|
||||||
await expect(flow.getByRole('tab').nth(2)).toContainText('Runner');
|
await expect(flow.getByRole('tab').nth(2)).toContainText('Runner');
|
||||||
await expect(flow.getByRole('tab').nth(3)).toContainText('Local Agent');
|
await expect(flow.getByRole('tab').nth(3)).toContainText('Local Agent');
|
||||||
|
|
||||||
await expect(configPanel.getByLabel('Name')).toBeVisible();
|
await expect(
|
||||||
await expect(configPanel.getByLabel('Icon')).toBeVisible();
|
page.getByRole('heading', { name: /agent-workbench/ }),
|
||||||
await expect(configPanel.getByLabel('Description')).toBeVisible();
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Edit basic information' }),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(configPanel.getByLabel('Name')).toHaveCount(0);
|
||||||
|
await expect(configPanel.getByLabel('Icon')).toHaveCount(0);
|
||||||
|
await expect(configPanel.getByLabel('Description')).toHaveCount(0);
|
||||||
|
|
||||||
const runnerStatus = page.getByRole('status', { name: 'Runner ready' });
|
const runnerStatus = page.getByRole('status', { name: 'Runner ready' });
|
||||||
await expect(runnerStatus).toBeVisible();
|
await expect(runnerStatus).toBeVisible();
|
||||||
await runnerStatus.hover();
|
await runnerStatus.hover();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText(
|
page
|
||||||
'Local Agent is registered and the plugin runtime is connected.',
|
.getByText(
|
||||||
),
|
'Local Agent is registered and the plugin runtime is connected.',
|
||||||
|
)
|
||||||
|
.last(),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
await flow.getByRole('tab').nth(1).click();
|
await flow.getByRole('tab').nth(1).click();
|
||||||
@@ -99,11 +105,18 @@ test.describe('processor detail workbench', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await page.goto('/home/agents?id=agent-workbench');
|
await page.goto('/home/agents?id=agent-workbench');
|
||||||
await page.getByLabel('Description').fill('Updated before debugging');
|
await page.getByRole('button', { name: 'Edit basic information' }).click();
|
||||||
|
const basicInfoDialog = page.getByRole('dialog');
|
||||||
|
await expect(basicInfoDialog.getByLabel('Icon')).toBeVisible();
|
||||||
|
await basicInfoDialog
|
||||||
|
.getByLabel('Description')
|
||||||
|
.fill('Updated before debugging');
|
||||||
|
await basicInfoDialog.getByRole('button', { name: 'Save' }).click();
|
||||||
|
await expect(basicInfoDialog).toHaveCount(0);
|
||||||
await page
|
await page
|
||||||
.getByRole('textbox', { name: 'Conversation input' })
|
.getByRole('textbox', { name: 'Conversation input' })
|
||||||
.fill('Hello');
|
.fill('Hello');
|
||||||
await page.getByRole('button', { name: 'Save and run' }).click();
|
await page.getByRole('button', { name: 'Run test' }).click();
|
||||||
|
|
||||||
await expect(page.getByText('Mock Agent response')).toBeVisible();
|
await expect(page.getByText('Mock Agent response')).toBeVisible();
|
||||||
expect(requests).toEqual(['save', 'debug']);
|
expect(requests).toEqual(['save', 'debug']);
|
||||||
@@ -186,6 +199,24 @@ test.describe('processor detail workbench', () => {
|
|||||||
page.getByText('Conversation reset successfully'),
|
page.getByText('Conversation reset successfully'),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByRole('heading', { name: /pipeline-workbench/ }),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(configPanel.locator('input[name="basic.name"]')).toHaveCount(
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
await page.getByRole('button', { name: 'Edit basic information' }).click();
|
||||||
|
const basicInfoDialog = page.getByRole('dialog');
|
||||||
|
await expect(basicInfoDialog.getByLabel('Icon')).toBeVisible();
|
||||||
|
await basicInfoDialog.getByLabel('Name').fill('Renamed Pipeline');
|
||||||
|
await basicInfoDialog
|
||||||
|
.getByLabel('Description')
|
||||||
|
.fill('Updated from the title dialog.');
|
||||||
|
await basicInfoDialog.getByRole('button', { name: 'Save' }).click();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('heading', { name: /Renamed Pipeline/ }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
const debugBox = await debugPanel.boundingBox();
|
const debugBox = await debugPanel.boundingBox();
|
||||||
const configBox = await configPanel.boundingBox();
|
const configBox = await configPanel.boundingBox();
|
||||||
expect(debugBox).not.toBeNull();
|
expect(debugBox).not.toBeNull();
|
||||||
|
|||||||
Reference in New Issue
Block a user