import { useEffect, useRef, useState } from 'react'; import { Alert, Button, Card, Col, Input, Row, Select, Space, Table, Tag, Typography } from 'antd'; import type { BlockComponentProps } from '@1flowbase/block-sdk'; const BASE = '/api/runtime/models/feedback_evidence'; const initial = { public_title: 'Demo-01: First project needs a clearer next step', public_summary: 'Add a guided checklist after creating the first project.', source_type: 'onboarding_observation', status: 'new', internal_source_url: '', internal_observed_step: 'SYNTHETIC: The participant created a project, opened Settings twice, and did not find how to add the first item.', internal_decision_reason: '', internal_hypothesis: 'Hypothesis, not observed fact: the next action may be hard to discover.', internal_moderator_prompt: 'After the participant paused, the moderator asked: What would you try next? No button was pointed out.' }; const transitions: Record = { new: ['planned', 'declined'], planned: ['in_progress', 'declined'], in_progress: ['done', 'declined'], done: [], declined: [] }; const colors: Record = { new: 'blue', planned: 'gold', in_progress: 'cyan', done: 'green', declined: 'red' }; const labels: Record = { public_title: 'Title', public_summary: 'Proposed improvement', internal_observed_step: 'Observed facts: attempt and point of friction', internal_hypothesis: 'Hypothesis, separate from evidence', internal_moderator_prompt: 'Moderator prompt or help', internal_source_url: 'Optional public evidence URL' }; export default function FeedbackDemo({ ctx }: BlockComponentProps) { const [rows, setRows] = useState([]); const [selected, setSelected] = useState(null); const [draft, setDraft] = useState(initial); const [reason, setReason] = useState(''); const [busy, setBusy] = useState(false); const [error, setError] = useState(''); const [notice, setNotice] = useState(''); const [total, setTotal] = useState(0); const request = useRef(0); const locked = useRef(false); async function load() { const ticket = ++request.current; const result: any = await ctx.api.get(BASE + '/list', { query: { page: 1, page_size: 100, sort: 'created_at:desc' } }); if (!Array.isArray(result.items)) throw new Error('The server did not return the expected items list.'); if (ticket === request.current) { setRows(result.items); setTotal(result.total ?? result.items.length); } return result.items; } useEffect(() => { load().catch(e => setError(String(e.message || e))); return () => { request.current++; }; }, []); async function action(work: () => Promise) { if (locked.current) return; locked.current = true; setBusy(true); setError(''); setNotice(''); try { await work(); } catch (e: any) { setError(String(e.message || e) + ' Refresh before retrying an uncertain write.'); } finally { locked.current = false; setBusy(false); } } async function create() { await action(async () => { for (const key of ['public_title', 'public_summary', 'internal_observed_step'] as const) if (!draft[key].trim()) throw new Error(labels[key] + ' is required.'); if (!['feedback', 'onboarding_observation'].includes(draft.source_type)) throw new Error('Choose a valid source type.'); const existing = await load(); const same = existing.find((r: any) => r.public_title === draft.public_title.trim()); if (same) { setSelected(same); setNotice('This synthetic title already exists. Selected the saved record; no duplicate was created.'); return; } const saved: any = await ctx.api.post(BASE + '/create', { body: { ...draft, public_title: draft.public_title.trim(), status: 'new', internal_decision_reason: '' } }); if (!saved.id) throw new Error('No server record ID was returned. Reconcile before retrying.'); const fresh = await load(); const verified = fresh.find((r: any) => r.id === saved.id); if (!verified) throw new Error('Write returned an ID, but the refreshed list did not contain it.'); setSelected(verified); setReason(''); setNotice('Created and reloaded server record ' + saved.id); }); } async function decide(target: string) { await action(async () => { if (!selected || !reason.trim()) throw new Error('Select an item and enter a human decision reason.'); const fresh = await load(); const current = fresh.find((r: any) => r.id === selected.id); if (!current || current.status !== selected.status) throw new Error('The record changed. Select its current version before deciding.'); if (!(transitions[current.status] || []).includes(target)) throw new Error('This status transition is not available.'); const history = [current.internal_decision_reason, '[' + target + '] ' + reason.trim()].filter(Boolean).join('\n'); await ctx.api.patch(BASE + '/update/{id}', { path: { id: current.id }, body: { status: target, internal_decision_reason: history } }); const updated = (await load()).find((r: any) => r.id === current.id); if (!updated || updated.status !== target) throw new Error('The refreshed server state did not match the requested status.'); setSelected(updated); setReason(''); setNotice('Saved and reloaded: ' + target); }); } const field = (key: keyof typeof labels, rows = 2) =>
{labels[key]} setDraft({ ...draft, [key]: e.target.value })} />
; return
Evidence โ†’ decision โ†’ roadmap {total} saved records{Object.keys(transitions).map(s => {s}: {rows.filter(r => r.status === s).length})} {error && } {notice && } {s} }, { title: 'Review', render: (_: any, r: any) => } ]} /> {field('public_title', 1)}{field('public_summary')} Source type