@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
JavaScript
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