🐛 fix(user-interaction): add humanIntervention to manifest and implement form UI

This commit is contained in:
Innei
2026-03-25 20:34:20 +08:00
parent d3755dcd43
commit 52e25d0f3a
3 changed files with 141 additions and 14 deletions

View File

@@ -11,5 +11,10 @@
"main": "./src/index.ts",
"devDependencies": {
"@lobechat/types": "workspace:*"
},
"peerDependencies": {
"@lobehub/ui": "^5",
"antd": "^6",
"react": "^19"
}
}

View File

@@ -1,33 +1,154 @@
'use client';
import type { BuiltinInterventionProps } from '@lobechat/types';
import { memo } from 'react';
import { Flexbox, Text } from '@lobehub/ui';
import { Button, Input, Select } from 'antd';
import { memo, useCallback, useState } from 'react';
import type { AskUserQuestionArgs } from '../../../types';
import type { AskUserQuestionArgs, InteractionField } from '../../../types';
const FieldInput = memo<{
field: InteractionField;
onChange: (key: string, value: string | string[]) => void;
value?: string | string[];
}>(({ field, value, onChange }) => {
switch (field.kind) {
case 'textarea': {
return (
<Input.TextArea
autoSize={{ maxRows: 6, minRows: 2 }}
placeholder={field.placeholder}
value={value as string}
onChange={(e) => onChange(field.key, e.target.value)}
/>
);
}
case 'select': {
return (
<Select
options={field.options?.map((o) => ({ label: o.label, value: o.value }))}
placeholder={field.placeholder}
style={{ width: '100%' }}
value={value as string}
onChange={(v) => onChange(field.key, v)}
/>
);
}
case 'multiselect': {
return (
<Select
mode="multiple"
options={field.options?.map((o) => ({ label: o.label, value: o.value }))}
placeholder={field.placeholder}
style={{ width: '100%' }}
value={value as string[]}
onChange={(v) => onChange(field.key, v)}
/>
);
}
default: {
return (
<Input
placeholder={field.placeholder}
value={value as string}
onChange={(e) => onChange(field.key, e.target.value)}
/>
);
}
}
});
const AskUserQuestionIntervention = memo<BuiltinInterventionProps<AskUserQuestionArgs>>(
({ args }) => {
({ args, interactionMode, onInteractionAction }) => {
const { question } = args;
const isCustom = interactionMode === 'custom';
const initialValues: Record<string, string | string[]> = {};
if (question.fields) {
for (const field of question.fields) {
if (field.value !== undefined) initialValues[field.key] = field.value;
}
}
const [formData, setFormData] = useState<Record<string, string | string[]>>(initialValues);
const [submitting, setSubmitting] = useState(false);
const handleFieldChange = useCallback((key: string, value: string | string[]) => {
setFormData((prev) => ({ ...prev, [key]: value }));
}, []);
const handleSubmit = useCallback(async () => {
if (!onInteractionAction) return;
setSubmitting(true);
try {
await onInteractionAction({ payload: formData, type: 'submit' });
} finally {
setSubmitting(false);
}
}, [formData, onInteractionAction]);
const handleSkip = useCallback(async () => {
if (!onInteractionAction) return;
await onInteractionAction({ type: 'skip' });
}, [onInteractionAction]);
const isSubmitDisabled = question.fields?.some((f) => f.required && !formData[f.key]) ?? false;
if (!isCustom) {
return (
<Flexbox gap={8}>
<Text>{question.prompt}</Text>
{question.fields && question.fields.length > 0 && (
<ul style={{ margin: 0, paddingLeft: 20 }}>
{question.fields.map((field) => (
<li key={field.key}>
{field.label}
{field.required && ' *'}
</li>
))}
</ul>
)}
</Flexbox>
);
}
return (
<div>
<p>{question.prompt}</p>
<Flexbox gap={12}>
<Text style={{ fontWeight: 500 }}>{question.prompt}</Text>
{question.description && (
<p style={{ color: 'var(--lobe-text-secondary)', fontSize: 13 }}>
<Text style={{ fontSize: 13 }} type="secondary">
{question.description}
</p>
</Text>
)}
{question.fields && question.fields.length > 0 && (
<ul style={{ margin: 0, paddingLeft: 20 }}>
<Flexbox gap={8}>
{question.fields.map((field) => (
<li key={field.key}>
{field.label}
{field.required && ' *'}
</li>
<Flexbox gap={4} key={field.key}>
<Text style={{ fontSize: 13 }}>
{field.label}
{field.required && <span style={{ color: 'red' }}> *</span>}
</Text>
<FieldInput
field={field}
value={formData[field.key]}
onChange={handleFieldChange}
/>
</Flexbox>
))}
</ul>
</Flexbox>
)}
</div>
<Flexbox horizontal gap={8} justify="flex-end">
<Button onClick={handleSkip}>Skip</Button>
<Button
disabled={isSubmitDisabled}
loading={submitting}
type="primary"
onClick={handleSubmit}
>
Submit
</Button>
</Flexbox>
</Flexbox>
);
},
);

View File

@@ -8,6 +8,7 @@ export const UserInteractionManifest: BuiltinToolManifest = {
{
description:
'Present a question to the user with either structured form fields or freeform input. Returns the interaction request in pending state.',
humanIntervention: 'required',
name: UserInteractionApiName.askUserQuestion,
parameters: {
properties: {