L9 Lotse – KI-Assistent: Transkription, Berichtsentwurf, Vollständigkeitsprüfung, Freigabeprinzip (Services)
- OpenAI-kompatible Transkription + Processor transcription (done/failed/disabled, AiGeneration, Notiz aus Sprachnotiz) - Claude-Lotse (strukturierte Ausgabe, Refusal/Fallback), Datenminimierung, Vorschläge in content.lotse - Vollständigkeitsprüfung (Regeln + KI-Hinweise mit Deep-Link), Einstellungen, KI-Protokoll - Freigabeprinzip: Submit eines Lotse-Entwurfs nur mit Prüfbestätigung (serverseitig) - Migration lotse_address_form (TenantSettings) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
/** Result of Lotse server actions (client-safe; "use server" files may only export async functions). */
|
||||
export const LOTSE_ERROR_CODES = [
|
||||
"generic",
|
||||
"not_found",
|
||||
"forbidden",
|
||||
"disabled",
|
||||
"not_configured",
|
||||
"provider_failed",
|
||||
"not_editable",
|
||||
"no_suggestion",
|
||||
"no_transcript",
|
||||
"pending",
|
||||
"conflict",
|
||||
"invalid",
|
||||
] as const;
|
||||
export type LotseActionErrorCode = (typeof LOTSE_ERROR_CODES)[number];
|
||||
|
||||
export type LotseActionState =
|
||||
| { status: "idle" }
|
||||
| { status: "ok"; at: number; count?: number; summary?: string; generationId?: string }
|
||||
| { status: "error"; code: LotseActionErrorCode; at: number };
|
||||
|
||||
export const LOTSE_IDLE: LotseActionState = { status: "idle" };
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Result items of the Lotse completeness check (client-safe, lane L9). Labels come from messages
|
||||
* `lotse.completeness.item.<code>`; `text` is the model's plain-language hint for `lotse_hint`.
|
||||
*/
|
||||
export const COMPLETENESS_CODES = [
|
||||
"photo_requirement",
|
||||
"checklist_item",
|
||||
"material_reason",
|
||||
"material_unconfirmed",
|
||||
"no_work_time",
|
||||
"no_description",
|
||||
"signature_missing",
|
||||
"lotse_hint",
|
||||
] as const;
|
||||
export type CompletenessCode = (typeof COMPLETENESS_CODES)[number];
|
||||
|
||||
export type CompletenessItem = {
|
||||
code: CompletenessCode;
|
||||
/** checklist/photo requirement/material label */
|
||||
label?: string;
|
||||
/** Lotse hint text (missingInformation) */
|
||||
text?: string;
|
||||
/** deep link into the mobile sub page */
|
||||
href: string;
|
||||
source: "rule" | "lotse";
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* Lotse block of the report snapshot (client-safe, lane L9). Stored as optional `content.lotse` of
|
||||
* `ReportContent` (src/lib/reports/content.ts), so it is frozen together with the report on approval.
|
||||
*
|
||||
* Suggestions are kept SEPARATE from `content.texts`: nothing the model wrote reaches the report text
|
||||
* until a person accepts it (Spec §15.3/§15.4, Brandbook §12.4). `reviewedAt` is set on submit when
|
||||
* the submitter confirmed "Ich habe den Vorschlag geprüft" (checked server-side in submitReport).
|
||||
*/
|
||||
|
||||
/** Report text fields the Lotse drafts (ReportDraftOutput); `problems` stays technician-only. */
|
||||
export const LOTSE_TEXT_FIELDS = ["workPerformed", "deviations", "additionalWork", "openItems", "nextSteps", "hints"] as const;
|
||||
export type LotseTextField = (typeof LOTSE_TEXT_FIELDS)[number];
|
||||
|
||||
export const LOTSE_SUGGESTION_STATES = ["pending", "accepted", "discarded"] as const;
|
||||
export type LotseSuggestionState = (typeof LOTSE_SUGGESTION_STATES)[number];
|
||||
|
||||
export const LOTSE_TEXT_MAX = 10_000;
|
||||
export const LOTSE_MISSING_MAX = 20;
|
||||
|
||||
const isoDateTime = z.string().datetime({ offset: true });
|
||||
|
||||
export const lotseSuggestionSchema = z.object({
|
||||
field: z.enum(LOTSE_TEXT_FIELDS),
|
||||
text: z.string().max(LOTSE_TEXT_MAX),
|
||||
state: z.enum(LOTSE_SUGGESTION_STATES),
|
||||
decidedAt: isoDateTime.nullable(),
|
||||
});
|
||||
export type LotseSuggestion = z.infer<typeof lotseSuggestionSchema>;
|
||||
|
||||
export const lotseBlockSchema = z.object({
|
||||
generationId: z.string(),
|
||||
model: z.string(),
|
||||
draftedAt: isoDateTime,
|
||||
draftedById: z.string().nullable(),
|
||||
suggestions: z.array(lotseSuggestionSchema),
|
||||
/** plain-language hints of what the model could not find in the data */
|
||||
missingInformation: z.array(z.string().max(500)).max(LOTSE_MISSING_MAX),
|
||||
reviewedAt: isoDateTime.nullable(),
|
||||
reviewedById: z.string().nullable(),
|
||||
});
|
||||
export type LotseBlock = z.infer<typeof lotseBlockSchema>;
|
||||
|
||||
export const LOTSE_ADDRESS_FORMS = ["sie", "du"] as const;
|
||||
export type LotseAddressForm = (typeof LOTSE_ADDRESS_FORMS)[number];
|
||||
|
||||
export function pendingSuggestions(block: LotseBlock | null | undefined): LotseSuggestion[] {
|
||||
return block ? block.suggestions.filter((s) => s.state === "pending") : [];
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { lotseBlockSchema } from "@/lib/lotse/content";
|
||||
|
||||
/**
|
||||
* Report content snapshot (ARCHITEKTUR §4.7, Spec §16.2/§17.2). Client-safe.
|
||||
@@ -170,6 +171,8 @@ export const reportContentSchema = z.object({
|
||||
checklist: z.array(z.object({ label: z.string(), required: z.boolean(), checked: z.boolean(), comment: nullableText })),
|
||||
signature: signatureBlockSchema.nullable(),
|
||||
technician: z.object({ userId: z.string(), name: z.string() }).nullable(),
|
||||
/** Lotse suggestions + review proof (lane L9, optional — older snapshots have none) */
|
||||
lotse: lotseBlockSchema.optional(),
|
||||
});
|
||||
|
||||
export type ReportContent = z.infer<typeof reportContentSchema>;
|
||||
|
||||
Reference in New Issue
Block a user