🐛 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", "main": "./src/index.ts",
"devDependencies": { "devDependencies": {
"@lobechat/types": "workspace:*" "@lobechat/types": "workspace:*"
},
"peerDependencies": {
"@lobehub/ui": "^5",
"antd": "^6",
"react": "^19"
} }
} }

View File

@@ -1,33 +1,154 @@
'use client'; 'use client';
import type { BuiltinInterventionProps } from '@lobechat/types'; 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>>( const AskUserQuestionIntervention = memo<BuiltinInterventionProps<AskUserQuestionArgs>>(
({ args }) => { ({ args, interactionMode, onInteractionAction }) => {
const { question } = args; 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 ( return (
<div> <Flexbox gap={12}>
<p>{question.prompt}</p> <Text style={{ fontWeight: 500 }}>{question.prompt}</Text>
{question.description && ( {question.description && (
<p style={{ color: 'var(--lobe-text-secondary)', fontSize: 13 }}> <Text style={{ fontSize: 13 }} type="secondary">
{question.description} {question.description}
</p> </Text>
)} )}
{question.fields && question.fields.length > 0 && ( {question.fields && question.fields.length > 0 && (
<ul style={{ margin: 0, paddingLeft: 20 }}> <Flexbox gap={8}>
{question.fields.map((field) => ( {question.fields.map((field) => (
<li key={field.key}> <Flexbox gap={4} key={field.key}>
{field.label} <Text style={{ fontSize: 13 }}>
{field.required && ' *'} {field.label}
</li> {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: description:
'Present a question to the user with either structured form fields or freeform input. Returns the interaction request in pending state.', '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, name: UserInteractionApiName.askUserQuestion,
parameters: { parameters: {
properties: { properties: {