mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-17 01:46:07 +00:00
fix: stabilize dynamic forms and mcp testing
This commit is contained in:
@@ -262,6 +262,14 @@ class AgentRunnerRegistry:
|
|||||||
stages = []
|
stages = []
|
||||||
|
|
||||||
for descriptor in runners:
|
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
|
# Add runner option
|
||||||
options.append(
|
options.append(
|
||||||
{
|
{
|
||||||
@@ -278,7 +286,7 @@ class AgentRunnerRegistry:
|
|||||||
'name': descriptor.id,
|
'name': descriptor.id,
|
||||||
'label': descriptor.label,
|
'label': descriptor.label,
|
||||||
'description': descriptor.description,
|
'description': descriptor.description,
|
||||||
'config': descriptor.config_schema,
|
'config': config_schema,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -502,12 +502,13 @@ export default function DynamicFormComponent({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{itemConfigList.map((config) => {
|
{itemConfigList.map((config, index) => {
|
||||||
// Create a normalized config with type converted to frontend format
|
// Create a normalized config with type converted to frontend format
|
||||||
const normalizedConfig = {
|
const normalizedConfig = {
|
||||||
...config,
|
...config,
|
||||||
type: normalizeItemType(config.type),
|
type: normalizeItemType(config.type),
|
||||||
};
|
};
|
||||||
|
const fieldKey = config.id || config.name || `field-${index}`;
|
||||||
|
|
||||||
if (config.show_if) {
|
if (config.show_if) {
|
||||||
const dependValue = resolveShowIfValue(
|
const dependValue = resolveShowIfValue(
|
||||||
@@ -596,7 +597,7 @@ export default function DynamicFormComponent({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<WebhookUrlField
|
<WebhookUrlField
|
||||||
key={config.id}
|
key={fieldKey}
|
||||||
label={extractI18nObject(config.label)}
|
label={extractI18nObject(config.label)}
|
||||||
description={
|
description={
|
||||||
config.description
|
config.description
|
||||||
@@ -627,7 +628,7 @@ export default function DynamicFormComponent({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<EmbedCodeField
|
<EmbedCodeField
|
||||||
key={config.id}
|
key={fieldKey}
|
||||||
label={extractI18nObject(config.label)}
|
label={extractI18nObject(config.label)}
|
||||||
description={
|
description={
|
||||||
config.description
|
config.description
|
||||||
@@ -642,7 +643,7 @@ export default function DynamicFormComponent({
|
|||||||
// QR code login button (e.g. Feishu one-click create, WeChat scan login)
|
// QR code login button (e.g. Feishu one-click create, WeChat scan login)
|
||||||
if (config.type === 'qr-code-login') {
|
if (config.type === 'qr-code-login') {
|
||||||
return (
|
return (
|
||||||
<FormItem key={config.id}>
|
<FormItem key={fieldKey}>
|
||||||
<div
|
<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"
|
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={{
|
style={{
|
||||||
@@ -703,7 +704,7 @@ export default function DynamicFormComponent({
|
|||||||
if (normalizedConfig.type === 'boolean') {
|
if (normalizedConfig.type === 'boolean') {
|
||||||
return (
|
return (
|
||||||
<FormField
|
<FormField
|
||||||
key={config.id}
|
key={fieldKey}
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name={config.name as keyof FormValues}
|
name={config.name as keyof FormValues}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
@@ -742,7 +743,7 @@ export default function DynamicFormComponent({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<FormField
|
<FormField
|
||||||
key={config.id}
|
key={fieldKey}
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name={config.name as keyof FormValues}
|
name={config.name as keyof FormValues}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
|
|||||||
@@ -253,6 +253,7 @@ export default function DynamicFormItemComponent({
|
|||||||
type="number"
|
type="number"
|
||||||
className="w-full max-w-xs"
|
className="w-full max-w-xs"
|
||||||
{...field}
|
{...field}
|
||||||
|
value={field.value ?? ''}
|
||||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -261,7 +262,11 @@ export default function DynamicFormItemComponent({
|
|||||||
if (config.options && config.options.length > 0) {
|
if (config.options && config.options.length > 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full max-w-md min-w-0 items-center gap-1.5">
|
<div className="flex w-full max-w-md min-w-0 items-center gap-1.5">
|
||||||
<Input className="min-w-0 flex-1" {...field} />
|
<Input
|
||||||
|
className="min-w-0 flex-1"
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ''}
|
||||||
|
/>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
@@ -292,12 +297,19 @@ export default function DynamicFormItemComponent({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <Input className="w-full max-w-md" {...field} />;
|
return (
|
||||||
|
<Input
|
||||||
|
className="w-full max-w-md"
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ''}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
case DynamicFormItemType.TEXT:
|
case DynamicFormItemType.TEXT:
|
||||||
return (
|
return (
|
||||||
<Textarea
|
<Textarea
|
||||||
{...field}
|
{...field}
|
||||||
|
value={field.value ?? ''}
|
||||||
className="min-h-[120px] w-full max-w-full resize-y overflow-x-hidden break-all"
|
className="min-h-[120px] w-full max-w-full resize-y overflow-x-hidden break-all"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -306,13 +318,16 @@ export default function DynamicFormItemComponent({
|
|||||||
return (
|
return (
|
||||||
<Textarea
|
<Textarea
|
||||||
{...field}
|
{...field}
|
||||||
|
value={field.value ?? ''}
|
||||||
className="min-h-[200px] font-mono text-sm"
|
className="min-h-[200px] font-mono text-sm"
|
||||||
placeholder='{"key": "value"}'
|
placeholder='{"key": "value"}'
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
case DynamicFormItemType.BOOLEAN:
|
case DynamicFormItemType.BOOLEAN:
|
||||||
return <Switch checked={field.value} onCheckedChange={field.onChange} />;
|
return (
|
||||||
|
<Switch checked={!!field.value} onCheckedChange={field.onChange} />
|
||||||
|
);
|
||||||
|
|
||||||
case DynamicFormItemType.STRING_ARRAY:
|
case DynamicFormItemType.STRING_ARRAY:
|
||||||
return (
|
return (
|
||||||
@@ -1430,7 +1445,7 @@ export default function DynamicFormItemComponent({
|
|||||||
{/* 内容输入 */}
|
{/* 内容输入 */}
|
||||||
<Textarea
|
<Textarea
|
||||||
className="min-h-20 w-full min-w-0 flex-1 resize-y overflow-x-hidden break-all sm:w-[300px]"
|
className="min-h-20 w-full min-w-0 flex-1 resize-y overflow-x-hidden break-all sm:w-[300px]"
|
||||||
value={item.content}
|
value={item.content ?? ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newValue = [...(field.value ?? promptItems)];
|
const newValue = [...(field.value ?? promptItems)];
|
||||||
newValue[index] = {
|
newValue[index] = {
|
||||||
|
|||||||
@@ -310,6 +310,7 @@ function SingleSelectField({
|
|||||||
{options.map((opt) => (
|
{options.map((opt) => (
|
||||||
<div key={opt.id}>
|
<div key={opt.id}>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => onChange(opt.id)}
|
onClick={() => onChange(opt.id)}
|
||||||
className={`w-full text-left text-sm px-3 py-2 rounded-lg border transition-colors ${
|
className={`w-full text-left text-sm px-3 py-2 rounded-lg border transition-colors ${
|
||||||
value === opt.id
|
value === opt.id
|
||||||
@@ -361,8 +362,16 @@ function MultiSelectField({
|
|||||||
const selected = value.includes(opt.id);
|
const selected = value.includes(opt.id);
|
||||||
return (
|
return (
|
||||||
<div key={opt.id}>
|
<div key={opt.id}>
|
||||||
<button
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
onClick={() => toggle(opt.id)}
|
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 ${
|
className={`w-full text-left text-sm px-3 py-2 rounded-lg border transition-colors flex items-center gap-2 ${
|
||||||
selected
|
selected
|
||||||
? 'border-primary bg-primary/5 text-primary'
|
? 'border-primary bg-primary/5 text-primary'
|
||||||
@@ -371,7 +380,7 @@ function MultiSelectField({
|
|||||||
>
|
>
|
||||||
<Checkbox checked={selected} className="pointer-events-none" />
|
<Checkbox checked={selected} className="pointer-events-none" />
|
||||||
{getI18nText(opt.label)}
|
{getI18nText(opt.label)}
|
||||||
</button>
|
</div>
|
||||||
{opt.has_input && selected && (
|
{opt.has_input && selected && (
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
|||||||
@@ -464,14 +464,11 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
|
|||||||
onRuntimeInfoChange?.(runtimeInfo);
|
onRuntimeInfoChange?.(runtimeInfo);
|
||||||
}, [onRuntimeInfoChange, runtimeInfo]);
|
}, [onRuntimeInfoChange, runtimeInfo]);
|
||||||
|
|
||||||
useImperativeHandle(
|
// Expose test action and testing state to parent
|
||||||
ref,
|
useImperativeHandle(ref, () => ({
|
||||||
() => ({
|
testMcp: () => testMcp(),
|
||||||
testMcp: () => testMcp(),
|
isTesting: mcpTesting,
|
||||||
isTesting: mcpTesting,
|
}));
|
||||||
}),
|
|
||||||
[mcpTesting],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
isInitializing.current = true;
|
isInitializing.current = true;
|
||||||
|
|||||||
@@ -169,6 +169,8 @@ export default function PipelineFormComponent({
|
|||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
basic: {
|
basic: {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
emoji: '⚙️',
|
emoji: '⚙️',
|
||||||
},
|
},
|
||||||
ai: {},
|
ai: {},
|
||||||
@@ -221,8 +223,8 @@ export default function PipelineFormComponent({
|
|||||||
|
|
||||||
const loadedValues = {
|
const loadedValues = {
|
||||||
basic: {
|
basic: {
|
||||||
name: resp.pipeline.name,
|
name: resp.pipeline.name ?? '',
|
||||||
description: resp.pipeline.description,
|
description: resp.pipeline.description ?? '',
|
||||||
emoji: resp.pipeline.emoji || '⚙️',
|
emoji: resp.pipeline.emoji || '⚙️',
|
||||||
},
|
},
|
||||||
ai: resp.pipeline.config.ai,
|
ai: resp.pipeline.config.ai,
|
||||||
@@ -601,7 +603,7 @@ export default function PipelineFormComponent({
|
|||||||
<span className="text-destructive">*</span>
|
<span className="text-destructive">*</span>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field} value={field.value ?? ''} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -632,7 +634,7 @@ export default function PipelineFormComponent({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('common.description')}</FormLabel>
|
<FormLabel>{t('common.description')}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field} value={field.value ?? ''} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
Reference in New Issue
Block a user