mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-26 13:19:34 +07:00
🐛 fix(user-interaction): add humanIntervention to manifest and implement form UI
This commit is contained in:
@@ -11,5 +11,10 @@
|
||||
"main": "./src/index.ts",
|
||||
"devDependencies": {
|
||||
"@lobechat/types": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@lobehub/ui": "^5",
|
||||
"antd": "^6",
|
||||
"react": "^19"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user