feat(L4-L5): KI-Steuerung + XRechnung-Format-Layer (EN16931/CII)
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
L5 Format-Layer (User-Klaerung: Verkaufsmodul spaeter, Format JETZT): - einvoice.py: EN16931/XRechnung CII-XML-Generator (ElementTree, XML-Escaping gratis), Pflichtfeld-Validierung mit BT/BG-Codes, Decimal-kommerzielles Rounding, Header-Tax-Breakdown pro VAT-Satz, Profile en16931|xrechnung (XRechnung 3.0) - POST /einvoice/render (inline->XML), /einvoice/validate (422 mit BT-Fehlliste), /einvoice/render-for (Contract-Resolver einvoice_data() — Andockpunkt Verkaufsmodul, 404 no_data_source ohne Beitrag) L4 KI-Steuerung: - POST /documents/suggest: Natuerliche Sprache -> Block-Komposition via zentralem llm_complete (Cost-Tracking, Tenant-Budget), Registry-Sanitizing (ungueltige KI-Bloecke gefiltert, IDs serverseitig), Code-Fence-Stripping, 502 ai_unavailable/invalid_ai_response - Frontend: KI-Vorschlag-Panel im PrintTemplateEditor (Sparkles-Icon, Prompt-Textarea, Bloecke werden angehaengt), i18n de/en TDD: Rot 25 failed -> Gruen 25/25 (Validierung, XML-Struktur/Escaping/Summen, Contract-Mocks, API 200/422/403/404, Suggest Mock-LLM/Fence/502). tsc exit 0, Build OK, ruff clean. Doku: api-documentation.md, plugin-development-guide.md (einvoice_data-Contract), PROGRESS.md
This commit is contained in:
@@ -294,3 +294,25 @@ export async function renderPrintTemplate(input: RenderInput): Promise<Blob> {
|
||||
export function useRenderPrintTemplate() {
|
||||
return useMutation({ mutationFn: renderPrintTemplate });
|
||||
}
|
||||
|
||||
// ─── AI Block Suggestion (L4) ───────────────────────────────────────────────
|
||||
|
||||
export interface SuggestInput {
|
||||
prompt: string;
|
||||
entityType?: string | null;
|
||||
}
|
||||
|
||||
export interface SuggestResult {
|
||||
blocks: DocBlock[];
|
||||
notes: string;
|
||||
}
|
||||
|
||||
export function useSuggestDocumentBlocks() {
|
||||
return useMutation<SuggestResult, Error, SuggestInput>({
|
||||
mutationFn: (input: SuggestInput) =>
|
||||
apiPost<SuggestResult>('/reports/documents/suggest', {
|
||||
prompt: input.prompt,
|
||||
entity_type: input.entityType ?? null,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Sparkles } from 'lucide-react';
|
||||
import { Modal } from '@/components/ui/Modal';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
useCreatePrintTemplate,
|
||||
useUpdatePrintTemplate,
|
||||
useDocumentPlaceholders,
|
||||
useSuggestDocumentBlocks,
|
||||
type DocBlock,
|
||||
type Letterhead,
|
||||
type PrintTemplate,
|
||||
@@ -36,6 +38,27 @@ export function PrintTemplateEditor({ template, letterheads, onClose }: PrintTem
|
||||
const [letterheadId, setLetterheadId] = useState<string>(template?.letterhead_id ?? '');
|
||||
const [entityType, setEntityType] = useState<string>(template?.entity_type ?? 'contact');
|
||||
const [blocks, setBlocks] = useState<DocBlock[]>(template?.blocks ?? []);
|
||||
const [aiPrompt, setAiPrompt] = useState('');
|
||||
const [aiNotes, setAiNotes] = useState('');
|
||||
const suggest = useSuggestDocumentBlocks();
|
||||
|
||||
const handleSuggest = async () => {
|
||||
if (!aiPrompt.trim()) return;
|
||||
setAiNotes('');
|
||||
try {
|
||||
const result = await suggest.mutateAsync({ prompt: aiPrompt.trim(), entityType });
|
||||
if (result.blocks.length > 0) {
|
||||
setBlocks((prev) => [...prev, ...result.blocks]);
|
||||
setAiNotes(result.notes);
|
||||
setAiPrompt('');
|
||||
toast.success(t('documents.ai.suggested', '{{count}} Blöcke übernommen', { count: result.blocks.length }));
|
||||
} else {
|
||||
toast.error(t('documents.ai.noBlocks', 'KI hat keine gültigen Blöcke vorgeschlagen'));
|
||||
}
|
||||
} catch {
|
||||
toast.error(t('documents.ai.failed', 'KI-Vorschlag fehlgeschlagen'));
|
||||
}
|
||||
};
|
||||
|
||||
const createMutation = useCreatePrintTemplate();
|
||||
const updateMutation = useUpdatePrintTemplate();
|
||||
@@ -140,6 +163,39 @@ export function PrintTemplateEditor({ template, letterheads, onClose }: PrintTem
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* AI suggestion (L4) */}
|
||||
<div className="border border-primary-200 rounded-lg p-3 bg-primary-50/50" data-testid="ai-suggest-panel">
|
||||
<label className="text-xs font-semibold text-primary-700 uppercase flex items-center gap-1" htmlFor="ai-prompt">
|
||||
<Sparkles className="w-3.5 h-3.5" aria-hidden="true" />
|
||||
{t('documents.ai.title', 'KI-Vorschlag')}
|
||||
</label>
|
||||
<p className="text-xs text-secondary-500 mt-1">
|
||||
{t('documents.ai.hint', 'Beschreibe die Vorlage — die KI schlägt Blöcke vor und fügt sie hinzu.')}
|
||||
</p>
|
||||
<div className="flex gap-2 mt-2">
|
||||
<textarea
|
||||
id="ai-prompt"
|
||||
className="flex-1 rounded-md border border-secondary-300 px-3 py-2 text-sm min-h-[60px] focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
value={aiPrompt}
|
||||
onChange={(e) => setAiPrompt(e.target.value)}
|
||||
placeholder={t('documents.ai.placeholder', 'z.B. Angebotsvorlage mit Anrede, Firmenname, Leistungsliste und Grußformel')}
|
||||
data-testid="ai-prompt-input"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleSuggest}
|
||||
isLoading={suggest.isPending}
|
||||
icon={<Sparkles className="w-4 h-4" aria-hidden="true" />}
|
||||
data-testid="ai-suggest-button"
|
||||
>
|
||||
{t('documents.ai.suggest', 'Vorschlagen')}
|
||||
</Button>
|
||||
</div>
|
||||
{aiNotes && (
|
||||
<p className="text-xs text-secondary-600 mt-2" data-testid="ai-suggest-notes">{aiNotes}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Block editor (content blocks + letterhead frame preview) */}
|
||||
<BlockEditor
|
||||
blocks={blocks}
|
||||
|
||||
@@ -1559,6 +1559,15 @@
|
||||
"generated": "Dokument wurde erstellt",
|
||||
"generateFailed": "Dokument konnte nicht erstellt werden",
|
||||
"noTemplates": "Keine Druckvorlagen für diesen Datentyp. Vorlagen unter Einstellungen → Dokumente anlegen."
|
||||
},
|
||||
"ai": {
|
||||
"title": "KI-Vorschlag",
|
||||
"hint": "Beschreibe die Vorlage — die KI schlägt Blöcke vor und fügt sie hinzu.",
|
||||
"placeholder": "z.B. Angebotsvorlage mit Anrede, Firmenname, Leistungsliste und Grußformel",
|
||||
"suggest": "Vorschlagen",
|
||||
"suggested": "{{count}} Blöcke übernommen",
|
||||
"noBlocks": "KI hat keine gültigen Blöcke vorgeschlagen",
|
||||
"failed": "KI-Vorschlag fehlgeschlagen"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1559,6 +1559,15 @@
|
||||
"generated": "Document created",
|
||||
"generateFailed": "Document could not be created",
|
||||
"noTemplates": "No print templates for this data type. Create templates under Settings → Documents."
|
||||
},
|
||||
"ai": {
|
||||
"title": "AI Suggestion",
|
||||
"hint": "Describe the template — the AI suggests blocks and adds them.",
|
||||
"placeholder": "e.g. quote template with salutation, company name, service list and closing",
|
||||
"suggest": "Suggest",
|
||||
"suggested": "{{count}} blocks added",
|
||||
"noBlocks": "AI did not suggest valid blocks",
|
||||
"failed": "AI suggestion failed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user