feat(iace): complete CE risk assessment — LLM tech-file generation, multi-format export, TipTap editor
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 36s
CI/CD / test-python-backend-compliance (push) Successful in 33s
CI/CD / test-python-document-crawler (push) Successful in 24s
CI/CD / test-python-dsms-gateway (push) Successful in 21s
CI/CD / validate-canonical-controls (push) Successful in 13s
CI/CD / Deploy (push) Successful in 2s
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 36s
CI/CD / test-python-backend-compliance (push) Successful in 33s
CI/CD / test-python-document-crawler (push) Successful in 24s
CI/CD / test-python-dsms-gateway (push) Successful in 21s
CI/CD / validate-canonical-controls (push) Successful in 13s
CI/CD / Deploy (push) Successful in 2s
Phase 1: Fix completeness gates G23 (require verified/rejected mitigations) and G09 (audit trail check) Phase 2: LLM-based tech-file section generation with 19 German prompts and RAG enrichment Phase 3: Multi-format document export (PDF/Excel/DOCX/Markdown/JSON) Phase 4: Company profile → IACE data flow with auto component/classification creation Phase 5: TipTap WYSIWYG editor replacing textarea for tech-file sections Phase 6: User journey tests, developer portal API reference, updated documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { TechFileEditor } from '@/components/sdk/iace/TechFileEditor'
|
||||
|
||||
interface TechFileSection {
|
||||
id: string
|
||||
@@ -67,6 +68,14 @@ const STATUS_CONFIG: Record<string, { label: string; color: string; bgColor: str
|
||||
approved: { label: 'Freigegeben', color: 'text-green-700', bgColor: 'bg-green-100' },
|
||||
}
|
||||
|
||||
const EXPORT_FORMATS: { value: string; label: string; extension: string }[] = [
|
||||
{ value: 'pdf', label: 'PDF', extension: '.pdf' },
|
||||
{ value: 'xlsx', label: 'Excel', extension: '.xlsx' },
|
||||
{ value: 'docx', label: 'Word', extension: '.docx' },
|
||||
{ value: 'md', label: 'Markdown', extension: '.md' },
|
||||
{ value: 'json', label: 'JSON', extension: '.json' },
|
||||
]
|
||||
|
||||
function StatusBadge({ status }: { status: string }) {
|
||||
const config = STATUS_CONFIG[status] || STATUS_CONFIG.empty
|
||||
return (
|
||||
@@ -87,7 +96,6 @@ function SectionViewer({
|
||||
onApprove: (id: string) => void
|
||||
onSave: (id: string, content: string) => void
|
||||
}) {
|
||||
const [editedContent, setEditedContent] = useState(section.content || '')
|
||||
const [editing, setEditing] = useState(false)
|
||||
|
||||
return (
|
||||
@@ -111,13 +119,10 @@ function SectionViewer({
|
||||
)}
|
||||
{editing && (
|
||||
<button
|
||||
onClick={() => {
|
||||
onSave(section.id, editedContent)
|
||||
setEditing(false)
|
||||
}}
|
||||
className="text-sm px-3 py-1.5 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
|
||||
onClick={() => setEditing(false)}
|
||||
className="text-sm px-3 py-1.5 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
Speichern
|
||||
Fertig
|
||||
</button>
|
||||
)}
|
||||
{section.status !== 'approved' && section.content && !editing && (
|
||||
@@ -136,19 +141,19 @@ function SectionViewer({
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
{editing ? (
|
||||
<textarea
|
||||
value={editedContent}
|
||||
onChange={(e) => setEditedContent(e.target.value)}
|
||||
rows={20}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
||||
/>
|
||||
) : section.content ? (
|
||||
<div className="prose prose-sm max-w-none dark:prose-invert">
|
||||
<pre className="whitespace-pre-wrap text-sm text-gray-700 dark:text-gray-300 bg-gray-50 dark:bg-gray-750 p-4 rounded-lg">
|
||||
{section.content}
|
||||
</pre>
|
||||
</div>
|
||||
{section.content ? (
|
||||
editing ? (
|
||||
<TechFileEditor
|
||||
content={section.content}
|
||||
onSave={(html) => onSave(section.id, html)}
|
||||
/>
|
||||
) : (
|
||||
<TechFileEditor
|
||||
content={section.content}
|
||||
onSave={() => {}}
|
||||
readOnly
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
Kein Inhalt vorhanden. Klicken Sie "Generieren" um den Abschnitt zu erstellen.
|
||||
@@ -167,6 +172,21 @@ export default function TechFilePage() {
|
||||
const [generatingSection, setGeneratingSection] = useState<string | null>(null)
|
||||
const [viewingSection, setViewingSection] = useState<TechFileSection | null>(null)
|
||||
const [exporting, setExporting] = useState(false)
|
||||
const [showExportMenu, setShowExportMenu] = useState(false)
|
||||
const exportMenuRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Close export menu when clicking outside
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (exportMenuRef.current && !exportMenuRef.current.contains(event.target as Node)) {
|
||||
setShowExportMenu(false)
|
||||
}
|
||||
}
|
||||
if (showExportMenu) {
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [showExportMenu])
|
||||
|
||||
useEffect(() => {
|
||||
fetchSections()
|
||||
@@ -236,18 +256,22 @@ export default function TechFilePage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleExportZip() {
|
||||
async function handleExport(format: string) {
|
||||
setExporting(true)
|
||||
setShowExportMenu(false)
|
||||
try {
|
||||
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/tech-file/export`, {
|
||||
method: 'POST',
|
||||
})
|
||||
const res = await fetch(
|
||||
`/api/sdk/v1/iace/projects/${projectId}/tech-file/export?format=${format}`,
|
||||
{ method: 'GET' }
|
||||
)
|
||||
if (res.ok) {
|
||||
const blob = await res.blob()
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const formatConfig = EXPORT_FORMATS.find((f) => f.value === format)
|
||||
const extension = formatConfig?.extension || `.${format}`
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `CE-Akte-${projectId}.zip`
|
||||
a.download = `CE-Akte-${projectId}${extension}`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
@@ -284,25 +308,45 @@ export default function TechFilePage() {
|
||||
Sie alle erforderlichen Abschnitte.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleExportZip}
|
||||
disabled={!allRequiredApproved || exporting}
|
||||
title={!allRequiredApproved ? 'Alle Pflichtabschnitte muessen freigegeben sein' : 'CE-Akte als ZIP exportieren'}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||
allRequiredApproved && !exporting
|
||||
? 'bg-purple-600 text-white hover:bg-purple-700'
|
||||
: 'bg-gray-200 text-gray-400 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
{exporting ? (
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white" />
|
||||
) : (
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 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" />
|
||||
{/* Export Dropdown */}
|
||||
<div className="relative" ref={exportMenuRef}>
|
||||
<button
|
||||
onClick={() => setShowExportMenu((prev) => !prev)}
|
||||
disabled={!allRequiredApproved || exporting}
|
||||
title={!allRequiredApproved ? 'Alle Pflichtabschnitte muessen freigegeben sein' : 'CE-Akte exportieren'}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||
allRequiredApproved && !exporting
|
||||
? 'bg-purple-600 text-white hover:bg-purple-700'
|
||||
: 'bg-gray-200 text-gray-400 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
{exporting ? (
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white" />
|
||||
) : (
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 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" />
|
||||
</svg>
|
||||
)}
|
||||
Exportieren
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
{showExportMenu && allRequiredApproved && !exporting && (
|
||||
<div className="absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1 z-50">
|
||||
{EXPORT_FORMATS.map((fmt) => (
|
||||
<button
|
||||
key={fmt.value}
|
||||
onClick={() => handleExport(fmt.value)}
|
||||
className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors flex items-center gap-3"
|
||||
>
|
||||
<span className="text-xs font-mono uppercase w-10 text-gray-400">{fmt.extension}</span>
|
||||
<span>{fmt.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
ZIP exportieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress */}
|
||||
|
||||
Reference in New Issue
Block a user