mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +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",
|
"main": "./src/index.ts",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lobechat/types": "workspace:*"
|
"@lobechat/types": "workspace:*"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@lobehub/ui": "^5",
|
||||||
|
"antd": "^6",
|
||||||
|
"react": "^19"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user