mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-11 21:30:59 +00:00
refactor(web): redesign bot config page with card-based layout and dirty-aware save button
- Restructure bot edit page from flat form to card-based layout (Basic Info, Pipeline Binding, Adapter Config, Danger Zone) - Move enable switch and save button to sticky header for quick access - Move webhook URL display into adapter config card (contextually related) - Remove redundant adapter icon card; show description as FormDescription - Add dedicated Danger Zone card with red border for delete action - Remove duplicate delete dialog from BotForm (single source in BotDetailContent) - Implement form dirty tracking: save button is disabled until user modifies content - Add i18n keys for new card titles/descriptions across all 4 locales
This commit is contained in:
@@ -1,9 +1,18 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -19,8 +28,9 @@ import type { BotSessionMonitorHandle } from '@/app/home/bots/components/bot-ses
|
|||||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
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 { Settings, FileText, Users, RefreshCw } from 'lucide-react';
|
import { Settings, FileText, Users, RefreshCw, Trash2 } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
export default function BotDetailContent({ id }: { id: string }) {
|
export default function BotDetailContent({ id }: { id: string }) {
|
||||||
const isCreateMode = id === 'new';
|
const isCreateMode = id === 'new';
|
||||||
@@ -44,7 +54,52 @@ export default function BotDetailContent({ id }: { id: string }) {
|
|||||||
const [isRefreshingSessions, setIsRefreshingSessions] = useState(false);
|
const [isRefreshingSessions, setIsRefreshingSessions] = useState(false);
|
||||||
const sessionMonitorRef = useRef<BotSessionMonitorHandle>(null);
|
const sessionMonitorRef = useRef<BotSessionMonitorHandle>(null);
|
||||||
|
|
||||||
|
// Track whether the form has unsaved changes
|
||||||
|
const [formDirty, setFormDirty] = useState(false);
|
||||||
|
|
||||||
|
// Enable state managed here so the header switch works
|
||||||
|
const [botEnabled, setBotEnabled] = useState(true);
|
||||||
|
const [enableLoaded, setEnableLoaded] = useState(false);
|
||||||
|
|
||||||
|
// Fetch bot enable state
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isCreateMode) {
|
||||||
|
httpClient.getBot(id).then((res) => {
|
||||||
|
setBotEnabled(res.bot.enable ?? true);
|
||||||
|
setEnableLoaded(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [id, isCreateMode]);
|
||||||
|
|
||||||
|
const handleEnableToggle = useCallback(
|
||||||
|
async (checked: boolean) => {
|
||||||
|
const prev = botEnabled;
|
||||||
|
setBotEnabled(checked);
|
||||||
|
try {
|
||||||
|
// Fetch current bot data to send a complete update
|
||||||
|
const res = await httpClient.getBot(id);
|
||||||
|
const bot = res.bot;
|
||||||
|
await httpClient.updateBot(id, {
|
||||||
|
name: bot.name,
|
||||||
|
description: bot.description,
|
||||||
|
adapter: bot.adapter,
|
||||||
|
adapter_config: bot.adapter_config,
|
||||||
|
enable: checked,
|
||||||
|
});
|
||||||
|
refreshBots();
|
||||||
|
} catch {
|
||||||
|
setBotEnabled(prev);
|
||||||
|
toast.error(t('bots.setBotEnableError'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[id, botEnabled, refreshBots, t],
|
||||||
|
);
|
||||||
|
|
||||||
function handleFormSubmit() {
|
function handleFormSubmit() {
|
||||||
|
// Re-sync enable state after form save (form may update enable too)
|
||||||
|
httpClient.getBot(id).then((res) => {
|
||||||
|
setBotEnabled(res.bot.enable ?? true);
|
||||||
|
});
|
||||||
refreshBots();
|
refreshBots();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,57 +110,79 @@ export default function BotDetailContent({ id }: { id: string }) {
|
|||||||
|
|
||||||
function handleNewBotCreated(newBotId: string) {
|
function handleNewBotCreated(newBotId: string) {
|
||||||
refreshBots();
|
refreshBots();
|
||||||
// Navigate to the newly created bot's detail view via query param
|
|
||||||
router.push(`/home/bots?id=${encodeURIComponent(newBotId)}`);
|
router.push(`/home/bots?id=${encodeURIComponent(newBotId)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDelete() {
|
|
||||||
setShowDeleteConfirm(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function confirmDelete() {
|
function confirmDelete() {
|
||||||
httpClient.deleteBot(id).then(() => {
|
httpClient
|
||||||
setShowDeleteConfirm(false);
|
.deleteBot(id)
|
||||||
handleBotDeleted();
|
.then(() => {
|
||||||
});
|
setShowDeleteConfirm(false);
|
||||||
|
toast.success(t('bots.deleteSuccess'));
|
||||||
|
handleBotDeleted();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error(t('bots.deleteError') + err.msg);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create mode: simple form layout
|
// ==================== Create Mode ====================
|
||||||
if (isCreateMode) {
|
if (isCreateMode) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
<div className="flex items-center gap-3 pb-4 shrink-0">
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between pb-4 shrink-0">
|
||||||
<h1 className="text-xl font-semibold">{t('bots.createBot')}</h1>
|
<h1 className="text-xl font-semibold">{t('bots.createBot')}</h1>
|
||||||
|
<Button type="submit" form="bot-form">
|
||||||
|
{t('common.submit')}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
<div className="flex-1 overflow-y-auto min-h-0">
|
<div className="flex-1 overflow-y-auto min-h-0">
|
||||||
<div className="mx-auto max-w-2xl space-y-6">
|
<div className="mx-auto max-w-3xl pb-8">
|
||||||
<BotForm
|
<BotForm
|
||||||
initBotId={undefined}
|
initBotId={undefined}
|
||||||
onFormSubmit={handleFormSubmit}
|
onFormSubmit={handleFormSubmit}
|
||||||
onBotDeleted={handleBotDeleted}
|
onBotDeleted={handleBotDeleted}
|
||||||
onNewBotCreated={handleNewBotCreated}
|
onNewBotCreated={handleNewBotCreated}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2 pb-4">
|
|
||||||
<Button type="submit" form="bot-form">
|
|
||||||
{t('common.submit')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Edit mode: tabbed layout with config, logs, sessions
|
// ==================== Edit Mode ====================
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
<div className="flex items-center gap-3 pb-4 shrink-0">
|
{/* Sticky Header: title + enable switch + save button */}
|
||||||
<h1 className="text-xl font-semibold">{t('bots.editBot')}</h1>
|
<div className="flex items-center justify-between pb-4 shrink-0">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<h1 className="text-xl font-semibold">{t('bots.editBot')}</h1>
|
||||||
|
{enableLoaded && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
id="bot-enable-switch"
|
||||||
|
checked={botEnabled}
|
||||||
|
onCheckedChange={handleEnableToggle}
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
htmlFor="bot-enable-switch"
|
||||||
|
className="text-sm text-muted-foreground cursor-pointer"
|
||||||
|
>
|
||||||
|
{t('common.enable')}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button type="submit" form="bot-form" disabled={!formDirty}>
|
||||||
|
{t('common.save')}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Horizontal Tabs */}
|
||||||
<Tabs
|
<Tabs
|
||||||
key={id}
|
key={id}
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
@@ -152,33 +229,56 @@ export default function BotDetailContent({ id }: { id: string }) {
|
|||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
|
{/* Tab: Configuration */}
|
||||||
<TabsContent
|
<TabsContent
|
||||||
value="config"
|
value="config"
|
||||||
className="flex-1 min-h-0 overflow-y-auto mt-4"
|
className="flex-1 min-h-0 overflow-y-auto mt-4"
|
||||||
>
|
>
|
||||||
<div className="mx-auto max-w-2xl">
|
<div className="mx-auto max-w-3xl space-y-6 pb-8">
|
||||||
<BotForm
|
<BotForm
|
||||||
initBotId={id}
|
initBotId={id}
|
||||||
onFormSubmit={handleFormSubmit}
|
onFormSubmit={handleFormSubmit}
|
||||||
onBotDeleted={handleBotDeleted}
|
onBotDeleted={handleBotDeleted}
|
||||||
onNewBotCreated={handleNewBotCreated}
|
onNewBotCreated={handleNewBotCreated}
|
||||||
|
onDirtyChange={setFormDirty}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2 mt-6 pb-4">
|
{/* Card: Danger Zone */}
|
||||||
<Button
|
<Card className="border-destructive/50">
|
||||||
type="button"
|
<CardHeader>
|
||||||
variant="destructive"
|
<CardTitle className="text-destructive">
|
||||||
onClick={handleDelete}
|
{t('bots.dangerZone')}
|
||||||
>
|
</CardTitle>
|
||||||
{t('common.delete')}
|
<CardDescription>
|
||||||
</Button>
|
{t('bots.dangerZoneDescription')}
|
||||||
<Button type="submit" form="bot-form">
|
</CardDescription>
|
||||||
{t('common.save')}
|
</CardHeader>
|
||||||
</Button>
|
<CardContent>
|
||||||
</div>
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-sm font-medium">
|
||||||
|
{t('bots.deleteBotAction')}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t('bots.deleteBotHint')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowDeleteConfirm(true)}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4 mr-1.5" />
|
||||||
|
{t('common.delete')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* Tab: Logs */}
|
||||||
<TabsContent
|
<TabsContent
|
||||||
value="logs"
|
value="logs"
|
||||||
className="flex-1 min-h-0 overflow-y-auto mt-4"
|
className="flex-1 min-h-0 overflow-y-auto mt-4"
|
||||||
@@ -186,6 +286,7 @@ export default function BotDetailContent({ id }: { id: string }) {
|
|||||||
<BotLogListComponent botId={id} />
|
<BotLogListComponent botId={id} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* Tab: Sessions */}
|
||||||
<TabsContent value="sessions" className="flex-1 min-h-0 mt-4">
|
<TabsContent value="sessions" className="flex-1 min-h-0 mt-4">
|
||||||
<BotSessionMonitor ref={sessionMonitorRef} botId={id} />
|
<BotSessionMonitor ref={sessionMonitorRef} botId={id} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
IChooseAdapterEntity,
|
IChooseAdapterEntity,
|
||||||
IPipelineEntity,
|
IPipelineEntity,
|
||||||
@@ -21,18 +21,11 @@ import { toast } from 'sonner';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Copy, Check } from 'lucide-react';
|
import { Copy, Check } from 'lucide-react';
|
||||||
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogFooter,
|
|
||||||
} from '@/components/ui/dialog';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
@@ -47,7 +40,13 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card';
|
||||||
import { extractI18nObject } from '@/i18n/I18nProvider';
|
import { extractI18nObject } from '@/i18n/I18nProvider';
|
||||||
import { CustomApiError } from '@/app/infra/entities/common';
|
import { CustomApiError } from '@/app/infra/entities/common';
|
||||||
|
|
||||||
@@ -68,11 +67,13 @@ export default function BotForm({
|
|||||||
onFormSubmit,
|
onFormSubmit,
|
||||||
onBotDeleted,
|
onBotDeleted,
|
||||||
onNewBotCreated,
|
onNewBotCreated,
|
||||||
|
onDirtyChange,
|
||||||
}: {
|
}: {
|
||||||
initBotId?: string;
|
initBotId?: string;
|
||||||
onFormSubmit: (value: z.infer<ReturnType<typeof getFormSchema>>) => void;
|
onFormSubmit: (value: z.infer<ReturnType<typeof getFormSchema>>) => void;
|
||||||
onBotDeleted: () => void;
|
onBotDeleted: () => void;
|
||||||
onNewBotCreated: (botId: string) => void;
|
onNewBotCreated: (botId: string) => void;
|
||||||
|
onDirtyChange?: (dirty: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const formSchema = getFormSchema(t);
|
const formSchema = getFormSchema(t);
|
||||||
@@ -89,19 +90,16 @@ export default function BotForm({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false);
|
// Track whether initial data loading is complete.
|
||||||
|
// setValue calls during init should NOT mark the form as dirty.
|
||||||
|
const isInitializing = useRef(true);
|
||||||
|
|
||||||
const [adapterNameToDynamicConfigMap, setAdapterNameToDynamicConfigMap] =
|
const [adapterNameToDynamicConfigMap, setAdapterNameToDynamicConfigMap] =
|
||||||
useState(new Map<string, IDynamicFormItemSchema[]>());
|
useState(new Map<string, IDynamicFormItemSchema[]>());
|
||||||
// const [form] = Form.useForm<IBotFormEntity>();
|
|
||||||
const [showDynamicForm, setShowDynamicForm] = useState<boolean>(false);
|
const [showDynamicForm, setShowDynamicForm] = useState<boolean>(false);
|
||||||
// const [dynamicForm] = Form.useForm();
|
|
||||||
const [adapterNameList, setAdapterNameList] = useState<
|
const [adapterNameList, setAdapterNameList] = useState<
|
||||||
IChooseAdapterEntity[]
|
IChooseAdapterEntity[]
|
||||||
>([]);
|
>([]);
|
||||||
const [adapterIconList, setAdapterIconList] = useState<
|
|
||||||
Record<string, string>
|
|
||||||
>({});
|
|
||||||
const [adapterDescriptionList, setAdapterDescriptionList] = useState<
|
const [adapterDescriptionList, setAdapterDescriptionList] = useState<
|
||||||
Record<string, string>
|
Record<string, string>
|
||||||
>({});
|
>({});
|
||||||
@@ -140,11 +138,16 @@ export default function BotForm({
|
|||||||
return dynamicFormConfigList;
|
return dynamicFormConfigList;
|
||||||
}, [currentAdapter, enableWebhook, dynamicFormConfigList]);
|
}, [currentAdapter, enableWebhook, dynamicFormConfigList]);
|
||||||
|
|
||||||
|
// Notify parent when dirty state changes
|
||||||
|
const { isDirty } = form.formState;
|
||||||
|
useEffect(() => {
|
||||||
|
onDirtyChange?.(isDirty);
|
||||||
|
}, [isDirty, onDirtyChange]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setBotFormValues();
|
setBotFormValues();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 复制到剪贴板的辅助函数
|
|
||||||
const copyToClipboard = (
|
const copyToClipboard = (
|
||||||
text: string,
|
text: string,
|
||||||
setStatus: React.Dispatch<React.SetStateAction<boolean>>,
|
setStatus: React.Dispatch<React.SetStateAction<boolean>>,
|
||||||
@@ -157,7 +160,6 @@ export default function BotForm({
|
|||||||
setTimeout(() => setStatus(false), 2000);
|
setTimeout(() => setStatus(false), 2000);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// 降级:创建临时textarea复制
|
|
||||||
fallbackCopy(text, setStatus);
|
fallbackCopy(text, setStatus);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -184,21 +186,23 @@ export default function BotForm({
|
|||||||
};
|
};
|
||||||
|
|
||||||
function setBotFormValues() {
|
function setBotFormValues() {
|
||||||
|
isInitializing.current = true;
|
||||||
initBotFormComponent().then(() => {
|
initBotFormComponent().then(() => {
|
||||||
// 拉取初始化表单信息
|
|
||||||
if (initBotId) {
|
if (initBotId) {
|
||||||
getBotConfig(initBotId)
|
getBotConfig(initBotId)
|
||||||
.then((val) => {
|
.then((val) => {
|
||||||
form.setValue('name', val.name);
|
// Use form.reset() to set values AND update the dirty baseline,
|
||||||
form.setValue('description', val.description);
|
// so isDirty stays false after initial load.
|
||||||
form.setValue('adapter', val.adapter);
|
form.reset({
|
||||||
form.setValue('adapter_config', val.adapter_config);
|
name: val.name,
|
||||||
form.setValue('enable', val.enable);
|
description: val.description,
|
||||||
form.setValue('use_pipeline_uuid', val.use_pipeline_uuid || '');
|
adapter: val.adapter,
|
||||||
|
adapter_config: val.adapter_config,
|
||||||
|
enable: val.enable,
|
||||||
|
use_pipeline_uuid: val.use_pipeline_uuid || '',
|
||||||
|
});
|
||||||
handleAdapterSelect(val.adapter);
|
handleAdapterSelect(val.adapter);
|
||||||
// dynamicForm.setFieldsValue(val.adapter_config);
|
|
||||||
|
|
||||||
// 设置 webhook 地址(如果有)
|
|
||||||
if (val.webhook_full_url) {
|
if (val.webhook_full_url) {
|
||||||
setWebhookUrl(val.webhook_full_url);
|
setWebhookUrl(val.webhook_full_url);
|
||||||
} else {
|
} else {
|
||||||
@@ -210,17 +214,20 @@ export default function BotForm({
|
|||||||
toast.error(
|
toast.error(
|
||||||
t('bots.getBotConfigError') + (err as CustomApiError).msg,
|
t('bots.getBotConfigError') + (err as CustomApiError).msg,
|
||||||
);
|
);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
isInitializing.current = false;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
form.reset();
|
form.reset();
|
||||||
setWebhookUrl('');
|
setWebhookUrl('');
|
||||||
setExtraWebhookUrl('');
|
setExtraWebhookUrl('');
|
||||||
|
isInitializing.current = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initBotFormComponent() {
|
async function initBotFormComponent() {
|
||||||
// 初始化流水线列表
|
|
||||||
const pipelinesRes = await httpClient.getPipelines();
|
const pipelinesRes = await httpClient.getPipelines();
|
||||||
setPipelineNameList(
|
setPipelineNameList(
|
||||||
pipelinesRes.pipelines.map((item) => {
|
pipelinesRes.pipelines.map((item) => {
|
||||||
@@ -231,7 +238,6 @@ export default function BotForm({
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 拉取adapter
|
|
||||||
const adaptersRes = await httpClient.getAdapters();
|
const adaptersRes = await httpClient.getAdapters();
|
||||||
setAdapterNameList(
|
setAdapterNameList(
|
||||||
adaptersRes.adapters.map((item) => {
|
adaptersRes.adapters.map((item) => {
|
||||||
@@ -242,18 +248,6 @@ export default function BotForm({
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 初始化适配器图标列表
|
|
||||||
setAdapterIconList(
|
|
||||||
adaptersRes.adapters.reduce(
|
|
||||||
(acc, item) => {
|
|
||||||
acc[item.name] = httpClient.getAdapterIconURL(item.name);
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{} as Record<string, string>,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 初始化适配器描述列表
|
|
||||||
setAdapterDescriptionList(
|
setAdapterDescriptionList(
|
||||||
adaptersRes.adapters.reduce(
|
adaptersRes.adapters.reduce(
|
||||||
(acc, item) => {
|
(acc, item) => {
|
||||||
@@ -264,7 +258,6 @@ export default function BotForm({
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 初始化适配器表单map
|
|
||||||
adaptersRes.adapters.forEach((rawAdapter) => {
|
adaptersRes.adapters.forEach((rawAdapter) => {
|
||||||
adapterNameToDynamicConfigMap.set(
|
adapterNameToDynamicConfigMap.set(
|
||||||
rawAdapter.name,
|
rawAdapter.name,
|
||||||
@@ -341,11 +334,9 @@ export default function BotForm({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只有通过外层固定表单验证才会走到这里,真正的提交逻辑在这里
|
|
||||||
function onDynamicFormSubmit() {
|
function onDynamicFormSubmit() {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
if (initBotId) {
|
if (initBotId) {
|
||||||
// 编辑提交
|
|
||||||
const updateBot: Bot = {
|
const updateBot: Bot = {
|
||||||
uuid: initBotId,
|
uuid: initBotId,
|
||||||
name: form.getValues().name,
|
name: form.getValues().name,
|
||||||
@@ -358,6 +349,8 @@ export default function BotForm({
|
|||||||
httpClient
|
httpClient
|
||||||
.updateBot(initBotId, updateBot)
|
.updateBot(initBotId, updateBot)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
// Reset dirty baseline to current values so isDirty becomes false
|
||||||
|
form.reset(form.getValues());
|
||||||
onFormSubmit(form.getValues());
|
onFormSubmit(form.getValues());
|
||||||
toast.success(t('bots.saveSuccess'));
|
toast.success(t('bots.saveSuccess'));
|
||||||
})
|
})
|
||||||
@@ -366,11 +359,8 @@ export default function BotForm({
|
|||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
// form.reset();
|
|
||||||
// dynamicForm.resetFields();
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 创建提交
|
|
||||||
const newBot: Bot = {
|
const newBot: Bot = {
|
||||||
name: form.getValues().name,
|
name: form.getValues().name,
|
||||||
description: form.getValues().description,
|
description: form.getValues().description,
|
||||||
@@ -393,181 +383,30 @@ export default function BotForm({
|
|||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
form.reset();
|
form.reset();
|
||||||
// dynamicForm.resetFields();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteBot() {
|
// --- Webhook URL display helper ---
|
||||||
if (initBotId) {
|
const showWebhook =
|
||||||
httpClient
|
initBotId &&
|
||||||
.deleteBot(initBotId)
|
webhookUrl &&
|
||||||
.then(() => {
|
(currentAdapter !== 'lark' || enableWebhook !== false);
|
||||||
onBotDeleted();
|
|
||||||
toast.success(t('bots.deleteSuccess'));
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
toast.error(t('bots.deleteError') + err.msg);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Form {...form}>
|
||||||
<Dialog
|
<form
|
||||||
open={showDeleteConfirmModal}
|
id="bot-form"
|
||||||
onOpenChange={setShowDeleteConfirmModal}
|
onSubmit={form.handleSubmit(onDynamicFormSubmit)}
|
||||||
|
className="space-y-6"
|
||||||
>
|
>
|
||||||
<DialogContent>
|
{/* Card 1: Basic Information */}
|
||||||
<DialogHeader>
|
<Card>
|
||||||
<DialogTitle>{t('common.confirmDelete')}</DialogTitle>
|
<CardHeader>
|
||||||
</DialogHeader>
|
<CardTitle>{t('bots.basicInfo')}</CardTitle>
|
||||||
<DialogDescription>{t('bots.deleteConfirmation')}</DialogDescription>
|
<CardDescription>{t('bots.basicInfoDescription')}</CardDescription>
|
||||||
<DialogFooter>
|
</CardHeader>
|
||||||
<Button
|
<CardContent className="space-y-4">
|
||||||
variant="outline"
|
|
||||||
onClick={() => setShowDeleteConfirmModal(false)}
|
|
||||||
>
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
onClick={() => {
|
|
||||||
deleteBot();
|
|
||||||
setShowDeleteConfirmModal(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('common.confirmDelete')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
id="bot-form"
|
|
||||||
onSubmit={form.handleSubmit(onDynamicFormSubmit)}
|
|
||||||
className="space-y-8"
|
|
||||||
>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{/* 是否启用 & 绑定流水线 仅在编辑模式 */}
|
|
||||||
{initBotId && (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center gap-6">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="enable"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-col justify-start gap-[0.8rem] h-[3.8rem]">
|
|
||||||
<FormLabel>{t('common.enable')}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="use_pipeline_uuid"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-col justify-start gap-[0.8rem] h-[3.8rem]">
|
|
||||||
<FormLabel>{t('bots.bindPipeline')}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Select onValueChange={field.onChange} {...field}>
|
|
||||||
<SelectTrigger className="bg-[#ffffff] dark:bg-[#2a2a2e]">
|
|
||||||
<SelectValue
|
|
||||||
placeholder={t('bots.selectPipeline')}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent className="fixed z-[1000]">
|
|
||||||
<SelectGroup>
|
|
||||||
{pipelineNameList.map((item) => (
|
|
||||||
<SelectItem
|
|
||||||
key={item.value}
|
|
||||||
value={item.value}
|
|
||||||
>
|
|
||||||
{item.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectGroup>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Webhook 地址显示(统一 Webhook 模式) */}
|
|
||||||
{webhookUrl &&
|
|
||||||
(currentAdapter !== 'lark' || enableWebhook !== false) && (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('bots.webhookUrl')}</FormLabel>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Input
|
|
||||||
value={webhookUrl}
|
|
||||||
readOnly
|
|
||||||
className="flex-1 bg-gray-50 dark:bg-gray-900"
|
|
||||||
onClick={(e) => {
|
|
||||||
// 点击输入框时自动全选
|
|
||||||
(e.target as HTMLInputElement).select();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => copyToClipboard(webhookUrl, setCopied)}
|
|
||||||
>
|
|
||||||
{copied ? (
|
|
||||||
<Check className="h-4 w-4 text-green-600 mr-2" />
|
|
||||||
) : (
|
|
||||||
<Copy className="h-4 w-4 mr-2" />
|
|
||||||
)}
|
|
||||||
{t('common.copy')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{extraWebhookUrl && (
|
|
||||||
<div className="flex items-center gap-2 mt-2">
|
|
||||||
<Input
|
|
||||||
value={extraWebhookUrl}
|
|
||||||
readOnly
|
|
||||||
className="flex-1 bg-gray-50 dark:bg-gray-900"
|
|
||||||
onClick={(e) => {
|
|
||||||
(e.target as HTMLInputElement).select();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() =>
|
|
||||||
copyToClipboard(extraWebhookUrl, setExtraCopied)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{extraCopied ? (
|
|
||||||
<Check className="h-4 w-4 text-green-600 mr-2" />
|
|
||||||
) : (
|
|
||||||
<Copy className="h-4 w-4 mr-2" />
|
|
||||||
)}
|
|
||||||
{t('common.copy')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<p className="text-sm text-gray-500 mt-1">
|
|
||||||
{extraWebhookUrl
|
|
||||||
? t('bots.webhookUrlHintEither')
|
|
||||||
: t('bots.webhookUrlHint')}
|
|
||||||
</p>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
@@ -575,7 +414,7 @@ export default function BotForm({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t('bots.botName')}
|
{t('bots.botName')}
|
||||||
<span className="text-red-500">*</span>
|
<span className="text-destructive">*</span>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
@@ -591,7 +430,7 @@ export default function BotForm({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t('bots.botDescription')}
|
{t('bots.botDescription')}
|
||||||
<span className="text-red-500">*</span>
|
<span className="text-destructive">*</span>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
@@ -600,31 +439,33 @@ export default function BotForm({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<FormField
|
{/* Card 2: Pipeline Binding (edit mode only) */}
|
||||||
control={form.control}
|
{initBotId && (
|
||||||
name="adapter"
|
<Card>
|
||||||
render={({ field }) => (
|
<CardHeader>
|
||||||
<FormItem>
|
<CardTitle>{t('bots.routingConnection')}</CardTitle>
|
||||||
<FormLabel>
|
<CardDescription>
|
||||||
{t('bots.platformAdapter')}
|
{t('bots.routingConnectionDescription')}
|
||||||
<span className="text-red-500">*</span>
|
</CardDescription>
|
||||||
</FormLabel>
|
</CardHeader>
|
||||||
<FormControl>
|
<CardContent>
|
||||||
<div className="relative">
|
<FormField
|
||||||
<Select
|
control={form.control}
|
||||||
onValueChange={(value) => {
|
name="use_pipeline_uuid"
|
||||||
field.onChange(value);
|
render={({ field }) => (
|
||||||
handleAdapterSelect(value);
|
<FormItem>
|
||||||
}}
|
<FormLabel>{t('bots.bindPipeline')}</FormLabel>
|
||||||
value={field.value}
|
<FormControl>
|
||||||
>
|
<Select onValueChange={field.onChange} {...field}>
|
||||||
<SelectTrigger className="w-[180px] bg-[#ffffff] dark:bg-[#2a2a2e]">
|
<SelectTrigger>
|
||||||
<SelectValue placeholder={t('bots.selectAdapter')} />
|
<SelectValue placeholder={t('bots.selectPipeline')} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="fixed z-[1000]">
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
{adapterNameList.map((item) => (
|
{pipelineNameList.map((item) => (
|
||||||
<SelectItem key={item.value} value={item.value}>
|
<SelectItem key={item.value} value={item.value}>
|
||||||
{item.label}
|
{item.label}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@@ -632,52 +473,138 @@ export default function BotForm({
|
|||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Card 3: Adapter Configuration */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{t('bots.adapterConfig')}</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{t('bots.adapterConfigDescription')}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="adapter"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t('bots.platformAdapter')}
|
||||||
|
<span className="text-destructive">*</span>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Select
|
||||||
|
onValueChange={(value) => {
|
||||||
|
field.onChange(value);
|
||||||
|
handleAdapterSelect(value);
|
||||||
|
}}
|
||||||
|
value={field.value}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[240px]">
|
||||||
|
<SelectValue placeholder={t('bots.selectAdapter')} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
{adapterNameList.map((item) => (
|
||||||
|
<SelectItem key={item.value} value={item.value}>
|
||||||
|
{item.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
{currentAdapter && adapterDescriptionList[currentAdapter] && (
|
||||||
|
<FormDescription>
|
||||||
|
{adapterDescriptionList[currentAdapter]}
|
||||||
|
</FormDescription>
|
||||||
|
)}
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{form.watch('adapter') && (
|
{/* Webhook URL: shown after adapter is selected (edit mode only) */}
|
||||||
<div className="flex items-start gap-3 p-4 rounded-lg border">
|
{showWebhook && (
|
||||||
<img
|
<FormItem>
|
||||||
src={adapterIconList[form.watch('adapter')]}
|
<FormLabel>{t('bots.webhookUrl')}</FormLabel>
|
||||||
alt="adapter icon"
|
<div className="flex items-center gap-2">
|
||||||
className="w-12 h-12 rounded-[8%]"
|
<Input
|
||||||
/>
|
value={webhookUrl}
|
||||||
<div className="flex flex-col gap-1">
|
readOnly
|
||||||
<div className="font-medium">
|
className="flex-1 bg-muted"
|
||||||
{
|
onClick={(e) => {
|
||||||
adapterNameList.find(
|
(e.target as HTMLInputElement).select();
|
||||||
(item) => item.value === form.watch('adapter'),
|
}}
|
||||||
)?.label
|
/>
|
||||||
}
|
<Button
|
||||||
</div>
|
type="button"
|
||||||
<div className="text-sm text-gray-500">
|
variant="outline"
|
||||||
{adapterDescriptionList[form.watch('adapter')]}
|
size="sm"
|
||||||
</div>
|
onClick={() => copyToClipboard(webhookUrl, setCopied)}
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<Check className="h-4 w-4 text-green-600" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{extraWebhookUrl && (
|
||||||
|
<div className="flex items-center gap-2 mt-2">
|
||||||
|
<Input
|
||||||
|
value={extraWebhookUrl}
|
||||||
|
readOnly
|
||||||
|
className="flex-1 bg-muted"
|
||||||
|
onClick={(e) => {
|
||||||
|
(e.target as HTMLInputElement).select();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
copyToClipboard(extraWebhookUrl, setExtraCopied)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{extraCopied ? (
|
||||||
|
<Check className="h-4 w-4 text-green-600" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<FormDescription>
|
||||||
|
{extraWebhookUrl
|
||||||
|
? t('bots.webhookUrlHintEither')
|
||||||
|
: t('bots.webhookUrlHint')}
|
||||||
|
</FormDescription>
|
||||||
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showDynamicForm && filteredDynamicFormConfigList.length > 0 && (
|
{showDynamicForm && filteredDynamicFormConfigList.length > 0 && (
|
||||||
<div className="space-y-4">
|
<DynamicFormComponent
|
||||||
<div className="text-lg font-medium">
|
itemConfigList={filteredDynamicFormConfigList}
|
||||||
{t('bots.adapterConfig')}
|
initialValues={currentAdapterConfig}
|
||||||
</div>
|
onSubmit={(values) => {
|
||||||
<DynamicFormComponent
|
form.setValue('adapter_config', values, {
|
||||||
itemConfigList={filteredDynamicFormConfigList}
|
shouldDirty: !isInitializing.current,
|
||||||
initialValues={currentAdapterConfig}
|
});
|
||||||
onSubmit={(values) => {
|
}}
|
||||||
form.setValue('adapter_config', values);
|
/>
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</CardContent>
|
||||||
</form>
|
</Card>
|
||||||
</Form>
|
</form>
|
||||||
</div>
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -294,6 +294,17 @@ const enUS = {
|
|||||||
log: 'Log',
|
log: 'Log',
|
||||||
configuration: 'Configuration',
|
configuration: 'Configuration',
|
||||||
logs: 'Logs',
|
logs: 'Logs',
|
||||||
|
basicInfo: 'Basic Information',
|
||||||
|
basicInfoDescription: 'Set the bot name and description',
|
||||||
|
routingConnection: 'Routing & Connection',
|
||||||
|
routingConnectionDescription:
|
||||||
|
'Bind the pipeline that processes messages for this bot',
|
||||||
|
adapterConfigDescription: 'Configure the selected platform adapter',
|
||||||
|
dangerZone: 'Danger Zone',
|
||||||
|
dangerZoneDescription: 'Irreversible and destructive actions',
|
||||||
|
deleteBotAction: 'Delete this bot',
|
||||||
|
deleteBotHint:
|
||||||
|
'Once deleted, all associated configuration will be permanently removed.',
|
||||||
webhookUrl: 'Webhook Callback URL',
|
webhookUrl: 'Webhook Callback URL',
|
||||||
webhookUrlCopied: 'Webhook URL copied',
|
webhookUrlCopied: 'Webhook URL copied',
|
||||||
webhookUrlHint:
|
webhookUrlHint:
|
||||||
|
|||||||
@@ -299,6 +299,17 @@
|
|||||||
log: 'ログ',
|
log: 'ログ',
|
||||||
configuration: '設定',
|
configuration: '設定',
|
||||||
logs: 'ログ',
|
logs: 'ログ',
|
||||||
|
basicInfo: '基本情報',
|
||||||
|
basicInfoDescription: 'ボットの名前と説明を設定',
|
||||||
|
routingConnection: 'ルーティングと接続',
|
||||||
|
routingConnectionDescription:
|
||||||
|
'このボットのメッセージを処理するパイプラインを紐付け',
|
||||||
|
adapterConfigDescription: '選択したプラットフォームアダプターを設定',
|
||||||
|
dangerZone: '危険ゾーン',
|
||||||
|
dangerZoneDescription: '元に戻せない操作',
|
||||||
|
deleteBotAction: 'このボットを削除',
|
||||||
|
deleteBotHint:
|
||||||
|
'削除すると、関連する全ての設定が完全に削除され、復元できません。',
|
||||||
webhookUrl: 'Webhook コールバック URL',
|
webhookUrl: 'Webhook コールバック URL',
|
||||||
webhookUrlCopied: 'Webhook URL をコピーしました',
|
webhookUrlCopied: 'Webhook URL をコピーしました',
|
||||||
webhookUrlHint:
|
webhookUrlHint:
|
||||||
|
|||||||
@@ -282,6 +282,15 @@ const zhHans = {
|
|||||||
log: '日志',
|
log: '日志',
|
||||||
configuration: '配置',
|
configuration: '配置',
|
||||||
logs: '日志',
|
logs: '日志',
|
||||||
|
basicInfo: '基础信息',
|
||||||
|
basicInfoDescription: '设置机器人名称和描述',
|
||||||
|
routingConnection: '路由与连接',
|
||||||
|
routingConnectionDescription: '绑定处理此机器人消息的流水线',
|
||||||
|
adapterConfigDescription: '配置所选平台适配器',
|
||||||
|
dangerZone: '危险区域',
|
||||||
|
dangerZoneDescription: '不可逆的操作',
|
||||||
|
deleteBotAction: '删除此机器人',
|
||||||
|
deleteBotHint: '删除后,所有关联配置将被永久移除,且无法恢复。',
|
||||||
webhookUrl: 'Webhook 回调地址',
|
webhookUrl: 'Webhook 回调地址',
|
||||||
webhookUrlCopied: 'Webhook 地址已复制',
|
webhookUrlCopied: 'Webhook 地址已复制',
|
||||||
webhookUrlHint:
|
webhookUrlHint:
|
||||||
|
|||||||
@@ -281,6 +281,15 @@ const zhHant = {
|
|||||||
log: '日誌',
|
log: '日誌',
|
||||||
configuration: '設定',
|
configuration: '設定',
|
||||||
logs: '日誌',
|
logs: '日誌',
|
||||||
|
basicInfo: '基礎資訊',
|
||||||
|
basicInfoDescription: '設定機器人名稱和描述',
|
||||||
|
routingConnection: '路由與連接',
|
||||||
|
routingConnectionDescription: '綁定處理此機器人訊息的流程線',
|
||||||
|
adapterConfigDescription: '設定所選平台適配器',
|
||||||
|
dangerZone: '危險區域',
|
||||||
|
dangerZoneDescription: '不可逆的操作',
|
||||||
|
deleteBotAction: '刪除此機器人',
|
||||||
|
deleteBotHint: '刪除後,所有關聯設定將被永久移除,且無法復原。',
|
||||||
webhookUrl: 'Webhook 回調位址',
|
webhookUrl: 'Webhook 回調位址',
|
||||||
webhookUrlCopied: 'Webhook 位址已複製',
|
webhookUrlCopied: 'Webhook 位址已複製',
|
||||||
webhookUrlHint:
|
webhookUrlHint:
|
||||||
|
|||||||
Reference in New Issue
Block a user