mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 03:55:55 +00:00
fix: stabilize dynamic forms and mcp testing
This commit is contained in:
@@ -262,6 +262,14 @@ class AgentRunnerRegistry:
|
||||
stages = []
|
||||
|
||||
for descriptor in runners:
|
||||
config_schema = []
|
||||
for index, config_item in enumerate(descriptor.config_schema):
|
||||
item = dict(config_item)
|
||||
if not item.get('id'):
|
||||
item_name = item.get('name') or str(index)
|
||||
item['id'] = f'{descriptor.id}.{item_name}'
|
||||
config_schema.append(item)
|
||||
|
||||
# Add runner option
|
||||
options.append(
|
||||
{
|
||||
@@ -278,7 +286,7 @@ class AgentRunnerRegistry:
|
||||
'name': descriptor.id,
|
||||
'label': descriptor.label,
|
||||
'description': descriptor.description,
|
||||
'config': descriptor.config_schema,
|
||||
'config': config_schema,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -494,12 +494,13 @@ export default function DynamicFormComponent({
|
||||
}}
|
||||
/>
|
||||
|
||||
{itemConfigList.map((config) => {
|
||||
{itemConfigList.map((config, index) => {
|
||||
// Create a normalized config with type converted to frontend format
|
||||
const normalizedConfig = {
|
||||
...config,
|
||||
type: normalizeItemType(config.type),
|
||||
};
|
||||
const fieldKey = config.id || config.name || `field-${index}`;
|
||||
|
||||
if (config.show_if) {
|
||||
const dependValue = resolveShowIfValue(
|
||||
@@ -543,7 +544,7 @@ export default function DynamicFormComponent({
|
||||
|
||||
return (
|
||||
<WebhookUrlField
|
||||
key={config.id}
|
||||
key={fieldKey}
|
||||
label={extractI18nObject(config.label)}
|
||||
description={
|
||||
config.description
|
||||
@@ -574,7 +575,7 @@ export default function DynamicFormComponent({
|
||||
|
||||
return (
|
||||
<EmbedCodeField
|
||||
key={config.id}
|
||||
key={fieldKey}
|
||||
label={extractI18nObject(config.label)}
|
||||
description={
|
||||
config.description
|
||||
@@ -589,7 +590,7 @@ export default function DynamicFormComponent({
|
||||
// QR code login button (e.g. Feishu one-click create, WeChat scan login)
|
||||
if (config.type === 'qr-code-login') {
|
||||
return (
|
||||
<FormItem key={config.id}>
|
||||
<FormItem key={fieldKey}>
|
||||
<div
|
||||
className="relative flex items-center gap-4 p-4 rounded-xl border-2 border-dashed cursor-pointer transition-all hover:border-solid hover:shadow-md group"
|
||||
style={{
|
||||
@@ -650,7 +651,7 @@ export default function DynamicFormComponent({
|
||||
if (normalizedConfig.type === 'boolean') {
|
||||
return (
|
||||
<FormField
|
||||
key={config.id}
|
||||
key={fieldKey}
|
||||
control={form.control}
|
||||
name={config.name as keyof FormValues}
|
||||
render={({ field }) => (
|
||||
@@ -688,7 +689,7 @@ export default function DynamicFormComponent({
|
||||
|
||||
return (
|
||||
<FormField
|
||||
key={config.id}
|
||||
key={fieldKey}
|
||||
control={form.control}
|
||||
name={config.name as keyof FormValues}
|
||||
render={({ field }) => (
|
||||
|
||||
@@ -254,6 +254,7 @@ export default function DynamicFormItemComponent({
|
||||
type="number"
|
||||
className="max-w-xs"
|
||||
{...field}
|
||||
value={field.value ?? ''}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
);
|
||||
@@ -262,7 +263,7 @@ export default function DynamicFormItemComponent({
|
||||
if (config.options && config.options.length > 0) {
|
||||
return (
|
||||
<div className="flex items-center gap-1.5 max-w-md">
|
||||
<Input className="flex-1" {...field} />
|
||||
<Input className="flex-1" {...field} value={field.value ?? ''} />
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
@@ -293,22 +294,33 @@ export default function DynamicFormItemComponent({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <Input className="max-w-md" {...field} />;
|
||||
return (
|
||||
<Input className="max-w-md" {...field} value={field.value ?? ''} />
|
||||
);
|
||||
|
||||
case DynamicFormItemType.TEXT:
|
||||
return <Textarea {...field} className="min-h-[120px] max-w-2xl" />;
|
||||
return (
|
||||
<Textarea
|
||||
{...field}
|
||||
value={field.value ?? ''}
|
||||
className="min-h-[120px] max-w-2xl"
|
||||
/>
|
||||
);
|
||||
|
||||
case DynamicFormItemType.JSON:
|
||||
return (
|
||||
<Textarea
|
||||
{...field}
|
||||
value={field.value ?? ''}
|
||||
className="min-h-[200px] font-mono text-sm"
|
||||
placeholder='{"key": "value"}'
|
||||
/>
|
||||
);
|
||||
|
||||
case DynamicFormItemType.BOOLEAN:
|
||||
return <Switch checked={field.value} onCheckedChange={field.onChange} />;
|
||||
return (
|
||||
<Switch checked={!!field.value} onCheckedChange={field.onChange} />
|
||||
);
|
||||
|
||||
case DynamicFormItemType.STRING_ARRAY:
|
||||
return (
|
||||
@@ -1421,7 +1433,7 @@ export default function DynamicFormItemComponent({
|
||||
{/* 内容输入 */}
|
||||
<Textarea
|
||||
className="w-[300px]"
|
||||
value={item.content}
|
||||
value={item.content ?? ''}
|
||||
onChange={(e) => {
|
||||
const newValue = [...(field.value ?? promptItems)];
|
||||
newValue[index] = {
|
||||
|
||||
@@ -310,6 +310,7 @@ function SingleSelectField({
|
||||
{options.map((opt) => (
|
||||
<div key={opt.id}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChange(opt.id)}
|
||||
className={`w-full text-left text-sm px-3 py-2 rounded-lg border transition-colors ${
|
||||
value === opt.id
|
||||
@@ -361,8 +362,16 @@ function MultiSelectField({
|
||||
const selected = value.includes(opt.id);
|
||||
return (
|
||||
<div key={opt.id}>
|
||||
<button
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => toggle(opt.id)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
toggle(opt.id);
|
||||
}
|
||||
}}
|
||||
className={`w-full text-left text-sm px-3 py-2 rounded-lg border transition-colors flex items-center gap-2 ${
|
||||
selected
|
||||
? 'border-primary bg-primary/5 text-primary'
|
||||
@@ -371,7 +380,7 @@ function MultiSelectField({
|
||||
>
|
||||
<Checkbox checked={selected} className="pointer-events-none" />
|
||||
{getI18nText(opt.label)}
|
||||
</button>
|
||||
</div>
|
||||
{opt.has_input && selected && (
|
||||
<input
|
||||
type="text"
|
||||
|
||||
@@ -283,14 +283,10 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
|
||||
}, [mcpTesting, onTestingChange]);
|
||||
|
||||
// Expose test action and testing state to parent
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
testMcp: () => testMcp(),
|
||||
isTesting: mcpTesting,
|
||||
}),
|
||||
[mcpTesting],
|
||||
);
|
||||
useImperativeHandle(ref, () => ({
|
||||
testMcp: () => testMcp(),
|
||||
isTesting: mcpTesting,
|
||||
}));
|
||||
|
||||
// Load server data
|
||||
useEffect(() => {
|
||||
|
||||
@@ -167,6 +167,8 @@ export default function PipelineFormComponent({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
basic: {
|
||||
name: '',
|
||||
description: '',
|
||||
emoji: '⚙️',
|
||||
},
|
||||
ai: {},
|
||||
@@ -215,8 +217,8 @@ export default function PipelineFormComponent({
|
||||
|
||||
const loadedValues = {
|
||||
basic: {
|
||||
name: resp.pipeline.name,
|
||||
description: resp.pipeline.description,
|
||||
name: resp.pipeline.name ?? '',
|
||||
description: resp.pipeline.description ?? '',
|
||||
emoji: resp.pipeline.emoji || '⚙️',
|
||||
},
|
||||
ai: resp.pipeline.config.ai,
|
||||
@@ -576,7 +578,7 @@ export default function PipelineFormComponent({
|
||||
<span className="text-destructive">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
<Input {...field} value={field.value ?? ''} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -607,7 +609,7 @@ export default function PipelineFormComponent({
|
||||
<FormItem>
|
||||
<FormLabel>{t('common.description')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
<Input {...field} value={field.value ?? ''} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
Reference in New Issue
Block a user