feat: Phase 4.4 — Human-in-the-Loop APIs (AIIA-27)

- POST /api/analyses/:id/feedback — correction/approval/rejection workflow
- GET /api/headnotes — Leitsatz-Vorschlags-Workflow (pending headnote review)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
CTO (LegalAI)
2026-04-09 00:24:34 +00:00
parent 78ccf64948
commit b837f4a71e
3 changed files with 109 additions and 1 deletions

View File

@@ -0,0 +1,63 @@
import { NextRequest } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { db } from '@/lib/db';
import { analyses } from '@/lib/db/schema';
import { eq, and } from 'drizzle-orm';
/**
* POST /api/analyses/:id/feedback — submit correction/feedback for an AI analysis.
* Part of the Human-in-the-Loop Korrektur-Workflow.
*/
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
const session = await getServerSession(authOptions);
if (!session?.user) {
return Response.json({ error: 'Nicht authentifiziert' }, { status: 401 });
}
const { id } = await params;
const body = await request.json();
const { feedbackType, comment, correctedText } = body as {
feedbackType: 'correction' | 'approval' | 'rejection';
comment?: string;
correctedText?: string;
};
if (!feedbackType) {
return Response.json({ error: 'feedbackType erforderlich' }, { status: 400 });
}
const existing = await db
.select({ id: analyses.id, metadata: analyses.metadata })
.from(analyses)
.where(and(eq(analyses.id, id), eq(analyses.tenantId, session.user.tenantId)))
.limit(1);
if (existing.length === 0) {
return Response.json({ error: 'Analyse nicht gefunden' }, { status: 404 });
}
const currentMetadata = (existing[0].metadata ?? {}) as Record<string, unknown>;
const feedback = ((currentMetadata.feedback ?? []) as unknown[]);
feedback.push({
type: feedbackType,
comment,
correctedText,
userId: session.user.id,
userName: session.user.name,
timestamp: new Date().toISOString(),
});
await db
.update(analyses)
.set({
metadata: { ...currentMetadata, feedback },
updatedAt: new Date(),
})
.where(eq(analyses.id, id));
return Response.json({ success: true, feedbackCount: feedback.length });
}

View File

@@ -0,0 +1,45 @@
import { NextRequest } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { hasPermission } from '@/lib/auth/rbac';
import { db } from '@/lib/db';
import { decisions } from '@/lib/db/schema';
import { eq, and, isNull, isNotNull, desc } from 'drizzle-orm';
/**
* GET /api/headnotes — list decisions with proposed headnotes pending review.
* Used for the Human-in-the-Loop Leitsatz-Vorschlags-Workflow.
*/
export async function GET(request: NextRequest) {
const session = await getServerSession(authOptions);
if (!session?.user) {
return Response.json({ error: 'Nicht authentifiziert' }, { status: 401 });
}
if (!hasPermission(session.user.role, 'decisions:write')) {
return Response.json({ error: 'Keine Berechtigung' }, { status: 403 });
}
const { searchParams } = new URL(request.url);
const status = searchParams.get('status') ?? 'pending';
const results = await db
.select({
id: decisions.id,
court: decisions.court,
caseReference: decisions.caseReference,
decisionDate: decisions.decisionDate,
headnote: decisions.headnote,
metadata: decisions.metadata,
})
.from(decisions)
.where(
status === 'pending'
? isNull(decisions.headnote)
: isNotNull(decisions.headnote)
)
.orderBy(desc(decisions.decisionDate))
.limit(50);
return Response.json(results);
}

View File

@@ -11,7 +11,7 @@ const NAV_ITEMS = [
{ href: '/vertraege', label: 'Verträge', icon: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z' },
{ href: '/verfahren', label: 'Verfahren', icon: 'M3 6l3 1m0 0l-3 9a5.002 5.002 0 006.001 0M6 7l3 9M6 7l6-2m6 2l3-1m-3 1l-3 9a5.002 5.002 0 006.001 0M18 7l3 9m-3-9l-6-2m0-2v2m0 16V5m0 16H9m3 0h3' },
{ href: '/einstellungen', label: 'Einstellungen', icon: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 016 0z' },
];
] as const;
export default function Sidebar() {
const pathname = usePathname();