Download apps/web/src/plugins/SchemaPluginForm.tsx from hunian003/message: direct link, hf CLI and curl.
- Browser
- Download file 6.25 kB
-
https://huggingface.co/spaces/hunian003/message/resolve/main/apps/web/src/plugins/SchemaPluginForm.tsx
- Command line
-
hf download hf://spaces/hunian003/message/apps/web/src/plugins/SchemaPluginForm.tsx
-
curl -L -o SchemaPluginForm.tsx https://huggingface.co/spaces/hunian003/message/resolve/main/apps/web/src/plugins/SchemaPluginForm.tsx
6.25 kB
| /** | |
| * Schema UI 表单组件 | |
| * | |
| * 按 ui_entry.schema.fields 渲染表单,并按 ui_entry.submit 契约提交。 | |
| * 只允许使用后端 ui_entry.submit 指定的提交契约,不能由前端猜 endpoint。 | |
| * 使用 console 语义 token 保持视觉一致。 | |
| */ | |
| import { useState } from 'react'; | |
| import { AlertCircle, Loader2, CheckCircle } from 'lucide-react'; | |
| import { UISchemaDefinition, UISubmitContract } from '@typings/plugin'; | |
| import { Button } from '@components/ui/button'; | |
| import { Input } from '@components/ui/input'; | |
| import { Label } from '@components/ui/label'; | |
| interface SchemaPluginFormProps { | |
| pluginName: string; | |
| schema: UISchemaDefinition; | |
| submit: UISubmitContract; | |
| } | |
| export function SchemaPluginForm({ schema, submit }: SchemaPluginFormProps) { | |
| const [formData, setFormData] = useState<Record<string, string | number>>({}); | |
| const [isSubmitting, setIsSubmitting] = useState(false); | |
| const [result, setResult] = useState<unknown>(null); | |
| const [error, setError] = useState<string | null>(null); | |
| const handleFieldChange = (name: string, value: string | number) => { | |
| setFormData((prev) => ({ ...prev, [name]: value })); | |
| }; | |
| const handleSubmit = async () => { | |
| setIsSubmitting(true); | |
| setError(null); | |
| setResult(null); | |
| try { | |
| // 只使用 ui_entry.submit 契约 | |
| if (submit.method !== 'POST') { | |
| throw new Error(`不支持的提交方法: ${submit.method}`); | |
| } | |
| const response = await fetch(submit.path, { | |
| method: submit.method, | |
| headers: { | |
| 'Content-Type': submit.content_type || 'application/json', | |
| }, | |
| body: JSON.stringify(formData), | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json().catch(() => null); | |
| const errorMessage = | |
| errorData?.message || errorData?.detail || `请求失败: ${response.status}`; | |
| throw new Error(errorMessage); | |
| } | |
| const data: unknown = await response.json(); | |
| // 按 success_path 读取结果 | |
| if (submit.success_path) { | |
| const successResult = submit.success_path.split('.').reduce<unknown>((obj, key) => (obj as Record<string, unknown> | null | undefined)?.[key], data); | |
| setResult(successResult); | |
| } else { | |
| setResult(data); | |
| } | |
| } catch (err) { | |
| setError(err instanceof Error ? err.message || '提交失败' : '提交失败'); | |
| } finally { | |
| setIsSubmitting(false); | |
| } | |
| }; | |
| return ( | |
| <div className="space-y-4"> | |
| {/* 表单标题 */} | |
| <div> | |
| <h3 className="text-sm font-medium text-console-text-primary">{schema.title}</h3> | |
| {schema.description && ( | |
| <p className="text-sm text-console-text-secondary mt-1">{schema.description}</p> | |
| )} | |
| </div> | |
| {/* 表单字段 */} | |
| <div className="space-y-4"> | |
| <h4 className="text-sm font-medium text-console-text-primary">参数</h4> | |
| {schema.fields.map((field) => ( | |
| <div key={field.name} className="space-y-1.5"> | |
| <Label htmlFor={field.name} className="text-sm text-console-text-primary"> | |
| {field.label} | |
| {field.required && <span className="text-console-status-error ml-1">*</span>} | |
| </Label> | |
| {field.type === 'string' && field.name.includes('content') ? ( | |
| <textarea | |
| id={field.name} | |
| placeholder={field.placeholder} | |
| value={formData[field.name] || ''} | |
| onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => handleFieldChange(field.name, e.target.value)} | |
| className="flex min-h-[80px] w-full rounded-console-md border border-console-border bg-console-surface-base px-3 py-2 text-sm text-console-text-primary placeholder:text-console-text-muted focus:outline-none focus:ring-2 focus:ring-console-status-info/30 focus:border-console-status-info disabled:cursor-not-allowed disabled:opacity-50" | |
| /> | |
| ) : ( | |
| <Input | |
| id={field.name} | |
| type={field.type === 'number' ? 'number' : 'text'} | |
| placeholder={field.placeholder} | |
| value={formData[field.name] || ''} | |
| onChange={(e) => | |
| handleFieldChange( | |
| field.name, | |
| field.type === 'number' ? Number(e.target.value) : e.target.value | |
| ) | |
| } | |
| /> | |
| )} | |
| {field.help_text && ( | |
| <p className="text-xs text-console-text-muted">{field.help_text}</p> | |
| )} | |
| </div> | |
| ))} | |
| <Button onClick={handleSubmit} disabled={isSubmitting} className="w-full"> | |
| {isSubmitting ? ( | |
| <> | |
| <Loader2 className="h-4 w-4 mr-2 animate-spin" /> | |
| 提交中... | |
| </> | |
| ) : ( | |
| '提交' | |
| )} | |
| </Button> | |
| </div> | |
| {/* 错误信息 */} | |
| {error && ( | |
| <div className="p-3 rounded-console-md border border-console-status-error bg-console-status-error-bg"> | |
| <div className="flex items-start gap-2"> | |
| <AlertCircle className="h-4 w-4 text-console-status-error mt-0.5 shrink-0" /> | |
| <div> | |
| <p className="text-sm font-medium text-console-status-error">提交失败</p> | |
| <p className="text-sm text-console-text-secondary mt-1">{error}</p> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* 结果 */} | |
| {result !== null && ( | |
| <div className="p-3 rounded-console-md border border-console-status-enabled bg-console-status-enabled-bg"> | |
| <div className="flex items-start gap-2"> | |
| <CheckCircle className="h-4 w-4 text-console-status-enabled mt-0.5 shrink-0" /> | |
| <div className="min-w-0"> | |
| <p className="text-sm font-medium text-console-status-enabled">提交成功</p> | |
| <pre className="text-sm bg-console-surface-base text-console-text-primary p-3 rounded-console-sm mt-2 overflow-x-auto"> | |
| {typeof result === 'string' ? result : JSON.stringify(result, null, 2)} | |
| </pre> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |