Restore the SDK context, types, and component files to the HEAD version since newer pages (company-profile, import) depend on these API changes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
465 lines
15 KiB
TypeScript
465 lines
15 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState, useEffect, useRef, useMemo } from 'react'
|
|
import { useRouter } from 'next/navigation'
|
|
import { useSDK, SDK_STEPS, CommandType, CommandHistory, downloadExport } from '@/lib/sdk'
|
|
|
|
// =============================================================================
|
|
// TYPES
|
|
// =============================================================================
|
|
|
|
interface Suggestion {
|
|
id: string
|
|
type: CommandType
|
|
label: string
|
|
description: string
|
|
shortcut?: string
|
|
icon: React.ReactNode
|
|
action: () => void | Promise<void>
|
|
}
|
|
|
|
// =============================================================================
|
|
// ICONS
|
|
// =============================================================================
|
|
|
|
const icons = {
|
|
navigation: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7l5 5m0 0l-5 5m5-5H6" />
|
|
</svg>
|
|
),
|
|
action: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
</svg>
|
|
),
|
|
search: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
</svg>
|
|
),
|
|
generate: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
|
</svg>
|
|
),
|
|
help: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
),
|
|
}
|
|
|
|
// =============================================================================
|
|
// COMMAND BAR
|
|
// =============================================================================
|
|
|
|
interface CommandBarProps {
|
|
onClose: () => void
|
|
}
|
|
|
|
export function CommandBar({ onClose }: CommandBarProps) {
|
|
const router = useRouter()
|
|
const { state, dispatch, goToStep } = useSDK()
|
|
const inputRef = useRef<HTMLInputElement>(null)
|
|
const [query, setQuery] = useState('')
|
|
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
const [isLoading, setIsLoading] = useState(false)
|
|
|
|
// Focus input on mount
|
|
useEffect(() => {
|
|
inputRef.current?.focus()
|
|
}, [])
|
|
|
|
// Generate suggestions based on query
|
|
const suggestions = useMemo((): Suggestion[] => {
|
|
const results: Suggestion[] = []
|
|
const lowerQuery = query.toLowerCase()
|
|
|
|
// Navigation suggestions
|
|
SDK_STEPS.forEach(step => {
|
|
const matchesName = step.name.toLowerCase().includes(lowerQuery) ||
|
|
step.nameShort.toLowerCase().includes(lowerQuery)
|
|
const matchesDescription = step.description.toLowerCase().includes(lowerQuery)
|
|
|
|
if (!query || matchesName || matchesDescription) {
|
|
results.push({
|
|
id: `nav-${step.id}`,
|
|
type: 'NAVIGATION',
|
|
label: `Gehe zu ${step.name}`,
|
|
description: step.description,
|
|
icon: icons.navigation,
|
|
action: () => {
|
|
goToStep(step.id)
|
|
onClose()
|
|
},
|
|
})
|
|
}
|
|
})
|
|
|
|
// Action suggestions
|
|
const actions: Suggestion[] = [
|
|
{
|
|
id: 'action-new-usecase',
|
|
type: 'ACTION',
|
|
label: 'Neuen Anwendungsfall erstellen',
|
|
description: 'Startet die Anwendungsfall-Erfassung',
|
|
icon: icons.action,
|
|
action: () => {
|
|
goToStep('use-case-assessment')
|
|
onClose()
|
|
},
|
|
},
|
|
{
|
|
id: 'action-export-pdf',
|
|
type: 'ACTION',
|
|
label: 'Als PDF exportieren',
|
|
description: 'Exportiert Compliance-Bericht als PDF',
|
|
icon: icons.action,
|
|
action: async () => {
|
|
setIsLoading(true)
|
|
try {
|
|
await downloadExport(state, 'pdf')
|
|
} catch (error) {
|
|
console.error('PDF export failed:', error)
|
|
} finally {
|
|
setIsLoading(false)
|
|
onClose()
|
|
}
|
|
},
|
|
},
|
|
{
|
|
id: 'action-export-zip',
|
|
type: 'ACTION',
|
|
label: 'Als ZIP exportieren',
|
|
description: 'Exportiert alle Daten und Dokumente als ZIP-Archiv',
|
|
icon: icons.action,
|
|
action: async () => {
|
|
setIsLoading(true)
|
|
try {
|
|
await downloadExport(state, 'zip')
|
|
} catch (error) {
|
|
console.error('ZIP export failed:', error)
|
|
} finally {
|
|
setIsLoading(false)
|
|
onClose()
|
|
}
|
|
},
|
|
},
|
|
{
|
|
id: 'action-export-json',
|
|
type: 'ACTION',
|
|
label: 'Als JSON exportieren',
|
|
description: 'Exportiert den kompletten State als JSON',
|
|
icon: icons.action,
|
|
action: async () => {
|
|
try {
|
|
await downloadExport(state, 'json')
|
|
} catch (error) {
|
|
console.error('JSON export failed:', error)
|
|
} finally {
|
|
onClose()
|
|
}
|
|
},
|
|
},
|
|
{
|
|
id: 'action-validate',
|
|
type: 'ACTION',
|
|
label: 'Checkpoint validieren',
|
|
description: 'Validiert den aktuellen Schritt',
|
|
icon: icons.action,
|
|
action: () => {
|
|
// Trigger validation
|
|
onClose()
|
|
},
|
|
},
|
|
]
|
|
|
|
actions.forEach(action => {
|
|
if (!query || action.label.toLowerCase().includes(lowerQuery)) {
|
|
results.push(action)
|
|
}
|
|
})
|
|
|
|
// Generate suggestions
|
|
const generateSuggestions: Suggestion[] = [
|
|
{
|
|
id: 'gen-dsfa',
|
|
type: 'GENERATE',
|
|
label: 'DSFA generieren',
|
|
description: 'Generiert eine Datenschutz-Folgenabschätzung',
|
|
icon: icons.generate,
|
|
action: () => {
|
|
goToStep('dsfa')
|
|
onClose()
|
|
},
|
|
},
|
|
{
|
|
id: 'gen-tom',
|
|
type: 'GENERATE',
|
|
label: 'TOMs generieren',
|
|
description: 'Generiert technische und organisatorische Maßnahmen',
|
|
icon: icons.generate,
|
|
action: () => {
|
|
goToStep('tom')
|
|
onClose()
|
|
},
|
|
},
|
|
{
|
|
id: 'gen-vvt',
|
|
type: 'GENERATE',
|
|
label: 'VVT generieren',
|
|
description: 'Generiert das Verarbeitungsverzeichnis',
|
|
icon: icons.generate,
|
|
action: () => {
|
|
goToStep('vvt')
|
|
onClose()
|
|
},
|
|
},
|
|
{
|
|
id: 'gen-cookie',
|
|
type: 'GENERATE',
|
|
label: 'Cookie Banner generieren',
|
|
description: 'Generiert Cookie-Consent-Banner Code',
|
|
icon: icons.generate,
|
|
action: () => {
|
|
goToStep('cookie-banner')
|
|
onClose()
|
|
},
|
|
},
|
|
]
|
|
|
|
generateSuggestions.forEach(suggestion => {
|
|
if (!query || suggestion.label.toLowerCase().includes(lowerQuery)) {
|
|
results.push(suggestion)
|
|
}
|
|
})
|
|
|
|
// Help suggestions
|
|
const helpSuggestions: Suggestion[] = [
|
|
{
|
|
id: 'help-docs',
|
|
type: 'HELP',
|
|
label: 'Dokumentation öffnen',
|
|
description: 'Öffnet die SDK-Dokumentation',
|
|
icon: icons.help,
|
|
action: () => {
|
|
window.open('/docs/sdk', '_blank')
|
|
onClose()
|
|
},
|
|
},
|
|
{
|
|
id: 'help-next',
|
|
type: 'HELP',
|
|
label: 'Was muss ich als nächstes tun?',
|
|
description: 'Zeigt den nächsten empfohlenen Schritt',
|
|
icon: icons.help,
|
|
action: () => {
|
|
// Show contextual help
|
|
onClose()
|
|
},
|
|
},
|
|
]
|
|
|
|
helpSuggestions.forEach(suggestion => {
|
|
if (!query || suggestion.label.toLowerCase().includes(lowerQuery)) {
|
|
results.push(suggestion)
|
|
}
|
|
})
|
|
|
|
return results.slice(0, 10)
|
|
}, [query, state, goToStep, onClose])
|
|
|
|
// Keyboard navigation
|
|
useEffect(() => {
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
switch (e.key) {
|
|
case 'ArrowDown':
|
|
e.preventDefault()
|
|
setSelectedIndex(prev => Math.min(prev + 1, suggestions.length - 1))
|
|
break
|
|
case 'ArrowUp':
|
|
e.preventDefault()
|
|
setSelectedIndex(prev => Math.max(prev - 1, 0))
|
|
break
|
|
case 'Enter':
|
|
e.preventDefault()
|
|
if (suggestions[selectedIndex]) {
|
|
executeSuggestion(suggestions[selectedIndex])
|
|
}
|
|
break
|
|
case 'Escape':
|
|
onClose()
|
|
break
|
|
}
|
|
}
|
|
|
|
window.addEventListener('keydown', handleKeyDown)
|
|
return () => window.removeEventListener('keydown', handleKeyDown)
|
|
}, [suggestions, selectedIndex, onClose])
|
|
|
|
// Reset selected index when query changes
|
|
useEffect(() => {
|
|
setSelectedIndex(0)
|
|
}, [query])
|
|
|
|
const executeSuggestion = async (suggestion: Suggestion) => {
|
|
// Add to history
|
|
const historyEntry: CommandHistory = {
|
|
id: `${Date.now()}`,
|
|
query: suggestion.label,
|
|
type: suggestion.type,
|
|
timestamp: new Date(),
|
|
success: true,
|
|
}
|
|
dispatch({ type: 'ADD_COMMAND_HISTORY', payload: historyEntry })
|
|
|
|
try {
|
|
await suggestion.action()
|
|
} catch (error) {
|
|
console.error('Command execution failed:', error)
|
|
}
|
|
}
|
|
|
|
const getTypeLabel = (type: CommandType): string => {
|
|
switch (type) {
|
|
case 'NAVIGATION':
|
|
return 'Navigation'
|
|
case 'ACTION':
|
|
return 'Aktion'
|
|
case 'SEARCH':
|
|
return 'Suche'
|
|
case 'GENERATE':
|
|
return 'Generieren'
|
|
case 'HELP':
|
|
return 'Hilfe'
|
|
default:
|
|
return type
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 overflow-y-auto">
|
|
{/* Backdrop */}
|
|
<div
|
|
className="fixed inset-0 bg-black/50 transition-opacity"
|
|
onClick={onClose}
|
|
/>
|
|
|
|
{/* Dialog */}
|
|
<div className="relative min-h-screen flex items-start justify-center pt-[15vh] px-4">
|
|
<div className="relative w-full max-w-2xl bg-white rounded-2xl shadow-2xl overflow-hidden">
|
|
{/* Search Input */}
|
|
<div className="flex items-center gap-3 px-4 py-3 border-b border-gray-200">
|
|
<svg className="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
/>
|
|
</svg>
|
|
<input
|
|
ref={inputRef}
|
|
type="text"
|
|
value={query}
|
|
onChange={e => setQuery(e.target.value)}
|
|
placeholder="Suchen oder Befehl eingeben..."
|
|
className="flex-1 text-lg bg-transparent border-none outline-none placeholder-gray-400"
|
|
/>
|
|
{isLoading && (
|
|
<svg className="animate-spin w-5 h-5 text-purple-600" fill="none" viewBox="0 0 24 24">
|
|
<circle
|
|
className="opacity-25"
|
|
cx="12"
|
|
cy="12"
|
|
r="10"
|
|
stroke="currentColor"
|
|
strokeWidth="4"
|
|
/>
|
|
<path
|
|
className="opacity-75"
|
|
fill="currentColor"
|
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
/>
|
|
</svg>
|
|
)}
|
|
<kbd className="px-2 py-1 text-xs text-gray-400 bg-gray-100 rounded">ESC</kbd>
|
|
</div>
|
|
|
|
{/* Suggestions */}
|
|
<div className="max-h-96 overflow-y-auto py-2">
|
|
{suggestions.length === 0 ? (
|
|
<div className="px-4 py-8 text-center text-gray-500">
|
|
Keine Ergebnisse für "{query}"
|
|
</div>
|
|
) : (
|
|
suggestions.map((suggestion, index) => (
|
|
<button
|
|
key={suggestion.id}
|
|
onClick={() => executeSuggestion(suggestion)}
|
|
className={`w-full flex items-center gap-3 px-4 py-3 text-left transition-colors ${
|
|
index === selectedIndex
|
|
? 'bg-purple-50 text-purple-900'
|
|
: 'text-gray-700 hover:bg-gray-50'
|
|
}`}
|
|
>
|
|
<div
|
|
className={`flex-shrink-0 w-10 h-10 rounded-lg flex items-center justify-center ${
|
|
index === selectedIndex
|
|
? 'bg-purple-100 text-purple-600'
|
|
: 'bg-gray-100 text-gray-500'
|
|
}`}
|
|
>
|
|
{suggestion.icon}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="font-medium truncate">{suggestion.label}</div>
|
|
<div className="text-sm text-gray-500 truncate">{suggestion.description}</div>
|
|
</div>
|
|
<div className="flex-shrink-0 flex items-center gap-2">
|
|
<span
|
|
className={`px-2 py-0.5 text-xs rounded-full ${
|
|
suggestion.type === 'NAVIGATION'
|
|
? 'bg-blue-100 text-blue-700'
|
|
: suggestion.type === 'ACTION'
|
|
? 'bg-green-100 text-green-700'
|
|
: suggestion.type === 'GENERATE'
|
|
? 'bg-purple-100 text-purple-700'
|
|
: 'bg-gray-100 text-gray-700'
|
|
}`}
|
|
>
|
|
{getTypeLabel(suggestion.type)}
|
|
</span>
|
|
{suggestion.shortcut && (
|
|
<kbd className="px-1.5 py-0.5 text-xs bg-gray-100 rounded">
|
|
{suggestion.shortcut}
|
|
</kbd>
|
|
)}
|
|
</div>
|
|
</button>
|
|
))
|
|
)}
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="flex items-center justify-between px-4 py-2 border-t border-gray-200 bg-gray-50 text-xs text-gray-500">
|
|
<div className="flex items-center gap-4">
|
|
<span className="flex items-center gap-1">
|
|
<kbd className="px-1.5 py-0.5 bg-gray-200 rounded">↑</kbd>
|
|
<kbd className="px-1.5 py-0.5 bg-gray-200 rounded">↓</kbd>
|
|
navigieren
|
|
</span>
|
|
<span className="flex items-center gap-1">
|
|
<kbd className="px-1.5 py-0.5 bg-gray-200 rounded">↵</kbd>
|
|
auswählen
|
|
</span>
|
|
</div>
|
|
<span>AI Compliance SDK v1.0</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|