From a84e5dcb545a29b8856c2dd78959a39a67cd6bd0 Mon Sep 17 00:00:00 2001 From: kongkx Date: Wed, 8 Jul 2026 22:12:34 +0800 Subject: [PATCH] feat(studio/robot-authors): manual edit form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an "编辑" action opening a sheet form to edit a robot author's status, persona, positioning, target readers, and the article/image/publishing style objects (JSON-validated as objects). Saves via the existing admin PUT endpoint, then refetches the enriched list. --- .../studio/robot-author-edit-sheet.tsx | 218 ++++++++++++++++++ src/routes/_authed/studio/robot-authors.tsx | 7 + 2 files changed, 225 insertions(+) create mode 100644 src/features/studio/robot-author-edit-sheet.tsx diff --git a/src/features/studio/robot-author-edit-sheet.tsx b/src/features/studio/robot-author-edit-sheet.tsx new file mode 100644 index 0000000..574473f --- /dev/null +++ b/src/features/studio/robot-author-edit-sheet.tsx @@ -0,0 +1,218 @@ +import { useEffect } from 'react' +import { useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { z } from 'zod' +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { toast } from 'sonner' +import type { StudioRobotAuthor } from '@/api/types' +import { updateRobotAuthor } from '@/api/modules/studio' +import { handleApiError } from '@/lib/errors' +import { Button } from '@/components/ui/button' +import { Label } from '@/components/ui/label' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { + Sheet, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, +} from '@/components/ui/sheet' +import { StatusPill } from '@/components/status-pill' +import { Textarea } from '@/components/ui/textarea' + +const STATUSES = ['pending', 'profiling', 'ready', 'paused'] as const + +/** A JSON-object text field: empty is allowed (→ null), otherwise must parse to an object. */ +const jsonObject = z.string().refine( + (value) => { + const trimmed = value.trim() + if (!trimmed) return true + try { + const parsed = JSON.parse(trimmed) + return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed) + } catch { + return false + } + }, + { message: 'JSON 格式错误(需为对象 {…})' }, +) + +const schema = z.object({ + persona: z.string(), + positioning: z.string(), + target_readers: z.string(), + status: z.enum(STATUSES), + article_style: jsonObject, + image_style: jsonObject, + publishing_rules: jsonObject, +}) + +type FormValues = z.infer + +const asText = (value: unknown) => + value == null ? '' : JSON.stringify(value, null, 2) + +const parseObject = (value: string) => { + const trimmed = value.trim() + return trimmed ? (JSON.parse(trimmed) as Record) : null +} + +interface RobotAuthorEditSheetProps { + author: StudioRobotAuthor | null + onOpenChange: (open: boolean) => void +} + +export function RobotAuthorEditSheet({ author, onOpenChange }: RobotAuthorEditSheetProps) { + const queryClient = useQueryClient() + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { + persona: '', + positioning: '', + target_readers: '', + status: 'pending', + article_style: '', + image_style: '', + publishing_rules: '', + }, + }) + + useEffect(() => { + if (!author) return + form.reset({ + persona: author.persona ?? '', + positioning: author.positioning ?? '', + target_readers: author.target_readers ?? '', + status: author.status, + article_style: asText(author.article_style), + image_style: asText(author.image_style), + publishing_rules: asText(author.publishing_rules), + }) + }, [author, form]) + + const mutation = useMutation({ + mutationFn: (values: FormValues) => { + if (!author) throw new Error('no author') + return updateRobotAuthor(author.id, { + persona: values.persona.trim() || null, + positioning: values.positioning.trim() || null, + target_readers: values.target_readers.trim() || null, + status: values.status, + article_style: parseObject(values.article_style), + image_style: parseObject(values.image_style), + publishing_rules: parseObject(values.publishing_rules), + }) + }, + onSuccess: () => { + toast.success('已保存') + void queryClient.invalidateQueries({ queryKey: ['studio-robot-authors'] }) + onOpenChange(false) + }, + onError: (error) => handleApiError(error, form.setError), + }) + + return ( + + + + 编辑机器人作者 + + {author?.display_name || author?.user_uid} + + + +
mutation.mutate(values))} + className="flex flex-col gap-4 px-4 pb-4" + > + + + + + +