UNPKG

@adonis-agora/agent

Version:

Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.

106 lines • 4.58 kB
import { useCallback, useRef, useState } from 'react'; import { requireBackendMethod } from '../backend.js'; import { useAgentBackend } from '../provider.js'; /** * Thumbs-up/down (+ comment) on assistant messages, over `POST <base>/messages/:id/feedback`. * Headless: it holds the state and the calls, the host draws the buttons. A rating shows at once * and rolls back if the server refuses it. */ export function useMessageFeedback(options = {}) { const backend = useAgentBackend(options.backend); const latest = useRef({ ...options, backend }); latest.current = { ...options, backend }; const [local, setLocal] = useState(new Map()); const [pending, setPending] = useState(new Set()); const [error, setError] = useState(null); const localRef = useRef(local); localRef.current = local; // Live message id → the persisted row it resolved to, so a message is looked up once. const resolved = useRef(new Map()); const feedbackOf = useCallback((message) => { const mine = local.get(message.id); if (mine !== undefined) return mine.feedback; return metadataOf(message).feedback ?? null; }, [local]); const isPending = useCallback((message) => pending.has(message.id), [pending]); const storedIdFor = useCallback(async (message) => { const runId = metadataOf(message).runId; if (runId === undefined) return message.id; const known = resolved.current.get(message.id); if (known !== undefined) return known; const { backend, threadId } = latest.current; const thread = typeof threadId === 'function' ? threadId() : threadId; if (thread === undefined) { throw new Error('useMessageFeedback: a streamed message needs `threadId` to be rated'); } const detail = await backend.getThread(thread); const row = [...detail.messages] .reverse() .find((candidate) => candidate.role === 'assistant' && candidate.runId === runId); if (row === undefined) { throw new Error(`useMessageFeedback: run ${runId} has no persisted answer yet`); } resolved.current.set(message.id, row.id); return row.id; }, []); const rate = useCallback(async (message, value, comment) => { const setFeedback = requireBackendMethod(latest.current.backend, 'setMessageFeedback'); const had = localRef.current.get(message.id); const optimistic = value === null ? null : { value, ...(comment !== undefined && comment.trim().length > 0 ? { comment: comment.trim() } : {}), updatedAt: new Date().toISOString(), }; setLocal((current) => new Map(current).set(message.id, { feedback: optimistic })); setPending((current) => new Set(current).add(message.id)); try { const storedId = await storedIdFor(message); const { feedback } = await setFeedback(storedId, { value, ...(comment !== undefined ? { comment } : {}), }); setLocal((current) => new Map(current).set(message.id, { feedback })); setError(null); } catch (caught) { setLocal((current) => { const next = new Map(current); if (had === undefined) next.delete(message.id); else next.set(message.id, had); return next; }); const failure = caught instanceof Error ? caught : new Error(String(caught)); setError(failure); throw failure; } finally { setPending((current) => { const next = new Set(current); next.delete(message.id); return next; }); } }, [storedIdFor]); const toggle = useCallback((message, value) => { const mine = localRef.current.get(message.id); const current = mine !== undefined ? mine.feedback : (metadataOf(message).feedback ?? null); return rate(message, current?.value === value ? null : value); }, [rate]); return { feedbackOf, isPending, rate, toggle, error }; } function metadataOf(message) { const metadata = message.metadata; return metadata !== null && typeof metadata === 'object' ? metadata : {}; } //# sourceMappingURL=use-message-feedback.js.map