Add human-led conversation learning loop
This commit is contained in:
1 parent
bc42c55555
commit
687abab82b
52 files changed
+2112
-53
No files matched your search
@@ -18,6 +18,7 @@ export const navigationItems: readonly NavigationItem[] = [
|
||||
{ label: 'Media', path: '/media', description: 'Private media and sync controls', group: 'Content' },
|
||||
{ label: 'Exchange library', path: '/media-library', description: 'Images the bot may send when asked', group: 'Content' },
|
||||
{ label: 'Performance', path: '/performance', description: 'Response and strategy trends', group: 'Analyse' },
|
||||
{ label: 'Learning review', path: '/learning', description: 'Human-led examples and attribution', group: 'Analyse' },
|
||||
{ label: 'Operations', path: '/operations', description: 'Jobs, events, and diagnostics', group: 'Analyse' },
|
||||
{ label: 'Subreddits', path: '/subreddits', description: 'Where the bot posts and how it talks', group: 'Configure' },
|
||||
{ label: 'Accounts', path: '/accounts', description: 'Reddit sessions and access', group: 'Configure' },
|
||||
|
||||
@@ -13,6 +13,7 @@ import { MediaLibraryPage } from '../features/media-library/media-library-page.j
|
||||
import { SubredditsPage } from '../features/subreddits/subreddits-page.js';
|
||||
import { AccountsPage } from '../features/accounts/accounts-page.js';
|
||||
import { GrowthPage } from '../features/growth/growth-page.js';
|
||||
import { LearningPage } from '../features/learning/learning-page.js';
|
||||
|
||||
export function createDashboardRoutes(): RouteObject[] {
|
||||
return [
|
||||
@@ -29,6 +30,7 @@ export function createDashboardRoutes(): RouteObject[] {
|
||||
{ path: 'media', element: <MediaRoute /> },
|
||||
{ path: 'media-library', element: <MediaLibraryPage /> },
|
||||
{ path: 'performance', element: <PerformancePage /> },
|
||||
{ path: 'learning', element: <LearningPage /> },
|
||||
{ path: 'operations', element: <OperationsPage /> },
|
||||
{ path: 'subreddits', element: <SubredditsPage /> },
|
||||
{ path: 'accounts', element: <AccountsPage /> },
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { LearningOverviewResponseSchema, LearningReviewResponseSchema, type LearningOverviewResponse } from '../../../../../packages/api-contracts/src/index.js';
|
||||
import { createDashboardRequest } from '../../api/request.js';
|
||||
|
||||
async function parse<T>(response: Response, schema: { parse(value: unknown): T }): Promise<T> {
|
||||
const body: unknown = await response.json();
|
||||
if (!response.ok) throw new Error(typeof body === 'object' && body && 'error' in body ? String(body.error) : `Request failed (${response.status})`);
|
||||
return schema.parse(body);
|
||||
}
|
||||
|
||||
const request = createDashboardRequest();
|
||||
export const learningApi = {
|
||||
getOverview(): Promise<LearningOverviewResponse> {
|
||||
return request('/api/learning').then((response) => parse(response, LearningOverviewResponseSchema));
|
||||
},
|
||||
review(id: string, status: 'eligible' | 'excluded'): Promise<{ success: true }> {
|
||||
return request(`/api/learning/trajectories/${encodeURIComponent(id)}`, {
|
||||
method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status }),
|
||||
}).then((response) => parse(response, LearningReviewResponseSchema));
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { learningApi } from './learning-api.js';
|
||||
import './learning.css';
|
||||
|
||||
const humanCredits = new Set(['human-created-success', 'human-improved-success', 'human-completed-ai-trajectory']);
|
||||
|
||||
export function LearningPage() {
|
||||
const client = useQueryClient();
|
||||
const overview = useQuery({ queryKey: ['learning'], queryFn: learningApi.getOverview });
|
||||
const review = useMutation({
|
||||
mutationFn: ({ id, status }: { id: string; status: 'eligible' | 'excluded' }) => learningApi.review(id, status),
|
||||
onSuccess: () => client.invalidateQueries({ queryKey: ['learning'] }),
|
||||
});
|
||||
if (overview.isPending) return <main className="page-shell"><div className="panel-loading" role="status"><span className="spinner" />Loading learning evidence…</div></main>;
|
||||
if (overview.isError) return <main className="page-shell"><div className="error-panel" role="alert">{overview.error.message}</div></main>;
|
||||
const { summary, trajectories } = overview.data;
|
||||
const humanTotal = summary.humanCreated + summary.humanImproved + summary.humanCompletedAi;
|
||||
return (
|
||||
<main className="page-shell learning-page">
|
||||
<header className="page-heading"><div><span className="eyebrow">Human-in-the-loop evidence</span><h1>Learning review</h1><p>Audit who made the successful decision before examples can influence generation.</p></div><span className={`learning-mode ${overview.data.generationEnabled ? 'live' : ''}`}>{overview.data.generationEnabled ? (overview.data.shadowMode ? 'Shadow only' : 'Injected') : 'Capture only'}</span></header>
|
||||
<section className="metrics-grid learning-metrics" aria-label="Learning totals">
|
||||
<article className="metric-card"><span>Human-led outcomes</span><strong>{humanTotal}</strong><small>{summary.humanCreated} created · {summary.humanImproved} improved · {summary.humanCompletedAi} completed</small></article>
|
||||
<article className="metric-card"><span>Eligible examples</span><strong>{summary.eligible}</strong><small>{summary.provisional} still maturing</small></article>
|
||||
<article className="metric-card"><span>Needs review</span><strong>{summary.needsReview}</strong><small>{summary.inferred} inferred attributions</small></article>
|
||||
<article className="metric-card"><span>Revoked or excluded</span><strong>{summary.invalidated + summary.excluded}</strong><small>Never supplied to generation</small></article>
|
||||
</section>
|
||||
<section className="panel learning-panel" aria-labelledby="trajectory-heading">
|
||||
<div className="panel-heading"><div><span className="eyebrow">Policy {overview.data.policyVersion}</span><h2 id="trajectory-heading">Attributed trajectories</h2></div><span className="sample-size">n = {summary.total}</span></div>
|
||||
{trajectories.length === 0 ? <p className="feature-empty">No received-image trajectories have been extracted yet. A full chat sync will backfill authoritative histories.</p> : (
|
||||
<div className="table-scroll"><table className="performance-table learning-table"><thead><tr><th>Credit</th><th>Community</th><th>Confidence</th><th>Status</th><th>Updated</th><th>Review</th></tr></thead><tbody>{trajectories.map((item) => (
|
||||
<tr key={item.id} className={humanCredits.has(item.creditClass) ? 'human-led' : ''}>
|
||||
<th scope="row">{item.creditClass.replaceAll('-', ' ')}</th><td>{item.subreddit ? `r/${item.subreddit}` : 'Unknown'}</td><td>{item.attributionConfidence}</td><td><span className={`trajectory-status status-${item.status}`}>{item.status}</span></td><td>{new Date(item.updatedAt).toLocaleString()}</td><td><div className="review-actions"><button className="button secondary" disabled={review.isPending || item.status === 'invalidated'} onClick={() => review.mutate({ id: item.id, status: 'eligible' })}>Include</button><button className="button secondary" disabled={review.isPending || item.status === 'invalidated'} onClick={() => review.mutate({ id: item.id, status: 'excluded' })}>Exclude</button></div></td>
|
||||
</tr>
|
||||
))}</tbody></table></div>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
.learning-page { display: grid; gap: 1rem; }
|
||||
.learning-metrics { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
.learning-mode, .trajectory-status { border-radius: 999px; background: var(--surface-strong); color: var(--muted); font-size: .78rem; padding: .35rem .65rem; text-transform: capitalize; }
|
||||
.learning-mode.live, .status-eligible { color: var(--success); }
|
||||
.status-needs-review { color: var(--warning); }
|
||||
.status-invalidated, .status-excluded { color: var(--danger); }
|
||||
.learning-panel { padding: 1rem; }
|
||||
.learning-table tr.human-led th:first-child { color: var(--accent); }
|
||||
.review-actions { display: flex; gap: .4rem; }
|
||||
.review-actions .button { padding: .4rem .6rem; }
|
||||
@media (max-width: 900px) { .learning-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
@media (max-width: 600px) { .learning-metrics { grid-template-columns: 1fr; } }
|
||||
Reference in new issue
Block a user