This commit is contained in:
1 parent
1c61c7ad04
commit
7b2c43ddaf
18 files changed
+1648
-2
No files matched your search
@@ -196,6 +196,40 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/v1/ingest-reviews": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** List Reviews */
|
||||
get: operations["ingest_review_list"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/v1/ingest-reviews/{review_id}/decision": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Decide Review */
|
||||
post: operations["ingest_review_decide"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/v1/me": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -894,6 +928,91 @@ export interface components {
|
||||
/** Detail */
|
||||
detail?: components["schemas"]["ValidationError"][];
|
||||
};
|
||||
/** IngestReviewDecision */
|
||||
IngestReviewDecision: {
|
||||
/**
|
||||
* Decision
|
||||
* @enum {string}
|
||||
*/
|
||||
decision: "include" | "exclude" | "defer";
|
||||
/** Decision Id */
|
||||
decision_id: string;
|
||||
/** Guidance Rule */
|
||||
guidance_rule?: string | null;
|
||||
/** Rationale */
|
||||
rationale: string;
|
||||
/** Review Id */
|
||||
review_id: string;
|
||||
/**
|
||||
* Save As Guidance
|
||||
* @default false
|
||||
*/
|
||||
save_as_guidance: boolean;
|
||||
/**
|
||||
* Schema Version
|
||||
* @default 1
|
||||
* @constant
|
||||
*/
|
||||
schema_version: 1;
|
||||
};
|
||||
/** IngestReviewItem */
|
||||
IngestReviewItem: {
|
||||
/**
|
||||
* Created At
|
||||
* Format: date-time
|
||||
*/
|
||||
created_at: string;
|
||||
/** Excerpt */
|
||||
excerpt: string;
|
||||
/** Extraction Id */
|
||||
extraction_id: string;
|
||||
/** Jev Lens */
|
||||
jev_lens?: ("technical" | "conceptual" | "cultural-historical" | "financial-market") | null;
|
||||
/** Jev Lens Confidence */
|
||||
jev_lens_confidence?: number | null;
|
||||
/** Jev Model */
|
||||
jev_model?: string | null;
|
||||
/** Jev Relation Probability */
|
||||
jev_relation_probability?: number | null;
|
||||
/**
|
||||
* Lens
|
||||
* @enum {string}
|
||||
*/
|
||||
lens: "technical" | "conceptual" | "cultural-historical" | "financial-market";
|
||||
locator: components["schemas"]["Locator"];
|
||||
/** Project Id */
|
||||
project_id: string;
|
||||
/**
|
||||
* Relevance
|
||||
* @default borderline
|
||||
* @enum {string}
|
||||
*/
|
||||
relevance: "borderline" | "low";
|
||||
/** Relevance Rationale */
|
||||
relevance_rationale: string;
|
||||
/** Review Id */
|
||||
review_id: string;
|
||||
/** Run Id */
|
||||
run_id: string;
|
||||
/**
|
||||
* Schema Version
|
||||
* @default 1
|
||||
* @constant
|
||||
*/
|
||||
schema_version: 1;
|
||||
/** Source Revision Id */
|
||||
source_revision_id: string;
|
||||
/**
|
||||
* Status
|
||||
* @default needs-user-review
|
||||
* @enum {string}
|
||||
*/
|
||||
status: "needs-user-review" | "included" | "excluded" | "deferred";
|
||||
/** Submitted By */
|
||||
submitted_by: string;
|
||||
/** Summary */
|
||||
summary: string;
|
||||
};
|
||||
/** IssuedCredential */
|
||||
IssuedCredential: {
|
||||
/** Credential Id */
|
||||
@@ -1363,6 +1482,16 @@ export interface components {
|
||||
owners: string[];
|
||||
/** Project Id */
|
||||
project_id: string;
|
||||
/**
|
||||
* Research Lenses
|
||||
* @default [
|
||||
* "technical",
|
||||
* "conceptual",
|
||||
* "cultural-historical",
|
||||
* "financial-market"
|
||||
* ]
|
||||
*/
|
||||
research_lenses: ("technical" | "conceptual" | "cultural-historical" | "financial-market")[];
|
||||
/**
|
||||
* Schema Version
|
||||
* @default 1
|
||||
@@ -3913,6 +4042,87 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
ingest_review_list: {
|
||||
parameters: {
|
||||
query: {
|
||||
project_id: string;
|
||||
status?: string | null;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
};
|
||||
header?: {
|
||||
authorization?: string | null;
|
||||
"X-GEWU-CSRF"?: string | null;
|
||||
};
|
||||
path?: never;
|
||||
cookie?: {
|
||||
gewu_session?: string | null;
|
||||
};
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["IngestReviewItem"][];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
ingest_review_decide: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: {
|
||||
authorization?: string | null;
|
||||
"X-GEWU-CSRF"?: string | null;
|
||||
};
|
||||
path: {
|
||||
review_id: string;
|
||||
};
|
||||
cookie?: {
|
||||
gewu_session?: string | null;
|
||||
};
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["IngestReviewDecision"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
account_current: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
||||
+68
-1
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import CredentialsSection from './CredentialsSection';
|
||||
import AccountLogin from './AccountLogin';
|
||||
import ProjectsSection from './ProjectsSection';
|
||||
@@ -8,6 +8,7 @@ import type { components } from '../generated/api';
|
||||
|
||||
const adminTabs = [
|
||||
{ id: 'projects', label: 'Projects' },
|
||||
{ id: 'ingest-review', label: 'Ingest review' },
|
||||
{ id: 'users', label: 'Users' },
|
||||
{ id: 'credentials', label: 'Credentials' },
|
||||
{ id: 'logs', label: 'Logs' },
|
||||
@@ -18,6 +19,69 @@ type AdminTab = typeof adminTabs[number]['id'];
|
||||
type Credential = components['schemas']['CredentialSummary'];
|
||||
type Activity = components['schemas']['AdminEvent'];
|
||||
type Project = components['schemas']['ProjectRecord'];
|
||||
type IngestReview = {
|
||||
review_id: string; project_id: string; run_id: string; source_revision_id: string;
|
||||
extraction_id: string; locator: { kind: string; block_id: string; start: number; end: number; page?: number | null };
|
||||
excerpt: string; lens: string; summary: string; relevance_rationale: string;
|
||||
jev_relation_probability?: number | null; jev_lens?: string | null;
|
||||
jev_lens_confidence?: number | null; jev_model?: string | null;
|
||||
relevance: 'borderline' | 'low'; status: string; created_at: string;
|
||||
};
|
||||
|
||||
function IngestReviewSection({ csrf, projects, onError, onMessage }: {
|
||||
csrf: string; projects: Project[]; onError: (message: string) => void; onMessage: (message: string) => void;
|
||||
}) {
|
||||
const [projectId, setProjectId] = useState('');
|
||||
const [items, setItems] = useState<IngestReview[]>([]);
|
||||
const [guidance, setGuidance] = useState<Record<string, string>>({});
|
||||
const [busyId, setBusyId] = useState('');
|
||||
const [rationales, setRationales] = useState<Record<string, string>>({});
|
||||
const [saveGuidance, setSaveGuidance] = useState<Record<string, boolean>>({});
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
if (!projectId) { setItems([]); return; }
|
||||
try {
|
||||
const rows = await request<IngestReview[]>(`/v1/ingest-reviews?project_id=${encodeURIComponent(projectId)}&status=needs-user-review&limit=100`, csrf);
|
||||
setItems(rows);
|
||||
} catch (failure) { onError(failure instanceof Error ? failure.message : 'Could not load review queue.'); }
|
||||
}, [projectId, csrf, onError]);
|
||||
useEffect(() => { void refresh(); }, [refresh]);
|
||||
|
||||
async function decide(item: IngestReview, decision: 'include' | 'exclude' | 'defer') {
|
||||
const rationale = rationales[item.review_id]?.trim();
|
||||
if (!rationale) { onError('Add a short reason for this decision.'); return; }
|
||||
const wantsGuidance = saveGuidance[item.review_id] ?? false;
|
||||
const guidanceRule = guidance[item.review_id]?.trim();
|
||||
if (wantsGuidance && !guidanceRule) { onError('Write the reusable guidance rule to save it.'); return; }
|
||||
setBusyId(item.review_id); onError('');
|
||||
try {
|
||||
await request(`/v1/ingest-reviews/${encodeURIComponent(item.review_id)}/decision`, csrf, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ decision_id: crypto.randomUUID(), review_id: item.review_id, decision, rationale,
|
||||
save_as_guidance: wantsGuidance, ...(wantsGuidance ? { guidance_rule: guidanceRule } : {}) }),
|
||||
});
|
||||
onMessage(decision === 'include' ? 'Relevance confirmed. Feedback saved; knowledge changes still use the normal proposal flow.' : decision === 'exclude' ? 'Marked not relevant. Feedback saved.' : 'Feedback deferred.');
|
||||
await refresh();
|
||||
} catch (failure) { onError(failure instanceof Error ? failure.message : 'Could not save review decision.'); }
|
||||
finally { setBusyId(''); }
|
||||
}
|
||||
|
||||
return <section className="admin-card" aria-labelledby="ingest-review-title">
|
||||
<div className="admin-heading"><div><h2 id="ingest-review-title">Ingest review</h2><p className="muted-copy">These flags are recorded alongside normal ingestion and do not hold up proposals or publication. Your decision helps tune future project triage.</p><p className="muted-copy">If configured, Jev sends the project context and excerpt to OpenRouter for a relevance probability and lens suggestion. That signal is advisory.</p></div>
|
||||
<button className="quiet-button" disabled={busyId !== '' || !projectId} onClick={() => void refresh()}>Refresh</button></div>
|
||||
<label>Project<select value={projectId} onChange={event => setProjectId(event.target.value)}><option value="">Choose a project</option>{projects.filter(project => project.status !== 'archived').map(project => <option key={project.project_id} value={project.project_id}>{project.title}</option>)}</select></label>
|
||||
{!projectId ? <p>Select a project to view its review flags.</p> : items.length === 0 ? <p>No review flags need feedback.</p> : <ol className="admin-activity">{items.map(item => <li key={item.review_id}>
|
||||
<strong>{item.lens.replaceAll('-', ' ')} · {item.relevance} relevance · {item.summary}</strong>
|
||||
<p>{item.excerpt}</p><p className="muted-copy">{item.relevance_rationale}</p>
|
||||
{item.jev_relation_probability != null ? <p className="muted-copy">Jev relevance: {Math.round(item.jev_relation_probability * 100)}% · lens: {item.jev_lens ?? 'unknown'} ({item.jev_lens_confidence != null ? `${Math.round(item.jev_lens_confidence * 100)}% confidence` : 'no confidence score'}) · {item.jev_model}</p> : <p className="muted-copy">Jev signal unavailable or not configured.</p>}
|
||||
<p className="muted-copy">Source {item.source_revision_id} · {item.locator.kind.replaceAll('_', ' ')} {item.locator.page ? `· page ${item.locator.page}` : ''} · run {item.run_id}</p>
|
||||
<label>Decision reason<textarea maxLength={2000} value={rationales[item.review_id] ?? ''} onChange={event => setRationales(value => ({ ...value, [item.review_id]: event.target.value }))} /></label>
|
||||
<label><input type="checkbox" checked={saveGuidance[item.review_id] ?? false} onChange={event => setSaveGuidance(value => ({ ...value, [item.review_id]: event.target.checked }))} /> Save a reusable rule for this project and lens</label>
|
||||
{saveGuidance[item.review_id] && <label>Project guidance<textarea maxLength={1000} value={guidance[item.review_id] ?? ''} onChange={event => setGuidance(value => ({ ...value, [item.review_id]: event.target.value }))} placeholder="For example: include historical show production and audience context when it informs dome practice." /></label>}
|
||||
<div className="admin-actions"><button disabled={busyId !== ''} onClick={() => void decide(item, 'include')}>Relevant</button><button className="quiet-button" disabled={busyId !== ''} onClick={() => void decide(item, 'defer')}>Defer feedback</button><button className="quiet-button" disabled={busyId !== ''} onClick={() => void decide(item, 'exclude')}>Not relevant</button></div>
|
||||
</li>)}</ol>}
|
||||
</section>;
|
||||
}
|
||||
|
||||
function Admin({ personal = false }: { personal?: boolean }) {
|
||||
const [activeTab, setActiveTab] = useState<AdminTab>('projects');
|
||||
@@ -132,6 +196,9 @@ function Admin({ personal = false }: { personal?: boolean }) {
|
||||
{!personal && <div id="admin-panel-projects" role="tabpanel" aria-labelledby="admin-tab-projects" hidden={activeTab !== 'projects'} tabIndex={0}>
|
||||
<ProjectsSection csrf={csrf} projects={projectRecords} currentOwner={currentPrincipal} onCreated={refreshAdminData} onError={setError} onMessage={setMessage} />
|
||||
</div>}
|
||||
{!personal && <div id="admin-panel-ingest-review" role="tabpanel" aria-labelledby="admin-tab-ingest-review" hidden={activeTab !== 'ingest-review'} tabIndex={0}>
|
||||
<IngestReviewSection csrf={csrf} projects={projectRecords} onError={setError} onMessage={setMessage} />
|
||||
</div>}
|
||||
{!personal && <div id="admin-panel-users" role="tabpanel" aria-labelledby="admin-tab-users" hidden={activeTab !== 'users'} tabIndex={0}>
|
||||
<AccountsSection csrf={csrf} projects={userProjectOptions} onMessage={setMessage} onError={setError} />
|
||||
</div>}
|
||||
|
||||
Reference in new issue
Block a user