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:
Junyan Qin
2026-03-27 12:29:18 +08:00
parent e8dc6fde53
commit 127dc455c3
6 changed files with 379 additions and 311 deletions

View File

@@ -1,9 +1,18 @@
'use client';
import { useState, useEffect, useRef } from 'react';
import { useState, useEffect, useRef, useCallback } from 'react';
import { useRouter } from 'next/navigation';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
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 {
Dialog,
DialogContent,
@@ -19,8 +28,9 @@ import type { BotSessionMonitorHandle } from '@/app/home/bots/components/bot-ses
import { httpClient } from '@/app/infra/http/HttpClient';
import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext';
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 { toast } from 'sonner';
export default function BotDetailContent({ id }: { id: string }) {
const isCreateMode = id === 'new';
@@ -44,7 +54,52 @@ export default function BotDetailContent({ id }: { id: string }) {
const [isRefreshingSessions, setIsRefreshingSessions] = useState(false);
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() {
// Re-sync enable state after form save (form may update enable too)
httpClient.getBot(id).then((res) => {
setBotEnabled(res.bot.enable ?? true);
});
refreshBots();
}
@@ -55,57 +110,79 @@ export default function BotDetailContent({ id }: { id: string }) {
function handleNewBotCreated(newBotId: string) {
refreshBots();
// Navigate to the newly created bot's detail view via query param
router.push(`/home/bots?id=${encodeURIComponent(newBotId)}`);
}
function handleDelete() {
setShowDeleteConfirm(true);
}
function confirmDelete() {
httpClient.deleteBot(id).then(() => {
setShowDeleteConfirm(false);
handleBotDeleted();
});
httpClient
.deleteBot(id)
.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) {
return (
<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>
<Button type="submit" form="bot-form">
{t('common.submit')}
</Button>
</div>
{/* Content */}
<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
initBotId={undefined}
onFormSubmit={handleFormSubmit}
onBotDeleted={handleBotDeleted}
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>
);
}
// Edit mode: tabbed layout with config, logs, sessions
// ==================== Edit Mode ====================
return (
<>
<div className="flex h-full flex-col">
<div className="flex items-center gap-3 pb-4 shrink-0">
<h1 className="text-xl font-semibold">{t('bots.editBot')}</h1>
{/* Sticky Header: title + enable switch + save button */}
<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>
{/* Horizontal Tabs */}
<Tabs
key={id}
value={activeTab}
@@ -152,33 +229,56 @@ export default function BotDetailContent({ id }: { id: string }) {
</TabsTrigger>
</TabsList>
{/* Tab: Configuration */}
<TabsContent
value="config"
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
initBotId={id}
onFormSubmit={handleFormSubmit}
onBotDeleted={handleBotDeleted}
onNewBotCreated={handleNewBotCreated}
onDirtyChange={setFormDirty}
/>
<div className="flex justify-end gap-2 mt-6 pb-4">
<Button
type="button"
variant="destructive"
onClick={handleDelete}
>
{t('common.delete')}
</Button>
<Button type="submit" form="bot-form">
{t('common.save')}
</Button>
</div>
{/* Card: Danger Zone */}
<Card className="border-destructive/50">
<CardHeader>
<CardTitle className="text-destructive">
{t('bots.dangerZone')}
</CardTitle>
<CardDescription>
{t('bots.dangerZoneDescription')}
</CardDescription>
</CardHeader>
<CardContent>
<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>
</TabsContent>
{/* Tab: Logs */}
<TabsContent
value="logs"
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} />
</TabsContent>
{/* Tab: Sessions */}
<TabsContent value="sessions" className="flex-1 min-h-0 mt-4">
<BotSessionMonitor ref={sessionMonitorRef} botId={id} />
</TabsContent>

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useState } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import {
IChooseAdapterEntity,
IPipelineEntity,
@@ -21,18 +21,11 @@ import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import { Copy, Check } from 'lucide-react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
@@ -47,7 +40,13 @@ import {
SelectTrigger,
SelectValue,
} 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 { CustomApiError } from '@/app/infra/entities/common';
@@ -68,11 +67,13 @@ export default function BotForm({
onFormSubmit,
onBotDeleted,
onNewBotCreated,
onDirtyChange,
}: {
initBotId?: string;
onFormSubmit: (value: z.infer<ReturnType<typeof getFormSchema>>) => void;
onBotDeleted: () => void;
onNewBotCreated: (botId: string) => void;
onDirtyChange?: (dirty: boolean) => void;
}) {
const { t } = useTranslation();
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] =
useState(new Map<string, IDynamicFormItemSchema[]>());
// const [form] = Form.useForm<IBotFormEntity>();
const [showDynamicForm, setShowDynamicForm] = useState<boolean>(false);
// const [dynamicForm] = Form.useForm();
const [adapterNameList, setAdapterNameList] = useState<
IChooseAdapterEntity[]
>([]);
const [adapterIconList, setAdapterIconList] = useState<
Record<string, string>
>({});
const [adapterDescriptionList, setAdapterDescriptionList] = useState<
Record<string, string>
>({});
@@ -140,11 +138,16 @@ export default function BotForm({
return dynamicFormConfigList;
}, [currentAdapter, enableWebhook, dynamicFormConfigList]);
// Notify parent when dirty state changes
const { isDirty } = form.formState;
useEffect(() => {
onDirtyChange?.(isDirty);
}, [isDirty, onDirtyChange]);
useEffect(() => {
setBotFormValues();
}, []);
// 复制到剪贴板的辅助函数
const copyToClipboard = (
text: string,
setStatus: React.Dispatch<React.SetStateAction<boolean>>,
@@ -157,7 +160,6 @@ export default function BotForm({
setTimeout(() => setStatus(false), 2000);
})
.catch(() => {
// 降级创建临时textarea复制
fallbackCopy(text, setStatus);
});
} else {
@@ -184,21 +186,23 @@ export default function BotForm({
};
function setBotFormValues() {
isInitializing.current = true;
initBotFormComponent().then(() => {
// 拉取初始化表单信息
if (initBotId) {
getBotConfig(initBotId)
.then((val) => {
form.setValue('name', val.name);
form.setValue('description', val.description);
form.setValue('adapter', val.adapter);
form.setValue('adapter_config', val.adapter_config);
form.setValue('enable', val.enable);
form.setValue('use_pipeline_uuid', val.use_pipeline_uuid || '');
// Use form.reset() to set values AND update the dirty baseline,
// so isDirty stays false after initial load.
form.reset({
name: val.name,
description: val.description,
adapter: val.adapter,
adapter_config: val.adapter_config,
enable: val.enable,
use_pipeline_uuid: val.use_pipeline_uuid || '',
});
handleAdapterSelect(val.adapter);
// dynamicForm.setFieldsValue(val.adapter_config);
// 设置 webhook 地址(如果有)
if (val.webhook_full_url) {
setWebhookUrl(val.webhook_full_url);
} else {
@@ -210,17 +214,20 @@ export default function BotForm({
toast.error(
t('bots.getBotConfigError') + (err as CustomApiError).msg,
);
})
.finally(() => {
isInitializing.current = false;
});
} else {
form.reset();
setWebhookUrl('');
setExtraWebhookUrl('');
isInitializing.current = false;
}
});
}
async function initBotFormComponent() {
// 初始化流水线列表
const pipelinesRes = await httpClient.getPipelines();
setPipelineNameList(
pipelinesRes.pipelines.map((item) => {
@@ -231,7 +238,6 @@ export default function BotForm({
}),
);
// 拉取adapter
const adaptersRes = await httpClient.getAdapters();
setAdapterNameList(
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(
adaptersRes.adapters.reduce(
(acc, item) => {
@@ -264,7 +258,6 @@ export default function BotForm({
),
);
// 初始化适配器表单map
adaptersRes.adapters.forEach((rawAdapter) => {
adapterNameToDynamicConfigMap.set(
rawAdapter.name,
@@ -341,11 +334,9 @@ export default function BotForm({
}
}
// 只有通过外层固定表单验证才会走到这里,真正的提交逻辑在这里
function onDynamicFormSubmit() {
setIsLoading(true);
if (initBotId) {
// 编辑提交
const updateBot: Bot = {
uuid: initBotId,
name: form.getValues().name,
@@ -358,6 +349,8 @@ export default function BotForm({
httpClient
.updateBot(initBotId, updateBot)
.then(() => {
// Reset dirty baseline to current values so isDirty becomes false
form.reset(form.getValues());
onFormSubmit(form.getValues());
toast.success(t('bots.saveSuccess'));
})
@@ -366,11 +359,8 @@ export default function BotForm({
})
.finally(() => {
setIsLoading(false);
// form.reset();
// dynamicForm.resetFields();
});
} else {
// 创建提交
const newBot: Bot = {
name: form.getValues().name,
description: form.getValues().description,
@@ -393,181 +383,30 @@ export default function BotForm({
.finally(() => {
setIsLoading(false);
form.reset();
// dynamicForm.resetFields();
});
}
}
function deleteBot() {
if (initBotId) {
httpClient
.deleteBot(initBotId)
.then(() => {
onBotDeleted();
toast.success(t('bots.deleteSuccess'));
})
.catch((err) => {
toast.error(t('bots.deleteError') + err.msg);
});
}
}
// --- Webhook URL display helper ---
const showWebhook =
initBotId &&
webhookUrl &&
(currentAdapter !== 'lark' || enableWebhook !== false);
return (
<div>
<Dialog
open={showDeleteConfirmModal}
onOpenChange={setShowDeleteConfirmModal}
<Form {...form}>
<form
id="bot-form"
onSubmit={form.handleSubmit(onDynamicFormSubmit)}
className="space-y-6"
>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('common.confirmDelete')}</DialogTitle>
</DialogHeader>
<DialogDescription>{t('bots.deleteConfirmation')}</DialogDescription>
<DialogFooter>
<Button
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>
)}
</>
)}
{/* Card 1: Basic Information */}
<Card>
<CardHeader>
<CardTitle>{t('bots.basicInfo')}</CardTitle>
<CardDescription>{t('bots.basicInfoDescription')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<FormField
control={form.control}
name="name"
@@ -575,7 +414,7 @@ export default function BotForm({
<FormItem>
<FormLabel>
{t('bots.botName')}
<span className="text-red-500">*</span>
<span className="text-destructive">*</span>
</FormLabel>
<FormControl>
<Input {...field} />
@@ -591,7 +430,7 @@ export default function BotForm({
<FormItem>
<FormLabel>
{t('bots.botDescription')}
<span className="text-red-500">*</span>
<span className="text-destructive">*</span>
</FormLabel>
<FormControl>
<Input {...field} />
@@ -600,31 +439,33 @@ export default function BotForm({
</FormItem>
)}
/>
</CardContent>
</Card>
<FormField
control={form.control}
name="adapter"
render={({ field }) => (
<FormItem>
<FormLabel>
{t('bots.platformAdapter')}
<span className="text-red-500">*</span>
</FormLabel>
<FormControl>
<div className="relative">
<Select
onValueChange={(value) => {
field.onChange(value);
handleAdapterSelect(value);
}}
value={field.value}
>
<SelectTrigger className="w-[180px] bg-[#ffffff] dark:bg-[#2a2a2e]">
<SelectValue placeholder={t('bots.selectAdapter')} />
{/* Card 2: Pipeline Binding (edit mode only) */}
{initBotId && (
<Card>
<CardHeader>
<CardTitle>{t('bots.routingConnection')}</CardTitle>
<CardDescription>
{t('bots.routingConnectionDescription')}
</CardDescription>
</CardHeader>
<CardContent>
<FormField
control={form.control}
name="use_pipeline_uuid"
render={({ field }) => (
<FormItem>
<FormLabel>{t('bots.bindPipeline')}</FormLabel>
<FormControl>
<Select onValueChange={field.onChange} {...field}>
<SelectTrigger>
<SelectValue placeholder={t('bots.selectPipeline')} />
</SelectTrigger>
<SelectContent className="fixed z-[1000]">
<SelectContent>
<SelectGroup>
{adapterNameList.map((item) => (
{pipelineNameList.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
@@ -632,52 +473,138 @@ export default function BotForm({
</SelectGroup>
</SelectContent>
</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>
{currentAdapter && adapterDescriptionList[currentAdapter] && (
<FormDescription>
{adapterDescriptionList[currentAdapter]}
</FormDescription>
)}
<FormMessage />
</FormItem>
)}
/>
{form.watch('adapter') && (
<div className="flex items-start gap-3 p-4 rounded-lg border">
<img
src={adapterIconList[form.watch('adapter')]}
alt="adapter icon"
className="w-12 h-12 rounded-[8%]"
/>
<div className="flex flex-col gap-1">
<div className="font-medium">
{
adapterNameList.find(
(item) => item.value === form.watch('adapter'),
)?.label
}
</div>
<div className="text-sm text-gray-500">
{adapterDescriptionList[form.watch('adapter')]}
</div>
{/* Webhook URL: shown after adapter is selected (edit mode only) */}
{showWebhook && (
<FormItem>
<FormLabel>{t('bots.webhookUrl')}</FormLabel>
<div className="flex items-center gap-2">
<Input
value={webhookUrl}
readOnly
className="flex-1 bg-muted"
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" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</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 && (
<div className="space-y-4">
<div className="text-lg font-medium">
{t('bots.adapterConfig')}
</div>
<DynamicFormComponent
itemConfigList={filteredDynamicFormConfigList}
initialValues={currentAdapterConfig}
onSubmit={(values) => {
form.setValue('adapter_config', values);
}}
/>
</div>
<DynamicFormComponent
itemConfigList={filteredDynamicFormConfigList}
initialValues={currentAdapterConfig}
onSubmit={(values) => {
form.setValue('adapter_config', values, {
shouldDirty: !isInitializing.current,
});
}}
/>
)}
</div>
</form>
</Form>
</div>
</CardContent>
</Card>
</form>
</Form>
);
}

View File

@@ -294,6 +294,17 @@ const enUS = {
log: 'Log',
configuration: 'Configuration',
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',
webhookUrlCopied: 'Webhook URL copied',
webhookUrlHint:

View File

@@ -299,6 +299,17 @@
log: 'ログ',
configuration: '設定',
logs: 'ログ',
basicInfo: '基本情報',
basicInfoDescription: 'ボットの名前と説明を設定',
routingConnection: 'ルーティングと接続',
routingConnectionDescription:
'このボットのメッセージを処理するパイプラインを紐付け',
adapterConfigDescription: '選択したプラットフォームアダプターを設定',
dangerZone: '危険ゾーン',
dangerZoneDescription: '元に戻せない操作',
deleteBotAction: 'このボットを削除',
deleteBotHint:
'削除すると、関連する全ての設定が完全に削除され、復元できません。',
webhookUrl: 'Webhook コールバック URL',
webhookUrlCopied: 'Webhook URL をコピーしました',
webhookUrlHint:

View File

@@ -282,6 +282,15 @@ const zhHans = {
log: '日志',
configuration: '配置',
logs: '日志',
basicInfo: '基础信息',
basicInfoDescription: '设置机器人名称和描述',
routingConnection: '路由与连接',
routingConnectionDescription: '绑定处理此机器人消息的流水线',
adapterConfigDescription: '配置所选平台适配器',
dangerZone: '危险区域',
dangerZoneDescription: '不可逆的操作',
deleteBotAction: '删除此机器人',
deleteBotHint: '删除后,所有关联配置将被永久移除,且无法恢复。',
webhookUrl: 'Webhook 回调地址',
webhookUrlCopied: 'Webhook 地址已复制',
webhookUrlHint:

View File

@@ -281,6 +281,15 @@ const zhHant = {
log: '日誌',
configuration: '設定',
logs: '日誌',
basicInfo: '基礎資訊',
basicInfoDescription: '設定機器人名稱和描述',
routingConnection: '路由與連接',
routingConnectionDescription: '綁定處理此機器人訊息的流程線',
adapterConfigDescription: '設定所選平台適配器',
dangerZone: '危險區域',
dangerZoneDescription: '不可逆的操作',
deleteBotAction: '刪除此機器人',
deleteBotHint: '刪除後,所有關聯設定將被永久移除,且無法復原。',
webhookUrl: 'Webhook 回調位址',
webhookUrlCopied: 'Webhook 位址已複製',
webhookUrlHint: