generated from knso/webklar-preview-template
- Backend-Proxy (Cloudflare Worker in proxy/): API-Key serverseitig, Modell-Whitelist, max_tokens-Deckel, Tageslimits pro Gerät und IP - Modell-Split: Analyse auf Sonnet 5, Übersetzung/Antwort auf Haiku 4.5 - Bild-Downscaling auf 2000px vor dem Upload (ScanScreen) - Expo SDK 56 -> 57 Upgrade - Freemium: 3 Gratis-Analysen mit lokalem Zähler und Paywall-Hinweis - Einstellungen: API-Key-Feld nur noch im Dev-Modus (NUTZT_PROXY) - Build-Setup: Bundle-IDs, eas.json, Splash-Screen-Plugin - Tests: jest-expo + 17 Tests für die Ampel-/Fristlogik - Rechtliches: Datenschutzerklärung (Entwurf), Webseite (webseite/, live auf behoerdenklar.pages.dev), Store-Texte (store/) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
179 lines
6.0 KiB
TypeScript
179 lines
6.0 KiB
TypeScript
/**
|
|
* Brief-Analyse: Bild oder PDF -> strukturiertes JSON (BriefAnalyse).
|
|
* Ein einziger Claude-Vision-Aufruf extrahiert alles: Erklärung in einfacher
|
|
* Sprache, Fachbegriffe, Fristen, Termine, Checkliste und Antwort-Optionen.
|
|
*/
|
|
import { BriefAnalyse } from '../types';
|
|
import { claudeJsonAufruf, ContentBlock, MODELL_ANALYSE } from './claudeClient';
|
|
|
|
/**
|
|
* JSON-Schema für die Analyse. Die Claude API erzwingt dieses Schema
|
|
* (Structured Output) — die Antwort ist damit garantiert valides JSON
|
|
* in exakt dieser Form. Optionale Objekte (frist/termin) sind über
|
|
* anyOf [object, null] modelliert, da Structured Output
|
|
* additionalProperties: false und vollständige required-Listen verlangt.
|
|
*/
|
|
const ANALYSE_SCHEMA = {
|
|
type: 'object',
|
|
additionalProperties: false,
|
|
required: [
|
|
'brieftyp',
|
|
'absender',
|
|
'kernaussage',
|
|
'erklaerung_einfach',
|
|
'fachbegriffe',
|
|
'frist',
|
|
'termin',
|
|
'checkliste',
|
|
'antwort_noetig',
|
|
'antwort_optionen',
|
|
],
|
|
properties: {
|
|
brieftyp: {
|
|
type: 'string',
|
|
description:
|
|
'Kurze Kategorie des Briefs, z. B. "Einladung Jobcenter", "Bescheid Arbeitsagentur", "Steuerbescheid".',
|
|
},
|
|
absender: {
|
|
type: 'string',
|
|
description: 'Die Behörde, die den Brief geschickt hat.',
|
|
},
|
|
kernaussage: {
|
|
type: 'string',
|
|
description:
|
|
'Antwort auf "Was will das Amt von mir?" in 2-3 kurzen Sätzen, Sprachniveau A2.',
|
|
},
|
|
erklaerung_einfach: {
|
|
type: 'string',
|
|
description:
|
|
'Ausführlichere Erklärung des gesamten Briefs in einfacher Alltagssprache (A2/B1-Niveau). Kurze Sätze. Keine Fachbegriffe ohne Erklärung.',
|
|
},
|
|
fachbegriffe: {
|
|
type: 'array',
|
|
description: 'Alle Fachbegriffe/Amtsdeutsch-Wörter aus dem Brief mit einfacher Erklärung.',
|
|
items: {
|
|
type: 'object',
|
|
additionalProperties: false,
|
|
required: ['begriff', 'erklaerung'],
|
|
properties: {
|
|
begriff: { type: 'string' },
|
|
erklaerung: { type: 'string', description: 'Erklärung in einfacher Sprache (A2).' },
|
|
},
|
|
},
|
|
},
|
|
frist: {
|
|
description: 'Frist, bis wann der Nutzer reagieren muss. null wenn keine Frist im Brief steht.',
|
|
anyOf: [
|
|
{
|
|
type: 'object',
|
|
additionalProperties: false,
|
|
required: ['datum', 'aktion'],
|
|
properties: {
|
|
datum: { type: 'string', description: 'ISO-Format JJJJ-MM-TT.' },
|
|
aktion: { type: 'string', description: 'Was bis dahin passieren muss, z. B. "Unterlagen einreichen".' },
|
|
},
|
|
},
|
|
{ type: 'null' },
|
|
],
|
|
},
|
|
termin: {
|
|
description: 'Persönlicher Termin (Vorsprache o. Ä.). null wenn kein Termin im Brief steht.',
|
|
anyOf: [
|
|
{
|
|
type: 'object',
|
|
additionalProperties: false,
|
|
required: ['datum', 'uhrzeit', 'ort'],
|
|
properties: {
|
|
datum: { type: 'string', description: 'ISO-Format JJJJ-MM-TT.' },
|
|
uhrzeit: {
|
|
anyOf: [{ type: 'string' }, { type: 'null' }],
|
|
description: 'Format HH:MM, null wenn keine Uhrzeit genannt.',
|
|
},
|
|
ort: {
|
|
anyOf: [{ type: 'string' }, { type: 'null' }],
|
|
description: 'Adresse/Raum, null wenn nicht genannt.',
|
|
},
|
|
},
|
|
},
|
|
{ type: 'null' },
|
|
],
|
|
},
|
|
checkliste: {
|
|
type: 'array',
|
|
description:
|
|
'To-do-Liste für den Nutzer: was tun, was mitbringen, was einreichen. Leer wenn nichts zu tun ist.',
|
|
items: { type: 'string' },
|
|
},
|
|
antwort_noetig: {
|
|
type: 'boolean',
|
|
description: 'true wenn der Nutzer antworten/reagieren muss.',
|
|
},
|
|
antwort_optionen: {
|
|
type: 'array',
|
|
description: 'Welche Antwort-Vorlagen für diesen Brief sinnvoll sind.',
|
|
items: {
|
|
type: 'string',
|
|
enum: [
|
|
'terminbestaetigung',
|
|
'terminverschiebung',
|
|
'widerspruch',
|
|
'unterlagen_nachreichen',
|
|
'rueckfrage',
|
|
],
|
|
},
|
|
},
|
|
},
|
|
} as const;
|
|
|
|
const SYSTEM_PROMPT = `Du bist ein Assistent, der deutschen Behördenbriefe für Privatpersonen verständlich macht. Die Nutzer sind Deutsche, die Amtsdeutsch schwer verstehen, oder Menschen mit Deutsch als Fremdsprache.
|
|
|
|
Deine Aufgabe:
|
|
1. Lies den fotografierten/hochgeladenen Brief vollständig.
|
|
2. Erkläre ihn in einfacher Alltagssprache (Sprachniveau A2/B1): kurze Sätze, keine Schachtelsätze, keine unerklärten Fachbegriffe.
|
|
3. Extrahiere Fristen und Termine exakt. Datumsangaben immer als ISO-Format (JJJJ-MM-TT). Wenn du ein Datum nicht sicher lesen kannst, lass das Feld null — erfinde niemals Daten.
|
|
4. Erstelle eine konkrete Checkliste, was der Nutzer tun muss.
|
|
5. Sei sachlich und beruhigend, nicht alarmierend.
|
|
|
|
Wichtig: Wenn das Bild kein Behördenbrief ist oder unlesbar ist, schreibe das klar in kernaussage und erklaerung_einfach und lasse frist/termin null.`;
|
|
|
|
/**
|
|
* Analysiert ein Brief-Foto (JPEG/PNG als base64) oder ein PDF.
|
|
*
|
|
* @param base64 - Dateiinhalt als Base64-String (ohne data:-Präfix)
|
|
* @param mimeType - z. B. 'image/jpeg', 'image/png' oder 'application/pdf'
|
|
*/
|
|
export async function analysiereBrief(
|
|
base64: string,
|
|
mimeType: string
|
|
): Promise<BriefAnalyse> {
|
|
// PDF nutzt einen document-Block, Bilder einen image-Block
|
|
const dateiBlock: ContentBlock =
|
|
mimeType === 'application/pdf'
|
|
? {
|
|
type: 'document',
|
|
source: { type: 'base64', media_type: 'application/pdf', data: base64 },
|
|
}
|
|
: {
|
|
type: 'image',
|
|
source: { type: 'base64', media_type: mimeType, data: base64 },
|
|
};
|
|
|
|
return claudeJsonAufruf<BriefAnalyse>({
|
|
modell: MODELL_ANALYSE,
|
|
system: SYSTEM_PROMPT,
|
|
schema: ANALYSE_SCHEMA,
|
|
messages: [
|
|
{
|
|
role: 'user',
|
|
content: [
|
|
dateiBlock,
|
|
{
|
|
type: 'text',
|
|
text: `Analysiere diesen Behördenbrief. Heute ist der ${new Date().toISOString().slice(0, 10)} (wichtig für relative Datumsangaben wie "innerhalb von 14 Tagen").`,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
}
|