forensic-pathways/src/components/AIQueryInterface.astro
2025-08-03 14:28:04 +02:00

1406 lines
56 KiB
Plaintext

---
import { getToolsData } from '../utils/dataService.js';
const data = await getToolsData();
const tools = data.tools;
const phases = data.phases;
const domainAgnosticSoftware = data['domain-agnostic-software'] || [];
---
<section id="ai-interface" class="ai-interface hidden">
<div class="ai-query-section">
<div class="content-center-lg">
<h2 style="margin-bottom: 1rem; color: var(--color-primary);">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.75rem; vertical-align: middle;">
<path d="M9 11H5a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7a2 2 0 0 0-2-2h-4"/>
<path d="M9 11V7a3 3 0 0 1 6 0v4"/>
</svg>
KI-gestützte Workflow-Empfehlungen
</h2>
<p id="ai-description" class="text-muted" style="max-width: 700px; margin: 0 auto; line-height: 1.6;">
Beschreiben Sie Ihr forensisches Szenario und erhalten Sie maßgeschneiderte Workflow-Empfehlungen
basierend auf bewährten DFIR-Workflows und der verfügbaren Software-Datenbank.
</p>
</div>
<div class="ai-input-container" style="max-width: 1000px; margin: 0 auto;">
<!-- Mode Toggle -->
<div class="ai-mode-toggle">
<span id="workflow-label" class="toggle-label active">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="mr-2 align-middle">
<polyline points="9,11 12,14 22,4"/>
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>
</svg>
Workflow-Empfehlung
</span>
<div class="toggle-switch">
<div class="toggle-slider"></div>
</div>
<span id="tool-label" class="toggle-label">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="mr-2 align-middle">
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>
</svg>
Spezifische Software oder Methode
</span>
</div>
<!-- Input Layout -->
<div class="ai-input-layout">
<div class="ai-textarea-section">
<textarea
id="ai-query-input"
placeholder="Beschreiben Sie Ihr forensisches Szenario..."
style="min-height: 220px; resize: vertical; font-size: 0.9375rem; line-height: 1.5;"
maxlength="2000"
></textarea>
<div id="ai-char-counter" style="font-size: 0.75rem; color: var(--color-text-secondary); text-align: right; margin-top: 0.25rem;">0/2000</div>
</div>
<div class="ai-suggestions-section">
<div id="smart-prompting-hint" class="smart-prompting-hint">
<div class="hint-card">
<div class="hint-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--color-accent)" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
<line x1="12" y1="17" x2="12.01" y2="17"/>
</svg>
</div>
<div class="hint-content">
<h4 class="hint-title">Intelligente Hilfe</h4>
<p class="hint-description">
Während Sie tippen, analysiert die KI Ihre Eingabe und schlägt gezielten Fragen vor.
</p>
<div class="hint-trigger">
<span class="hint-label">Aktiviert ab:</span>
<span class="hint-value">40+ Zeichen</span>
</div>
</div>
</div>
</div>
<div id="smart-prompting-container" class="smart-prompting-container hidden">
<div class="prompting-card">
<div id="prompting-status" class="prompting-status">
<div class="status-icon">💡</div>
<span class="status-text">Analysiere Eingabe...</span>
<div id="prompting-spinner" class="prompting-spinner hidden">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--color-accent)" stroke-width="2">
<path d="M21 12a9 9 0 11-6.219-8.56"/>
</svg>
</div>
</div>
<div id="suggested-questions" class="suggested-questions hidden">
<div class="suggestions-header">
<span class="suggestions-label">Zur besseren Analyse:</span>
</div>
<div id="questions-list" class="questions-list"></div>
<button id="dismiss-suggestions" class="dismiss-button">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Privacy Notice -->
<div style="margin-top: 0.5rem; margin-bottom: 1rem;">
<p style="font-size: 0.75rem; color: var(--color-text-secondary); text-align: center; line-height: 1.4;">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.25rem; vertical-align: middle;">
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="8" x2="12" y2="12"/>
<line x1="12" y1="16" x2="12.01" y2="16"/>
</svg>
Ihre Anfrage wird über die kostenlose API von mistral.ai übertragen.
<a href="https://mistral.ai/privacy-policy/" target="_blank" rel="noopener noreferrer" style="color: var(--color-primary);">Datenschutzrichtlinien</a>
</p>
</div>
<!-- Submit Button -->
<div style="display: flex; justify-content: center; gap: 1rem;">
<button id="ai-submit-btn" class="btn btn-accent" style="padding: 0.75rem 2rem;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<path d="M14.828 14.828a4 4 0 0 1-5.656 0"/>
<path d="M9 9a3 3 0 1 1 6 0c0 .749-.269 1.433-.73 1.96L11 14v1a1 1 0 0 1-1 1h-1a1 1 0 0 1-1-1v-1l-3.27-3.04A3 3 0 0 1 5 9a3 3 0 0 1 6 0"/>
</svg>
<span id="submit-btn-text">Empfehlungen generieren</span>
</button>
</div>
</div>
<!-- Loading State -->
<div id="ai-loading" class="ai-loading hidden">
<div style="text-align: center; padding: 2rem;">
<div style="margin-bottom: 1rem;">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" stroke-width="2" style="animation: pulse 2s ease-in-out infinite;">
<path d="M14.828 14.828a4 4 0 0 1-5.656 0"/>
<path d="M9 9a3 3 0 1 1 6 0c0 .749-.269 1.433-.73 1.96L11 14v1a1 1 0 0 1-1 1h-1a1 1 0 0 1-1-1v-1l-3.27-3.04A3 3 0 0 1 5 9a3 3 0 0 1 6 0"/>
</svg>
</div>
<p id="loading-text" class="text-secondary">Analysiere Szenario und generiere Empfehlungen...</p>
<!-- Queue Status -->
<div id="queue-status" class="queue-status-card hidden">
<div class="queue-header">
<div class="queue-position-display">
<div id="queue-position-badge" class="position-badge">1</div>
<span class="position-label">Position in Warteschlange</span>
</div>
</div>
<div class="queue-details">
<div class="queue-stat">
<span class="stat-label">Warteschlange:</span>
<span id="queue-length" class="stat-value">0</span>
</div>
<div class="queue-stat">
<span class="stat-label">Geschätzte Zeit:</span>
<span id="estimated-time" class="stat-value">--</span>
</div>
</div>
<!-- Micro-task Progress -->
<div id="micro-task-progress" class="micro-task-progress hidden">
<div class="micro-task-header">
<span class="micro-task-label">🔬 Micro-Task Analyse</span>
<span id="micro-task-counter" class="micro-task-counter">1/6</span>
</div>
<div class="micro-task-steps">
<div class="micro-step" data-step="scenario">📋 Szenario</div>
<div class="micro-step" data-step="approach">🎯 Ansatz</div>
<div class="micro-step" data-step="considerations">⚠️ Kritisches</div>
<div class="micro-step" data-step="tools">🔧 Tools</div>
<div class="micro-step" data-step="knowledge">📚 Wissen</div>
<div class="micro-step" data-step="final">✅ Final</div>
</div>
</div>
<div class="queue-progress-container">
<div class="queue-progress-track">
<div id="queue-progress" class="queue-progress-fill"></div>
</div>
<div class="task-id-display">
<span class="task-label">Task:</span>
<code id="current-task-id" class="task-id">--</code>
</div>
</div>
</div>
</div>
</div>
<!-- Error State -->
<div id="ai-error" class="ai-error hidden">
<div style="text-align: center; padding: 2rem;">
<div style="background-color: var(--color-error); color: white; padding: 1rem; border-radius: 0.5rem; max-width: 600px; margin: 0 auto;">
<h3 style="margin-bottom: 0.5rem;">Fehler bei der KI-Anfrage</h3>
<p id="ai-error-message" style="margin: 0;">Ein unerwarteter Fehler ist aufgetreten.</p>
</div>
</div>
</div>
<!-- Results -->
<div id="ai-results" class="ai-results hidden"></div>
</div>
</section>
<script define:vars={{ tools, phases, domainAgnosticSoftware }}>
class AIQueryInterface {
constructor() {
this.currentMode = 'workflow';
this.currentRecommendation = null;
this.enhancementTimeout = null;
this.enhancementAbortController = null;
this.statusInterval = null;
this.microTaskInterval = null;
this.currentMicroTaskStep = 0;
this.initializeElements();
this.setupEventListeners();
this.updateModeUI();
}
initializeElements() {
this.elements = {
input: document.getElementById('ai-query-input'),
submitBtn: document.getElementById('ai-submit-btn'),
submitBtnText: document.getElementById('submit-btn-text'),
description: document.getElementById('ai-description'),
loading: document.getElementById('ai-loading'),
loadingText: document.getElementById('loading-text'),
error: document.getElementById('ai-error'),
errorMessage: document.getElementById('ai-error-message'),
results: document.getElementById('ai-results'),
charCounter: document.getElementById('ai-char-counter'),
toggleSwitch: document.querySelector('.toggle-switch'),
toggleSlider: document.querySelector('.toggle-slider'),
workflowLabel: document.getElementById('workflow-label'),
toolLabel: document.getElementById('tool-label'),
promptingContainer: document.getElementById('smart-prompting-container'),
promptingStatus: document.getElementById('prompting-status'),
promptingSpinner: document.getElementById('prompting-spinner'),
suggestedQuestions: document.getElementById('suggested-questions'),
questionsList: document.getElementById('questions-list'),
dismissSuggestions: document.getElementById('dismiss-suggestions'),
smartHint: document.getElementById('smart-prompting-hint'),
queueStatus: document.getElementById('queue-status'),
queueLength: document.getElementById('queue-length'),
estimatedTime: document.getElementById('estimated-time'),
positionBadge: document.getElementById('queue-position-badge'),
progressBar: document.getElementById('queue-progress'),
taskIdDisplay: document.getElementById('current-task-id'),
microTaskProgress: document.getElementById('micro-task-progress'),
microTaskCounter: document.getElementById('micro-task-counter')
};
if (!this.elements.input || !this.elements.submitBtn || !this.elements.results) {
console.error('[AI Interface] Critical elements not found');
return false;
}
return true;
}
setupEventListeners() {
this.elements.toggleSwitch?.addEventListener('click', () => this.toggleMode());
this.elements.workflowLabel?.addEventListener('click', () => this.setMode('workflow'));
this.elements.toolLabel?.addEventListener('click', () => this.setMode('tool'));
this.elements.input?.addEventListener('input', () => this.handleInputChange());
this.elements.input?.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.handleSubmit();
}
});
this.elements.submitBtn?.addEventListener('click', () => this.handleSubmit());
this.elements.dismissSuggestions?.addEventListener('click', () => this.hideSmartPrompting());
}
getModeConfig() {
return {
workflow: {
placeholder: "Beschreiben Sie Ihr forensisches Szenario... z.B. 'Verdacht auf Ransomware-Angriff auf Windows-Domänencontroller'",
description: "Beschreiben Sie Ihr forensisches Szenario und erhalten Sie maßgeschneiderte Workflow-Empfehlungen.",
submitText: "Empfehlungen generieren",
loadingText: "Analysiere Szenario und generiere Empfehlungen..."
},
tool: {
placeholder: "Beschreiben Sie Ihr Problem... z.B. 'Analyse von Android-Backups mit WhatsApp-Nachrichten'",
description: "Beschreiben Sie Ihr Problem und erhalten Sie 1-3 gezielt passende Empfehlungen.",
submitText: "Empfehlungen finden",
loadingText: "Analysiere Anforderungen und suche passende Methode..."
}
};
}
toggleMode() {
this.setMode(this.currentMode === 'workflow' ? 'tool' : 'workflow');
}
setMode(mode) {
if (this.currentMode === mode) return;
this.currentMode = mode;
this.hideSmartPrompting();
this.hideResults();
this.updateModeUI();
}
updateModeUI() {
const config = this.getModeConfig()[this.currentMode];
if (this.elements.input) this.elements.input.placeholder = config.placeholder;
if (this.elements.description) this.elements.description.textContent = config.description;
if (this.elements.submitBtnText) this.elements.submitBtnText.textContent = config.submitText;
if (this.elements.loadingText) this.elements.loadingText.textContent = config.loadingText;
if (this.currentMode === 'workflow') {
if (this.elements.toggleSlider) this.elements.toggleSlider.style.transform = 'translateX(0)';
if (this.elements.toggleSwitch) this.elements.toggleSwitch.style.backgroundColor = 'var(--color-primary)';
if (this.elements.workflowLabel) {
this.elements.workflowLabel.style.color = 'var(--color-primary)';
this.elements.workflowLabel.classList.add('active');
}
if (this.elements.toolLabel) {
this.elements.toolLabel.style.color = 'var(--color-text-secondary)';
this.elements.toolLabel.classList.remove('active');
}
} else {
if (this.elements.toggleSlider) this.elements.toggleSlider.style.transform = 'translateX(26px)';
if (this.elements.toggleSwitch) this.elements.toggleSwitch.style.backgroundColor = 'var(--color-accent)';
if (this.elements.toolLabel) {
this.elements.toolLabel.style.color = 'var(--color-accent)';
this.elements.toolLabel.classList.add('active');
}
if (this.elements.workflowLabel) {
this.elements.workflowLabel.style.color = 'var(--color-text-secondary)';
this.elements.workflowLabel.classList.remove('active');
}
}
}
handleInputChange() {
this.updateCharacterCount();
clearTimeout(this.enhancementTimeout);
if (this.enhancementAbortController) {
this.enhancementAbortController.abort();
}
const inputLength = this.elements.input.value.trim().length;
if (inputLength < 40) {
this.hideSmartPrompting();
return;
}
this.enhancementTimeout = setTimeout(() => {
if (this.elements.input.value.trim().length >= 50) {
this.triggerSmartPrompting();
}
}, 1000);
}
updateCharacterCount() {
if (!this.elements.input || !this.elements.charCounter) return;
const length = this.elements.input.value.length;
const maxLength = 2000;
this.elements.charCounter.textContent = `${length}/${maxLength}`;
this.elements.charCounter.style.color = length > maxLength * 0.9 ?
'var(--color-warning)' : 'var(--color-text-secondary)';
}
async triggerSmartPrompting() {
const inputText = this.elements.input.value.trim();
if (inputText.length < 50) {
this.hideSmartPrompting();
return;
}
this.enhancementAbortController = new AbortController();
try {
this.showPromptingStatus('analyzing');
const response = await fetch('/api/ai/enhance-input', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ input: inputText }),
signal: this.enhancementAbortController.signal
});
if (!response.ok) {
this.showPromptingStatus(response.status === 429 ? 'rate-limited' : 'error');
return;
}
const data = await response.json();
if (data.success && data.questions?.length > 0) {
this.displaySuggestions(data.questions);
} else {
this.hideSmartPrompting();
}
} catch (error) {
if (error.name !== 'AbortError') {
console.warn('[AI Interface] Smart prompting failed:', error);
this.showPromptingStatus('error');
}
}
}
showPromptingStatus(state) {
if (!this.elements.promptingContainer || !this.elements.promptingStatus) return;
const statusText = this.elements.promptingStatus.querySelector('.status-text');
if (!statusText) return;
switch (state) {
case 'analyzing':
this.showElement(this.elements.promptingContainer);
this.hideElement(this.elements.smartHint);
statusText.textContent = 'Analysiere Eingabe...';
this.showElement(this.elements.promptingSpinner);
this.hideElement(this.elements.suggestedQuestions);
break;
case 'suggestions':
statusText.textContent = 'Verbesserungsvorschläge verfügbar';
this.hideElement(this.elements.promptingSpinner);
this.showElement(this.elements.suggestedQuestions);
break;
case 'rate-limited':
statusText.textContent = 'Nach Hauptabfrage verfügbar';
this.hideElement(this.elements.promptingSpinner);
this.hideElement(this.elements.suggestedQuestions);
break;
case 'error':
case 'hidden':
this.hideSmartPrompting();
break;
}
}
displaySuggestions(suggestions) {
if (!this.elements.questionsList || !suggestions?.length) return;
this.elements.questionsList.innerHTML = suggestions.map(question =>
`<div class="suggestion-item">${this.escapeHtml(question)}</div>`
).join('');
this.showPromptingStatus('suggestions');
}
hideSmartPrompting() {
if (this.elements.promptingContainer) this.hideElement(this.elements.promptingContainer);
if (this.elements.smartHint) this.showElement(this.elements.smartHint);
}
async handleSubmit() {
const query = this.elements.input.value.trim();
if (!query) {
alert('Bitte geben Sie eine Beschreibung ein.');
return;
}
if (query.length < 10) {
alert('Bitte geben Sie eine detailliertere Beschreibung ein (mindestens 10 Zeichen).');
return;
}
const taskId = `ai_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
this.hideSmartPrompting();
this.hideResults();
this.hideError();
this.showLoading();
this.startQueueMonitoring(taskId);
this.elements.submitBtn.disabled = true;
this.elements.submitBtnText.textContent = this.currentMode === 'workflow' ?
'Generiere Empfehlungen...' : 'Suche passende Methode...';
try {
const response = await fetch('/api/ai/query', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query,
mode: this.currentMode,
taskId
})
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || `HTTP ${response.status}`);
}
if (!data.success) {
throw new Error(data.error || 'Unknown error');
}
this.currentRecommendation = data.recommendation;
this.displayResults(data.recommendation, query);
} catch (error) {
console.error('[AI Interface] Request failed:', error);
this.showError(this.getErrorMessage(error));
} finally {
this.stopQueueMonitoring();
this.hideLoading();
this.resetSubmitButton();
}
}
getErrorMessage(error) {
if (error.message.includes('429')) {
return 'Zu viele Anfragen. Bitte warten Sie einen Moment.';
} else if (error.message.includes('401')) {
return 'Authentifizierung erforderlich. Bitte melden Sie sich an.';
} else if (error.message.includes('503')) {
return 'KI-Service vorübergehend nicht verfügbar.';
} else {
return `Fehler: ${error.message}`;
}
}
resetSubmitButton() {
if (this.elements.submitBtn) this.elements.submitBtn.disabled = false;
if (this.elements.submitBtnText) {
const config = this.getModeConfig()[this.currentMode];
this.elements.submitBtnText.textContent = config.submitText;
}
}
startQueueMonitoring(taskId) {
if (this.elements.queueStatus) {
this.showElement(this.elements.queueStatus);
}
if (this.elements.taskIdDisplay) {
this.elements.taskIdDisplay.textContent = taskId.slice(-8);
}
this.startMicroTaskProgress();
setTimeout(() => {
this.updateQueueStatus(taskId);
this.statusInterval = setInterval(() => this.updateQueueStatus(taskId), 1000);
}, 500);
}
async updateQueueStatus(taskId) {
try {
const response = await fetch(`/api/ai/queue-status?taskId=${taskId}`);
if (!response.ok) return;
const data = await response.json();
if (this.elements.queueLength) this.elements.queueLength.textContent = data.queueLength || 0;
if (this.elements.estimatedTime) {
this.elements.estimatedTime.textContent = data.estimatedWaitTime > 0 ?
this.formatDuration(data.estimatedWaitTime) : 'Verarbeitung läuft...';
}
if (this.elements.positionBadge) {
if (data.currentPosition) {
this.elements.positionBadge.textContent = data.currentPosition;
if (this.elements.progressBar && data.queueLength > 0) {
const progress = Math.max(0, ((data.queueLength - data.currentPosition + 1) / data.queueLength) * 100);
this.elements.progressBar.style.width = `${progress}%`;
}
} else {
const stateIcons = {
processing: '⚡',
completed: '✅',
failed: '❌'
};
this.elements.positionBadge.textContent = stateIcons[data.taskStatus] || '?';
if (this.elements.progressBar) {
this.elements.progressBar.style.width = data.taskStatus === 'processing' ? '100%' : '0%';
}
}
}
} catch (error) {
console.error('[AI Interface] Queue status update failed:', error);
}
}
stopQueueMonitoring() {
if (this.statusInterval) {
clearInterval(this.statusInterval);
this.statusInterval = null;
}
if (this.microTaskInterval) {
clearInterval(this.microTaskInterval);
this.microTaskInterval = null;
}
if (this.elements.queueStatus) this.hideElement(this.elements.queueStatus);
if (this.elements.microTaskProgress) this.hideElement(this.elements.microTaskProgress);
}
startMicroTaskProgress() {
if (!this.elements.microTaskProgress) return;
this.showElement(this.elements.microTaskProgress);
this.currentMicroTaskStep = 0;
const steps = ['scenario', 'approach', 'considerations', 'tools', 'knowledge', 'final'];
const stepElements = this.elements.microTaskProgress.querySelectorAll('.micro-step');
stepElements.forEach(step => {
step.classList.remove('active', 'completed', 'failed');
});
this.microTaskInterval = setInterval(() => {
if (this.currentMicroTaskStep < steps.length) {
if (this.currentMicroTaskStep > 0) {
const prevStep = this.elements.microTaskProgress.querySelector(`[data-step="${steps[this.currentMicroTaskStep - 1]}"]`);
if (prevStep) {
prevStep.classList.remove('active');
prevStep.classList.add('completed');
}
}
const currentStep = this.elements.microTaskProgress.querySelector(`[data-step="${steps[this.currentMicroTaskStep]}"]`);
if (currentStep) {
currentStep.classList.add('active');
}
if (this.elements.microTaskCounter) {
this.elements.microTaskCounter.textContent = `${this.currentMicroTaskStep + 1}/${steps.length}`;
}
this.currentMicroTaskStep++;
} else {
const lastStep = this.elements.microTaskProgress.querySelector(`[data-step="${steps[steps.length - 1]}"]`);
if (lastStep) {
lastStep.classList.remove('active');
lastStep.classList.add('completed');
}
if (this.elements.microTaskCounter) {
this.elements.microTaskCounter.textContent = `${steps.length}/${steps.length}`;
}
clearInterval(this.microTaskInterval);
this.microTaskInterval = null;
}
}, 2000);
}
displayResults(recommendation, originalQuery) {
if (this.currentMode === 'workflow') {
this.displayWorkflowResults(recommendation, originalQuery);
} else {
this.displayToolResults(recommendation, originalQuery);
}
this.showResults();
}
displayWorkflowResults(recommendation, originalQuery) {
const toolsByPhase = {};
const phaseOrder = phases.map(phase => phase.id);
const phaseNames = phases.reduce((acc, phase) => {
acc[phase.id] = phase.name;
return acc;
}, {});
phaseOrder.forEach(phase => {
toolsByPhase[phase] = [];
});
recommendation.recommended_tools?.forEach(recTool => {
if (toolsByPhase[recTool.phase]) {
const fullTool = tools.find(t => t.name === recTool.name);
if (fullTool) {
toolsByPhase[recTool.phase].push({
...fullTool,
recommendation: recTool
});
}
}
});
const html = `
<div class="workflow-container">
${this.renderHeader('Empfohlener DFIR-Workflow', originalQuery)}
${this.renderContextualAnalysis(recommendation, 'workflow')}
${this.renderBackgroundKnowledge(recommendation.background_knowledge)}
${this.renderWorkflowPhases(toolsByPhase, phaseOrder, phaseNames)}
${this.renderWorkflowSuggestion(recommendation.workflow_suggestion)}
${this.renderAuditTrail(recommendation.auditTrail)}
</div>
`;
this.elements.results.innerHTML = html;
}
displayToolResults(recommendation, originalQuery) {
const html = `
<div class="tool-results-container">
${this.renderHeader('Passende Empfehlungen', originalQuery)}
${this.renderContextualAnalysis(recommendation, 'tool')}
${this.renderBackgroundKnowledge(recommendation.background_knowledge)}
${this.renderToolRecommendations(recommendation.recommended_tools)}
${this.renderAdditionalConsiderations(recommendation.additional_considerations)}
${this.renderAuditTrail(recommendation.auditTrail)}
</div>
`;
this.elements.results.innerHTML = html;
}
renderAuditTrail(auditTrail) {
if (!auditTrail || !Array.isArray(auditTrail) || auditTrail.length === 0) {
return '';
}
// Calculate summary statistics
const totalTime = auditTrail.reduce((sum, entry) => sum + entry.processingTimeMs, 0);
const avgConfidence = auditTrail.reduce((sum, entry) => sum + entry.confidence, 0) / auditTrail.length;
const lowConfidenceSteps = auditTrail.filter(entry => entry.confidence < 60).length;
const highConfidenceSteps = auditTrail.filter(entry => entry.confidence >= 80).length;
// Group entries by phase for better organization
const groupedEntries = auditTrail.reduce((groups, entry) => {
if (!groups[entry.phase]) groups[entry.phase] = [];
groups[entry.phase].push(entry);
return groups;
}, {});
return `
<div class="card-info-sm mt-4" style="border-left: 4px solid var(--color-accent);">
<div class="flex items-center justify-between mb-3 cursor-pointer" onclick="const container = this.closest('.card-info-sm'); const details = container.querySelector('.audit-trail-details'); const isHidden = details.style.display === 'none'; details.style.display = isHidden ? 'block' : 'none'; this.querySelector('.toggle-icon').style.transform = isHidden ? 'rotate(180deg)' : 'rotate(0deg)';">
<div class="flex items-center gap-3">
<div class="flex items-center gap-2">
<div style="width: 24px; height: 24px; background: linear-gradient(135deg, var(--color-accent) 0%, var(--color-primary) 100%); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white; font-size: 0.75rem; font-weight: bold;">
</div>
<h4 class="text-sm font-semibold text-accent mb-0">KI-Entscheidungspfad</h4>
</div>
<div class="flex gap-3 text-xs">
<div class="flex items-center gap-1">
<div class="w-2 h-2 rounded-full" style="background-color: var(--color-accent);"></div>
<span class="text-muted">${this.formatDuration(totalTime)}</span>
</div>
<div class="flex items-center gap-1">
<div class="w-2 h-2 rounded-full" style="background-color: ${avgConfidence >= 80 ? 'var(--color-accent)' : avgConfidence >= 60 ? 'var(--color-warning)' : 'var(--color-error)'};"></div>
<span class="text-muted">${Math.round(avgConfidence)}% Vertrauen</span>
</div>
<div class="flex items-center gap-1">
<span class="text-muted">${auditTrail.length} Schritte</span>
</div>
</div>
</div>
<div class="toggle-icon" style="transition: transform 0.2s ease;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"/>
</svg>
</div>
</div>
<div style="display: none;" class="audit-trail-details">
<div class="grid gap-4">
<!-- Summary Section -->
<div class="p-3 rounded-lg" style="background-color: var(--color-bg-tertiary);">
<div class="text-xs font-medium mb-2 text-accent">📊 Analyse-Qualität</div>
<div class="grid grid-cols-3 gap-3 text-xs">
<div class="text-center">
<div class="font-semibold" style="color: var(--color-accent);">${highConfidenceSteps}</div>
<div class="text-muted">Hohe Sicherheit</div>
</div>
<div class="text-center">
<div class="font-semibold" style="color: ${lowConfidenceSteps > 0 ? 'var(--color-warning)' : 'var(--color-accent)'};">${lowConfidenceSteps}</div>
<div class="text-muted">Unsichere Schritte</div>
</div>
<div class="text-center">
<div class="font-semibold">${this.formatDuration(totalTime)}</div>
<div class="text-muted">Verarbeitungszeit</div>
</div>
</div>
</div>
<!-- Process Flow -->
<div class="audit-process-flow">
${Object.entries(groupedEntries).map(([phase, entries]) => this.renderPhaseGroup(phase, entries)).join('')}
</div>
<!-- Technical Details Toggle -->
<div class="text-center">
<button class="text-xs text-muted hover:text-primary transition-colors cursor-pointer border-none bg-none" onclick="const techDetails = this.nextElementSibling; const isHidden = techDetails.style.display === 'none'; techDetails.style.display = isHidden ? 'block' : 'none'; this.textContent = isHidden ? '🔧 Technische Details ausblenden' : '🔧 Technische Details anzeigen';">
🔧 Technische Details anzeigen
</button>
<div style="display: none;" class="mt-3 p-3 rounded-lg bg-gray-50 dark:bg-gray-800 text-xs">
${auditTrail.map(entry => this.renderTechnicalEntry(entry)).join('')}
</div>
</div>
</div>
</div>
</div>
`;
}
renderPhaseGroup(phase, entries) {
const phaseIcons = {
'initialization': '🚀',
'retrieval': '🔍',
'selection': '🎯',
'micro-task': '⚡',
'completion': '✅'
};
const phaseNames = {
'initialization': 'Initialisierung',
'retrieval': 'Datensuche',
'selection': 'Tool-Auswahl',
'micro-task': 'Detail-Analyse',
'completion': 'Finalisierung'
};
const avgConfidence = entries.reduce((sum, entry) => sum + entry.confidence, 0) / entries.length;
const totalTime = entries.reduce((sum, entry) => sum + entry.processingTimeMs, 0);
return `
<div class="phase-group mb-4">
<div class="flex items-center gap-3 mb-3">
<div class="flex items-center gap-2">
<span class="text-lg">${phaseIcons[phase] || '📋'}</span>
<span class="font-medium text-sm">${phaseNames[phase] || phase}</span>
</div>
<div class="flex-1 h-px bg-border"></div>
<div class="flex items-center gap-2 text-xs text-muted">
<div class="confidence-indicator w-12 h-2 rounded-full overflow-hidden" style="background-color: var(--color-bg-tertiary);">
<div class="h-full rounded-full transition-all" style="width: ${avgConfidence}%; background-color: ${avgConfidence >= 80 ? 'var(--color-accent)' : avgConfidence >= 60 ? 'var(--color-warning)' : 'var(--color-error)'};"></div>
</div>
<span>${Math.round(avgConfidence)}%</span>
</div>
</div>
<div class="grid gap-2 ml-6">
${entries.map(entry => this.renderSimplifiedEntry(entry)).join('')}
</div>
</div>
`;
}
renderSimplifiedEntry(entry) {
const actionIcons = {
'pipeline-start': '▶️',
'embeddings-search': '🔍',
'ai-tool-selection': '🎯',
'ai-analysis': '🧠',
'phase-tool-selection': '⚙️',
'tool-evaluation': '📊',
'background-knowledge-selection': '📚',
'pipeline-end': '🏁'
};
const actionNames = {
'pipeline-start': 'Analyse gestartet',
'embeddings-search': 'Ähnliche Tools gesucht',
'ai-tool-selection': 'Tools automatisch ausgewählt',
'ai-analysis': 'KI-Analyse durchgeführt',
'phase-tool-selection': 'Phasen-Tools evaluiert',
'tool-evaluation': 'Tool-Bewertung erstellt',
'background-knowledge-selection': 'Hintergrundwissen ausgewählt',
'pipeline-end': 'Analyse abgeschlossen'
};
const confidenceColor = entry.confidence >= 80 ? 'var(--color-accent)' :
entry.confidence >= 60 ? 'var(--color-warning)' : 'var(--color-error)';
return `
<div class="flex items-center gap-3 py-2 px-3 rounded-lg hover:bg-secondary transition-colors">
<span class="text-sm">${actionIcons[entry.action] || '📋'}</span>
<div class="flex-1 min-w-0">
<div class="text-sm font-medium">${actionNames[entry.action] || entry.action}</div>
${entry.output && typeof entry.output === 'object' && entry.output.selectedToolCount ?
`<div class="text-xs text-muted">${entry.output.selectedToolCount} Tools ausgewählt</div>` : ''}
</div>
<div class="flex items-center gap-2 text-xs">
<div class="w-8 h-1.5 rounded-full overflow-hidden" style="background-color: var(--color-bg-tertiary);">
<div class="h-full rounded-full" style="width: ${entry.confidence}%; background-color: ${confidenceColor};"></div>
</div>
<span class="text-muted w-8 text-right">${entry.confidence}%</span>
<span class="text-muted w-12 text-right">${entry.processingTimeMs}ms</span>
</div>
</div>
`;
}
renderTechnicalEntry(entry) {
const formattedTime = new Date(entry.timestamp).toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
return `
<div class="border rounded p-2 mb-2" style="border-color: var(--color-border);">
<div class="flex justify-between items-center mb-1">
<span class="font-mono text-xs">${entry.phase}/${entry.action}</span>
<span class="text-xs text-muted">${formattedTime} • ${entry.processingTimeMs}ms</span>
</div>
${entry.input && Object.keys(entry.input).length > 0 ? `
<div class="text-xs mb-1">
<strong>Input:</strong> ${this.formatAuditData(entry.input)}
</div>
` : ''}
${entry.output && Object.keys(entry.output).length > 0 ? `
<div class="text-xs">
<strong>Output:</strong> ${this.formatAuditData(entry.output)}
</div>
` : ''}
</div>
`;
}
renderAuditEntry(entry) {
const confidenceColor = entry.confidence >= 80 ? 'var(--color-accent)' :
entry.confidence >= 60 ? 'var(--color-warning)' : 'var(--color-error)';
const formattedTime = new Date(entry.timestamp).toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
// Reuse existing grid and text utilities
return `
<div class="border-l-2 pl-3 py-2 mb-2" style="border-left-color: ${confidenceColor};">
<div class="flex justify-between items-center mb-1">
<span class="text-xs font-medium">${entry.phase} → ${entry.action}</span>
<div class="flex items-center gap-2">
<span class="badge badge-mini" style="background-color: ${confidenceColor}; color: white;">
${entry.confidence}% confidence
</span>
<span class="text-xs text-muted">${entry.processingTimeMs}ms</span>
<span class="text-xs text-muted">${formattedTime}</span>
</div>
</div>
<div class="text-xs text-muted grid-cols-2 gap-2" style="display: grid;">
<div><strong>Input:</strong> ${this.formatAuditData(entry.input)}</div>
<div><strong>Output:</strong> ${this.formatAuditData(entry.output)}</div>
</div>
${entry.metadata && Object.keys(entry.metadata).length > 0 ? `
<div class="text-xs text-muted mt-1 pt-1 border-t border-dashed">
<strong>Metadata:</strong> ${this.formatAuditData(entry.metadata)}
</div>
` : ''}
</div>
`;
}
formatAuditData(data) {
if (data === null || data === undefined) return 'null';
if (typeof data === 'string') {
return data.length > 100 ? data.slice(0, 100) + '...' : data;
}
if (typeof data === 'number') return data.toString();
if (typeof data === 'boolean') return data.toString();
if (Array.isArray(data)) {
if (data.length === 0) return '[]';
if (data.length <= 3) return JSON.stringify(data);
return `[${data.slice(0, 3).map(i => typeof i === 'string' ? i : JSON.stringify(i)).join(', ')}, ...+${data.length - 3}]`;
}
if (typeof data === 'object') {
const keys = Object.keys(data);
if (keys.length === 0) return '{}';
if (keys.length <= 3) {
return '{' + keys.map(k => `${k}: ${typeof data[k] === 'string' ? data[k].slice(0, 20) + (data[k].length > 20 ? '...' : '') : JSON.stringify(data[k])}`).join(', ') + '}';
}
return `{${keys.slice(0, 3).join(', ')}, ...+${keys.length - 3} keys}`;
}
return String(data);
}
renderHeader(title, query) {
return `
<div style="text-align: center; margin-bottom: 2rem; padding: 1.5rem; background: linear-gradient(135deg, var(--color-primary) 0%, #525252 100%); color: white; border-radius: 0.75rem;">
<h3 style="margin: 0 0 0.75rem 0; font-size: 1.5rem;">${title}</h3>
<p style="margin: 0; opacity: 0.9; line-height: 1.5;">
Basierend auf Ihrer Anfrage: "<em>${this.truncateText(query, 100)}</em>"
</p>
</div>
`;
}
renderContextualAnalysis(recommendation, mode) {
let html = '';
const analysisField = mode === 'workflow' ? recommendation.scenario_analysis : recommendation.problem_analysis;
if (analysisField) {
html += `
<div class="card contextual-analysis-card scenario">
<h4 style="margin: 0 0 1rem 0; color: var(--color-primary);">
${mode === 'workflow' ? 'Szenario-Analyse' : 'Problem-Analyse'}
</h4>
<div style="margin: 0; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word;">${this.sanitizeText(analysisField)}</div>
</div>
`;
}
if (recommendation.investigation_approach) {
html += `
<div class="card contextual-analysis-card approach">
<h4 style="margin: 0 0 1rem 0; color: var(--color-accent);">
${mode === 'workflow' ? 'Untersuchungsansatz' : 'Lösungsansatz'}
</h4>
<div style="margin: 0; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word;">${this.sanitizeText(recommendation.investigation_approach)}</div>
</div>
`;
}
if (recommendation.critical_considerations) {
html += `
<div class="card contextual-analysis-card critical">
<h4 style="margin: 0 0 1rem 0; color: var(--color-warning);">
${mode === 'workflow' ? 'Kritische Überlegungen' : 'Wichtige Voraussetzungen'}
</h4>
<div style="margin: 0; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word;">${this.sanitizeText(recommendation.critical_considerations)}</div>
</div>
`;
}
return html;
}
renderBackgroundKnowledge(backgroundKnowledge) {
if (!backgroundKnowledge?.length) return '';
const conceptLinks = backgroundKnowledge.map(concept => `
<div style="background-color: var(--color-concept-bg); border: 1px solid var(--color-concept); border-radius: 0.5rem; padding: 1rem; margin-bottom: 0.75rem;">
<div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem;">
<button onclick="window.showToolDetails('${concept.concept_name}', 'secondary')"
style="background: none; border: none; color: var(--color-concept); font-weight: 600; cursor: pointer; text-decoration: underline;">
📚 ${concept.concept_name}
</button>
<span class="badge" style="background-color: var(--color-concept); color: white; font-size: 0.625rem;">Hintergrundwissen</span>
</div>
<div style="margin: 0; font-size: 0.8125rem; line-height: 1.5; color: var(--color-text-secondary); white-space: pre-wrap; word-wrap: break-word;">
${this.sanitizeText(concept.relevance)}
</div>
</div>
`).join('');
return `
<div class="card" style="margin-bottom: 2rem; border-left: 4px solid var(--color-concept);">
<h4 style="margin: 0 0 1rem 0; color: var(--color-concept);">Empfohlenes Hintergrundwissen</h4>
${conceptLinks}
</div>
`;
}
renderWorkflowPhases(toolsByPhase, phaseOrder, phaseNames) {
return phaseOrder.map((phase, index) => {
const phaseTools = toolsByPhase[phase];
if (phaseTools.length === 0) return '';
return `
<div class="workflow-phase">
<div class="phase-header">
<div class="phase-number">${index + 1}</div>
<div class="phase-info">
<h3 class="phase-title">${phaseNames[phase]}</h3>
<div class="phase-tools">
${phaseTools.map(tool => this.renderWorkflowTool(tool)).join('')}
</div>
</div>
</div>
${index < phaseOrder.length - 1 ? '<div class="workflow-arrow">↓</div>' : ''}
</div>
`;
}).join('');
}
renderWorkflowTool(tool) {
const hasValidProjectUrl = this.isToolHosted(tool);
const priorityColors = {
high: 'var(--color-error)',
medium: 'var(--color-warning)',
low: 'var(--color-accent)'
};
return `
<div class="tool-recommendation ${this.getToolClass(tool, 'recommendation')}" onclick="window.showToolDetails('${tool.name}')">
<div class="tool-rec-header">
<h4 class="tool-rec-name">
${tool.icon ? `<span style="margin-right: 0.5rem;">${tool.icon}</span>` : ''}
${tool.name}
</h4>
<span class="tool-rec-priority ${tool.recommendation.priority}"
style="background-color: ${priorityColors[tool.recommendation.priority]}; color: white; padding: 0.25rem 0.5rem; border-radius: 1rem; font-size: 0.75rem;">
${tool.recommendation.priority}
</span>
</div>
<div class="tool-rec-justification" style="background-color: var(--color-bg-tertiary); padding: 0.75rem; border-radius: 0.375rem; border-left: 3px solid var(--color-primary); margin: 0.75rem 0; font-style: italic; white-space: pre-wrap; word-wrap: break-word;">
"${this.sanitizeText(tool.recommendation.justification)}"
</div>
<div style="font-size: 0.75rem; color: var(--color-text-secondary);">
${this.renderToolBadges(tool)}
<div style="margin-top: 0.5rem;">
${tool.type === 'method' ? 'Methode' : tool.platforms.join(', ') + ' • ' + tool.license}
</div>
</div>
</div>
`;
}
renderToolRecommendations(recommendedTools) {
if (!recommendedTools?.length) return '';
return `
<div class="tool-recommendations-grid" style="display: grid; gap: 1.5rem;">
${recommendedTools.map((toolRec, index) => {
const fullTool = tools.find(t => t.name === toolRec.name);
if (!fullTool) return '';
return this.renderDetailedTool(fullTool, toolRec, index + 1);
}).join('')}
</div>
`;
}
renderDetailedTool(tool, recommendation, rank) {
const rankColors = { 1: 'var(--color-accent)', 2: 'var(--color-primary)', 3: 'var(--color-warning)' };
const suitabilityColors = { high: 'var(--color-accent)', medium: 'var(--color-warning)', low: 'var(--color-text-secondary)' };
return `
<div class="card ${this.getToolClass(tool, 'card')}" style="cursor: pointer; position: relative;" onclick="window.showToolDetails('${tool.name}')">
<div style="position: absolute; top: -8px; right: -8px; width: 32px; height: 32px; background-color: ${rankColors[rank]}; color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 1.125rem;">
${rank}
</div>
<div style="margin-bottom: 1rem;">
<h3 style="margin: 0 0 0.5rem 0;">${tool.name}</h3>
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem;">
<span class="badge" style="background-color: ${suitabilityColors[recommendation.suitability_score]}; color: white;">
${this.getSuitabilityText(recommendation.suitability_score)}
</span>
${this.renderToolBadges(tool)}
</div>
</div>
<div style="margin-bottom: 1.5rem;">
<h4 style="margin: 0.8rem 0 0.75rem 0; color: var(--color-accent);">Warum diese Methode?</h4>
<div style="margin: 0; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word;">${this.sanitizeText(recommendation.detailed_explanation)}</div>
${recommendation.implementation_approach ? `
<h4 style="margin: 0.8rem 0 0.75rem 0; color: var(--color-primary);">Anwendungsansatz</h4>
<div style="margin: 0; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word;">${this.sanitizeText(recommendation.implementation_approach)}</div>
` : ''}
</div>
${this.renderProsAndCons(recommendation.pros, recommendation.cons)}
${this.renderToolMetadata(tool)}
${recommendation.alternatives ? this.renderAlternatives(recommendation.alternatives) : ''}
</div>
`;
}
renderProsAndCons(pros, cons) {
if (!pros?.length && !cons?.length) return '';
return `
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1.5rem;">
${pros?.length ? `
<div style="background-color: var(--color-oss-bg); padding: 1rem; border-radius: 0.5rem; border-left: 3px solid var(--color-accent);">
<h5 style="margin: 0 0 0.5rem 0; color: var(--color-accent); font-size: 0.875rem;">✓ Vorteile</h5>
<ul style="margin: 0; padding-left: 1rem;">
${pros.map(pro => `<li style="margin-bottom: 0.25rem;">${this.sanitizeText(pro)}</li>`).join('')}
</ul>
</div>
` : ''}
${cons?.length ? `
<div style="background-color: var(--color-hosted-bg); padding: 1rem; border-radius: 0.5rem; border-left: 3px solid var(--color-warning);">
<h5 style="margin: 0 0 0.5rem 0; color: var(--color-warning); font-size: 0.875rem;">✗ Nachteile</h5>
<ul style="margin: 0; padding-left: 1rem;">
${cons.map(con => `<li style="margin-bottom: 0.25rem;">${this.sanitizeText(con)}</li>`).join('')}
</ul>
</div>
` : ''}
</div>
`;
}
renderToolMetadata(tool) {
const isMethod = tool.type === 'method';
return `
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 0.75rem; font-size: 0.8125rem; color: var(--color-text-secondary); margin-bottom: 1rem;">
${!isMethod ? `<div><strong>Plattformen:</strong> ${tool.platforms.join(', ')}</div>` : ''}
<div><strong>Skill Level:</strong> ${tool.skillLevel}</div>
${!isMethod ? `<div><strong>Lizenz:</strong> ${tool.license}</div>` : ''}
<div><strong>Typ:</strong> ${isMethod ? 'Methode' : tool.accessType}</div>
</div>
`;
}
renderAlternatives(alternatives) {
return `
<div style="background-color: var(--color-bg-secondary); padding: 1rem; border-radius: 0.5rem; margin-bottom: 1rem;">
<h5 style="margin: 0 0 0.5rem 0; color: var(--color-text-secondary); font-size: 0.875rem;">Alternative Ansätze</h5>
<div style="margin: 0; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word;">${this.sanitizeText(alternatives)}</div>
</div>
`;
}
renderWorkflowSuggestion(suggestion) {
if (!suggestion) return '';
return `
<div class="card" style="margin-top: 2rem; border-left: 4px solid var(--color-accent);">
<h4 style="margin: 0 0 1rem 0; color: var(--color-accent);">Workflow-Empfehlung</h4>
<div style="margin: 0; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word;">${this.sanitizeText(suggestion)}</div>
</div>
`;
}
renderAdditionalConsiderations(considerations) {
if (!considerations) return '';
return `
<div class="card" style="margin-top: 2rem; background-color: var(--color-bg-secondary); border-left: 4px solid var(--color-text-secondary);">
<h4 style="margin: 0 0 1rem 0; color: var(--color-text-secondary);">Zusätzliche Überlegungen</h4>
<div style="margin: 0; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word;">${this.sanitizeText(considerations)}</div>
</div>
`;
}
renderToolBadges(tool) {
const isMethod = tool.type === 'method';
const hasValidProjectUrl = this.isToolHosted(tool);
let badges = '';
if (isMethod) {
badges += '<span class="badge" style="background-color: var(--color-method); color: white;">Methode</span>';
} else if (hasValidProjectUrl) {
badges += '<span class="badge badge-primary">CC24-Server</span>';
} else if (tool.license !== 'Proprietary') {
badges += '<span class="badge badge-success">Open Source</span>';
}
if (tool.knowledgebase === true) {
badges += '<span class="badge badge-error">📖</span>';
}
return badges;
}
sanitizeText(text) {
if (typeof text !== 'string') return '';
return text
// Remove markdown headers
.replace(/^#{1,6}\s+/gm, '')
// Remove markdown lists (bullet points and numbered)
.replace(/^\s*[-*+]\s+/gm, '')
.replace(/^\s*\d+\.\s+/gm, '')
// Remove markdown bold/italic
.replace(/\*\*(.+?)\*\*/g, '$1')
.replace(/\*(.+?)\*/g, '$1')
.replace(/__(.+?)__/g, '$1')
.replace(/_(.+?)_/g, '$1')
// Remove markdown links
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
// Remove code blocks
.replace(/```[\s\S]*?```/g, '[CODE BLOCK]')
.replace(/`([^`]+)`/g, '$1')
// Remove HTML tags
.replace(/<[^>]+>/g, '')
// Clean up excessive whitespace but preserve paragraphs
.replace(/\n\s*\n\s*\n/g, '\n\n')
.trim();
}
isToolHosted(tool) {
return tool.projectUrl !== undefined &&
tool.projectUrl !== null &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
}
getToolClass(tool, context = 'card') {
const isMethod = tool.type === 'method';
const hasValidProjectUrl = this.isToolHosted(tool);
if (context === 'recommendation') {
if (isMethod) return 'method';
if (hasValidProjectUrl) return 'hosted';
if (tool.license !== 'Proprietary') return 'oss';
return '';
} else {
if (isMethod) return 'card-method';
if (hasValidProjectUrl) return 'card-hosted';
if (tool.license !== 'Proprietary') return 'card-oss';
return '';
}
}
getSuitabilityText(score) {
const texts = {
high: 'GUT GEEIGNET',
medium: 'GEEIGNET',
low: 'VIELLEICHT GEEIGNET'
};
return texts[score] || 'GEEIGNET';
}
escapeHtml(text) {
if (typeof text !== 'string') return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
truncateText(text, maxLength) {
if (text.length <= maxLength) return text;
return text.slice(0, maxLength) + '...';
}
formatDuration(ms) {
if (ms < 1000) return '< 1s';
if (ms < 60000) return `${Math.ceil(ms / 1000)}s`;
return `${Math.ceil(ms / 60000)}m`;
}
showElement(element) {
if (element) {
element.style.display = 'block';
element.classList.remove('hidden');
}
}
hideElement(element) {
if (element) {
element.style.display = 'none';
element.classList.add('hidden');
}
}
showLoading() {
this.showElement(this.elements.loading);
}
hideLoading() {
this.hideElement(this.elements.loading);
}
showResults() {
this.showElement(this.elements.results);
}
hideResults() {
this.hideElement(this.elements.results);
this.hideElement(this.elements.error);
}
showError(message) {
if (this.elements.errorMessage) this.elements.errorMessage.textContent = message;
this.showElement(this.elements.error);
}
hideError() {
this.hideElement(this.elements.error);
}
}
document.addEventListener('DOMContentLoaded', () => {
const aiInterface = new AIQueryInterface();
window.restoreAIResults = () => {
if (aiInterface.currentRecommendation && aiInterface.elements.results) {
aiInterface.showResults();
aiInterface.hideLoading();
aiInterface.hideError();
}
};
});
</script>