updates to style

This commit is contained in:
overcuriousity 2025-07-17 01:16:31 +02:00
parent 89583664d6
commit eec6af739b
3 changed files with 433 additions and 548 deletions

View File

@ -2,6 +2,9 @@
Ein kuratiertes Verzeichnis für digitale Forensik- und Incident-Response-Tools, entwickelt für die Seminargruppe CC24-w1. Ein kuratiertes Verzeichnis für digitale Forensik- und Incident-Response-Tools, entwickelt für die Seminargruppe CC24-w1.
*DISCLAIMER:*
Hier wurde Exzessives Vibe-Coding verwendet. Die Auswahl der Software ist aber kuratiert.
## 🎯 Projektübersicht ## 🎯 Projektübersicht
CC24-Hub ist eine statische Website, die eine strukturierte Übersicht über bewährte DFIR-Tools bietet. Das Projekt orientiert sich am NIST-Framework (SP 800-86) und kategorisiert Tools nach forensischen Domänen und Untersuchungsphasen. CC24-Hub ist eine statische Website, die eine strukturierte Übersicht über bewährte DFIR-Tools bietet. Das Projekt orientiert sich am NIST-Framework (SP 800-86) und kategorisiert Tools nach forensischen Domänen und Untersuchungsphasen.

View File

@ -1,431 +1,157 @@
--- ---
// src/components/AIQueryInterface.astro
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { load } from 'js-yaml'; import { load } from 'js-yaml';
import path from 'path'; import path from 'path';
// Load tools data for tool details // Load tools data for validation
const yamlPath = path.join(process.cwd(), 'src/data/tools.yaml'); const yamlPath = path.join(process.cwd(), 'src/data/tools.yaml');
const yamlContent = await fs.readFile(yamlPath, 'utf8'); const yamlContent = await fs.readFile(yamlPath, 'utf8');
const data = load(yamlContent) as any; const data = load(yamlContent) as any;
const tools = data.tools; const tools = data.tools;
const phases = data.phases.filter((phase: any) => phase.id !== 'collaboration-general');
--- ---
<div id="ai-interface" class="ai-interface" style="display: none;"> <!-- AI Query Interface -->
<!-- AI Query Input Section --> <section id="ai-interface" class="ai-interface" style="display: none;">
<div class="ai-query-section"> <div class="ai-query-section">
<div style="text-align: center; margin-bottom: 2rem;"> <div style="text-align: center; margin-bottom: 2rem;">
<h2 style="margin-bottom: 1rem; color: var(--color-primary);"> <h2 style="margin-bottom: 1rem; color: var(--color-primary);">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem; vertical-align: text-top;"> <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 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"/> <path d="M9 11V7a3 3 0 0 1 6 0v4"/>
</svg> </svg>
KI-gestützte Tool-Empfehlungen KI-gestützte Tool-Empfehlungen
</h2> </h2>
<p class="text-muted" style="max-width: 600px; margin: 0 auto; line-height: 1.6;"> <p class="text-muted" style="max-width: 700px; margin: 0 auto; line-height: 1.6;">
Beschreiben Sie Ihr Ermittlungsszenario auf Deutsch oder Englisch und erhalten Sie Beschreiben Sie Ihr forensisches Szenario und erhalten Sie maßgeschneiderte Tool-Empfehlungen
personalisierte Tool-Empfehlungen basierend auf dem NIST-Framework. basierend auf bewährten DFIR-Workflows und der verfügbaren Software-Datenbank.
</p> </p>
</div> </div>
<div class="ai-input-container" style="max-width: 800px; margin: 0 auto 2rem;"> <div class="ai-input-container" style="max-width: 800px; margin: 0 auto;">
<div style="position: relative;">
<textarea <textarea
id="ai-query-input" id="ai-query-input"
placeholder="Beschreiben Sie Ihr Ermittlungsszenario... placeholder="Beschreiben Sie Ihr forensisches Szenario... z.B. 'Verdacht auf Ransomware-Angriff auf Windows-Domänencontroller mit verschlüsselten Dateien und verdächtigen Netzwerkverbindungen'"
style="min-height: 120px; resize: vertical; font-size: 0.9375rem; line-height: 1.5;"
Beispiele:
• Ich untersuche einen Ransomware-Angriff auf Windows-Systeme und brauche Tools für Memory-Analyse
• We need to investigate suspicious network traffic from an IoT device
• Ein Smartphone wurde kompromittiert - welche Tools für Mobile Forensik?"
style="width: 100%; min-height: 120px; padding: 1rem; border: 2px solid var(--color-border); border-radius: 0.5rem; resize: vertical; font-family: inherit; font-size: 0.875rem; line-height: 1.5;"
maxlength="2000" maxlength="2000"
></textarea> ></textarea>
<div style="position: absolute; bottom: 0.75rem; right: 0.75rem; font-size: 0.75rem; color: var(--color-text-secondary);">
<span id="char-count">0</span>/2000 <!-- Privacy Notice -->
</div> <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.0;">
<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>
Die Anfrage wird an mistral.ai übertragen und unterliegt deren
<a href="https://mistral.ai/privacy-policy/" target="_blank" rel="noopener noreferrer" style="color: var(--color-primary); text-decoration: underline;">Datenschutzrichtlinien</a>.
Eine typische Anfrage kostet mich $0.0008.
</p>
</div> </div>
<div style="display: flex; gap: 1rem; margin-top: 1rem; justify-content: center;"> <div style="display: flex; justify-content: center; gap: 1rem;">
<button id="ai-submit-btn" class="btn btn-primary" style="padding: 0.75rem 2rem;"> <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;"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<path d="M22 2L11 13"/> <path d="M14.828 14.828a4 4 0 0 1-5.656 0"/>
<path d="M22 2l-7 20-4-9-9-4 20-7z"/> <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"/>
<path d="M12 17h.01"/>
</svg> </svg>
Empfehlungen abrufen Empfehlungen generieren
</button>
<button id="ai-clear-btn" class="btn btn-secondary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<path d="M3 6h18"/>
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/>
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>
</svg>
Zurücksetzen
</button> </button>
</div> </div>
</div> </div>
</div>
<!-- Loading State --> <!-- Loading State -->
<div id="ai-loading" class="ai-loading" style="display: none; text-align: center; padding: 2rem;"> <div id="ai-loading" class="ai-loading" style="display: none; text-align: center; padding: 2rem;">
<div class="loading-spinner" style="margin-bottom: 1rem;"> <div style="display: inline-block; margin-bottom: 1rem;">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" stroke-width="2" style="animation: spin 1s linear infinite;"> <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="M12 2v4"/> <path d="M14.828 14.828a4 4 0 0 1-5.656 0"/>
<path d="M12 18v4"/> <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"/>
<path d="M4.93 4.93l2.83 2.83"/> <path d="M12 17h.01"/>
<path d="M16.24 16.24l2.83 2.83"/>
<path d="M2 12h4"/>
<path d="M18 12h4"/>
<path d="M4.93 19.07l2.83-2.83"/>
<path d="M16.24 7.76l2.83-2.83"/>
</svg> </svg>
</div> </div>
<h3 style="margin-bottom: 0.5rem; color: var(--color-primary);">KI analysiert Ihr Szenario...</h3> <p style="color: var(--color-text-secondary);">Analysiere Szenario und generiere Empfehlungen...</p>
<p class="text-muted">Dies kann einige Sekunden dauern.</p>
</div> </div>
<!-- Error State --> <!-- Error State -->
<div id="ai-error" class="ai-error" style="display: none; text-align: center; padding: 2rem;"> <div id="ai-error" class="ai-error" style="display: none; text-align: center; padding: 2rem;">
<div style="margin-bottom: 1rem;"> <div style="background-color: var(--color-error); color: white; padding: 1rem; border-radius: 0.5rem; max-width: 600px; margin: 0 auto;">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="var(--color-error)" stroke-width="2"> <h3 style="margin-bottom: 0.5rem;">Fehler bei der KI-Anfrage</h3>
<circle cx="12" cy="12" r="10"/> <p id="ai-error-message" style="margin: 0;">Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es erneut.</p>
<line x1="15" y1="9" x2="9" y2="15"/>
<line x1="9" y1="9" x2="15" y2="15"/>
</svg>
</div>
<h3 style="margin-bottom: 0.5rem; color: var(--color-error);">Fehler bei der Analyse</h3>
<p class="text-muted" id="ai-error-message">Ein unerwarteter Fehler ist aufgetreten.</p>
</div> </div>
</div> </div>
<!-- AI Results Section --> <!-- AI Results -->
<div id="ai-results" class="ai-results" style="display: none;"> <div id="ai-results" class="ai-results" style="display: none;">
<!-- Scenario Analysis --> <!-- Results will be populated here by JavaScript -->
<div class="card" style="margin-bottom: 2rem; border-left: 4px solid var(--color-primary);">
<h3 style="margin-bottom: 1rem; color: var(--color-primary);">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<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>
Szenario-Analyse
</h3>
<div id="scenario-analysis" style="line-height: 1.7;"></div>
</div> </div>
</section>
<!-- Workflow Visualization --> <script define:vars={{ tools }}>
<div style="margin-bottom: 2rem;">
<h3 style="margin-bottom: 1.5rem; text-align: center; color: var(--color-text);">
Empfohlener DFIR-Workflow
</h3>
<!-- Phase Flow -->
<div class="workflow-container">
{phases.map((phase: any, index: number) => (
<div class="workflow-phase" data-phase={phase.id}>
<div class="phase-header">
<div class="phase-number">{index + 1}</div>
<div class="phase-info">
<h4 class="phase-title">{phase.name}</h4>
<div class="phase-tools" id={`phase-tools-${phase.id}`}>
<!-- Tools will be populated by JavaScript -->
</div>
</div>
</div>
{index < phases.length - 1 && (
<div class="workflow-arrow">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" stroke-width="2">
<path d="M5 12h14"/>
<path d="M12 5l7 7-7 7"/>
</svg>
</div>
)}
</div>
))}
</div>
</div>
<!-- Additional Information -->
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
<div class="card">
<h4 style="margin-bottom: 1rem; color: var(--color-accent);">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>
Workflow-Empfehlung
</h4>
<div id="workflow-suggestion" style="line-height: 1.6;"></div>
</div>
<div class="card">
<h4 style="margin-bottom: 1rem; color: var(--color-warning);">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<circle cx="12" cy="12" r="10"/>
<path d="M12 16v-4"/>
<path d="M12 8h.01"/>
</svg>
Wichtige Hinweise
</h4>
<div id="additional-notes" style="line-height: 1.6;"></div>
</div>
</div>
<!-- New Query Button -->
<div style="text-align: center; margin-top: 2rem;">
<button id="new-query-btn" class="btn btn-secondary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/>
<path d="M21 3v5h-5"/>
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/>
<path d="M3 21v-5h5"/>
</svg>
Neue Anfrage starten
</button>
</div>
</div>
</div>
<style>
/* Loading spinner animation */
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Workflow styling */
.workflow-container {
display: flex;
flex-direction: column;
gap: 1rem;
max-width: 900px;
margin: 0 auto;
}
.workflow-phase {
position: relative;
}
.phase-header {
display: flex;
align-items: flex-start;
gap: 1rem;
padding: 1.5rem;
background-color: var(--color-bg);
border: 2px solid var(--color-border);
border-radius: 0.75rem;
transition: all 0.3s ease;
}
.phase-header:hover {
border-color: var(--color-primary);
box-shadow: var(--shadow-md);
}
.phase-number {
background-color: var(--color-primary);
color: white;
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 1.125rem;
flex-shrink: 0;
}
.phase-info {
flex: 1;
min-width: 0;
}
.phase-title {
margin: 0 0 1rem 0;
color: var(--color-text);
font-size: 1.25rem;
font-weight: 600;
}
.phase-tools {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.workflow-arrow {
display: flex;
justify-content: center;
margin: 0.5rem 0;
}
/* Tool recommendation cards */
.tool-recommendation {
background-color: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
padding: 1rem;
transition: all 0.2s ease;
cursor: pointer;
}
.tool-recommendation:hover {
border-color: var(--color-primary);
box-shadow: var(--shadow-sm);
}
.tool-recommendation.hosted {
background-color: var(--color-hosted-bg);
border-color: var(--color-hosted);
}
.tool-recommendation.oss {
background-color: var(--color-oss-bg);
border-color: var(--color-oss);
}
.tool-rec-header {
display: flex;
justify-content: space-between;
align-items: start;
margin-bottom: 0.75rem;
}
.tool-rec-name {
font-weight: 600;
font-size: 1rem;
margin: 0;
color: var(--color-text);
}
.tool-rec-priority {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
border-radius: 1rem;
font-weight: 500;
}
.tool-rec-priority.high {
background-color: var(--color-error);
color: white;
}
.tool-rec-priority.medium {
background-color: var(--color-warning);
color: white;
}
.tool-rec-priority.low {
background-color: var(--color-accent);
color: white;
}
.tool-rec-justification {
font-size: 0.875rem;
line-height: 1.5;
color: var(--color-text-secondary);
margin-bottom: 0.75rem;
font-style: italic;
}
.tool-rec-metadata {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
font-size: 0.75rem;
color: var(--color-text-secondary);
}
/* Responsive design */
@media (max-width: 768px) {
.workflow-container {
gap: 0.75rem;
}
.phase-header {
padding: 1rem;
}
.phase-number {
width: 2rem;
height: 2rem;
font-size: 1rem;
}
.phase-title {
font-size: 1.125rem;
}
.tool-rec-header {
flex-direction: column;
gap: 0.5rem;
align-items: start;
}
}
</style>
<script define:vars={{ toolsData: tools }}>
// Global tool data for quick lookup
window.aiToolsData = toolsData;
// Store AI session results
window.aiSessionResults = null;
// Authentication check function
async function checkAuthentication() {
try {
const response = await fetch('/api/auth/status');
const data = await response.json();
return {
authenticated: data.authenticated,
authRequired: data.authRequired
};
} catch (error) {
console.error('Auth check failed:', error);
return {
authenticated: false,
authRequired: true
};
}
}
// Initialize AI interface
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const queryInput = document.getElementById('ai-query-input'); const aiInterface = document.getElementById('ai-interface');
const submitBtn = document.getElementById('ai-submit-btn'); const aiInput = document.getElementById('ai-query-input');
const clearBtn = document.getElementById('ai-clear-btn'); const aiSubmitBtn = document.getElementById('ai-submit-btn');
const charCount = document.getElementById('char-count'); const aiLoading = document.getElementById('ai-loading');
const loadingEl = document.getElementById('ai-loading'); const aiError = document.getElementById('ai-error');
const errorEl = document.getElementById('ai-error'); const aiErrorMessage = document.getElementById('ai-error-message');
const resultsEl = document.getElementById('ai-results'); const aiResults = document.getElementById('ai-results');
const newQueryBtn = document.getElementById('new-query-btn');
// Character counter let currentRecommendation = null;
if (queryInput && charCount) {
queryInput.addEventListener('input', () => { if (!aiInput || !aiSubmitBtn || !aiLoading || !aiError || !aiResults) {
charCount.textContent = queryInput.value.length; console.error('AI interface elements not found');
}); return;
} }
// Submit query // Character counter for input
if (submitBtn) { const updateCharacterCount = () => {
submitBtn.addEventListener('click', async () => { const length = aiInput.value.length;
const query = queryInput?.value?.trim(); const maxLength = 2000;
// Find or create character counter
let counter = document.getElementById('ai-char-counter');
if (!counter) {
counter = document.createElement('div');
counter.id = 'ai-char-counter';
counter.style.cssText = 'font-size: 0.75rem; color: var(--color-text-secondary); text-align: right; margin-top: 0.25rem;';
aiInput.parentNode.insertBefore(counter, aiInput.nextSibling);
}
counter.textContent = `${length}/${maxLength}`;
if (length > maxLength * 0.9) {
counter.style.color = 'var(--color-warning)';
} else {
counter.style.color = 'var(--color-text-secondary)';
}
};
aiInput.addEventListener('input', updateCharacterCount);
updateCharacterCount(); // Initial count
// Submit handler
const handleSubmit = async () => {
const query = aiInput.value.trim();
if (!query) { if (!query) {
alert('Bitte geben Sie eine Beschreibung Ihres Ermittlungsszenarios ein.'); alert('Bitte geben Sie eine Beschreibung Ihres Szenarios ein.');
return; return;
} }
// Check authentication if (query.length < 10) {
const authStatus = await checkAuthentication(); alert('Bitte geben Sie eine detailliertere Beschreibung ein (mindestens 10 Zeichen).');
if (authStatus.authRequired && !authStatus.authenticated) {
// Redirect to login only if authentication is required
window.location.href = `/api/auth/login?returnTo=${encodeURIComponent(window.location.pathname)}`;
return; return;
} }
// Show loading state // Hide previous results and errors
showLoadingState(); aiResults.style.display = 'none';
aiError.style.display = 'none';
aiLoading.style.display = 'block';
// Disable submit button
aiSubmitBtn.disabled = true;
aiSubmitBtn.textContent = 'Generiere Empfehlungen...';
try { try {
const response = await fetch('/api/ai/query', { const response = await fetch('/api/ai/query', {
@ -433,166 +159,223 @@ document.addEventListener('DOMContentLoaded', () => {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ query }), body: JSON.stringify({ query })
}); });
const data = await response.json(); const data = await response.json();
if (data.success) { if (!response.ok) {
showResults(data.recommendation); throw new Error(data.error || `HTTP ${response.status}`);
} else {
showError(data.error || 'Unbekannter Fehler');
} }
if (!data.success) {
throw new Error(data.error || 'Unknown error');
}
// Store recommendation for restoration
currentRecommendation = data.recommendation;
// Display results
displayResults(data.recommendation, query);
aiLoading.style.display = 'none';
aiResults.style.display = 'block';
} catch (error) { } catch (error) {
console.error('AI query failed:', error); console.error('AI query failed:', error);
showError('Verbindungsfehler. Bitte versuchen Sie es erneut.'); aiLoading.style.display = 'none';
aiError.style.display = 'block';
// Show user-friendly error messages
if (error.message.includes('429')) {
aiErrorMessage.textContent = 'Zu viele Anfragen. Bitte warten Sie einen Moment und versuchen Sie es erneut.';
} else if (error.message.includes('401')) {
aiErrorMessage.textContent = 'Authentifizierung erforderlich. Bitte melden Sie sich an.';
} else if (error.message.includes('503')) {
aiErrorMessage.textContent = 'KI-Service vorübergehend nicht verfügbar. Bitte versuchen Sie es später erneut.';
} else {
aiErrorMessage.textContent = `Fehler: ${error.message}`;
}
} finally {
// Re-enable submit button
aiSubmitBtn.disabled = false;
aiSubmitBtn.innerHTML = `
<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"/>
<path d="M12 17h.01"/>
</svg>
Empfehlungen generieren
`;
}
};
// Event listeners
aiSubmitBtn.addEventListener('click', handleSubmit);
aiInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
handleSubmit();
} }
}); });
}
// Clear button // Function to restore AI results when switching back to AI view
if (clearBtn) { window.restoreAIResults = () => {
clearBtn.addEventListener('click', () => { if (currentRecommendation) {
queryInput.value = ''; aiResults.style.display = 'block';
charCount.textContent = '0'; aiLoading.style.display = 'none';
hideAllStates(); aiError.style.display = 'none';
// Clear session results }
window.aiSessionResults = null; };
function displayResults(recommendation, originalQuery) {
// Group tools by phase
const toolsByPhase = {};
const phaseOrder = ['data-collection', 'examination', 'analysis', 'reporting'];
const phaseNames = {
'data-collection': 'Datensammlung',
'examination': 'Auswertung',
'analysis': 'Analyse',
'reporting': 'Bericht & Präsentation'
};
// Initialize phases
phaseOrder.forEach(phase => {
toolsByPhase[phase] = [];
});
// Group recommended tools by phase
recommendation.recommended_tools?.forEach(recTool => {
if (toolsByPhase[recTool.phase]) {
// Find full tool data
const fullTool = tools.find(t => t.name === recTool.name);
if (fullTool) {
toolsByPhase[recTool.phase].push({
...fullTool,
recommendation: recTool
}); });
} }
}
// New query button
if (newQueryBtn) {
newQueryBtn.addEventListener('click', () => {
hideAllStates();
// Clear session results
window.aiSessionResults = null;
queryInput?.focus();
}); });
}
// Function to restore previous results when switching back to AI view const resultsHTML = `
function restoreSessionResults() { <div class="workflow-container">
if (window.aiSessionResults) { <div style="text-align: center; margin-bottom: 2rem; padding: 1.5rem; background: linear-gradient(135deg, var(--color-accent) 0%, var(--color-primary) 100%); color: white; border-radius: 0.75rem;">
showResults(window.aiSessionResults); <h3 style="margin: 0 0 0.75rem 0; font-size: 1.5rem;">Empfohlener DFIR-Workflow</h3>
} <p style="margin: 0; opacity: 0.9; line-height: 1.5;">
} Basierend auf Ihrer Anfrage: "<em>${originalQuery.slice(0, 100)}${originalQuery.length > 100 ? '...' : ''}</em>"
</p>
</div>
// Make restore function globally accessible ${recommendation.scenario_analysis ? `
window.restoreAIResults = restoreSessionResults; <div class="card" style="margin-bottom: 2rem; border-left: 4px solid var(--color-primary);">
<h4 style="margin: 0 0 1rem 0; color: var(--color-primary); display: flex; align-items: center; gap: 0.5rem;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 20h9"/>
<path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
</svg>
Szenario-Analyse
</h4>
<p style="margin: 0; line-height: 1.6; color: var(--color-text);">${recommendation.scenario_analysis}</p>
</div>
` : ''}
// State management functions ${phaseOrder.map((phase, index) => {
function showLoadingState() { const phaseTools = toolsByPhase[phase];
hideAllStates(); if (phaseTools.length === 0) return '';
loadingEl.style.display = 'block';
submitBtn.disabled = true;
}
function showError(message) {
hideAllStates();
errorEl.style.display = 'block';
document.getElementById('ai-error-message').textContent = message;
submitBtn.disabled = false;
}
function showResults(recommendation) {
hideAllStates();
populateResults(recommendation);
// Save results to session
window.aiSessionResults = recommendation;
resultsEl.style.display = 'block';
submitBtn.disabled = false;
}
function hideAllStates() {
loadingEl.style.display = 'none';
errorEl.style.display = 'none';
resultsEl.style.display = 'none';
}
// Populate results
function populateResults(recommendation) {
// Scenario analysis
document.getElementById('scenario-analysis').textContent = recommendation.scenario_analysis || 'Keine Analyse verfügbar.';
// Workflow suggestion
document.getElementById('workflow-suggestion').textContent = recommendation.workflow_suggestion || 'Keine Workflow-Empfehlung verfügbar.';
// Additional notes
document.getElementById('additional-notes').textContent = recommendation.additional_notes || 'Keine zusätzlichen Hinweise verfügbar.';
// Populate phase tools
populatePhaseTools(recommendation.recommended_tools || []);
}
// Populate tools by phase
function populatePhaseTools(recommendedTools) {
const phases = ['data-collection', 'examination', 'analysis', 'reporting'];
phases.forEach(phaseId => {
const container = document.getElementById(`phase-tools-${phaseId}`);
if (!container) return;
const phaseTools = recommendedTools.filter(tool => tool.phase === phaseId);
if (phaseTools.length === 0) {
container.innerHTML = '<p class="text-muted" style="font-size: 0.875rem; margin: 0; font-style: italic; grid-column: 1 / -1; text-align: center; padding: 2rem;">Keine spezifischen Tools für diese Phase empfohlen.</p>';
return;
}
// Sort by priority: high -> medium -> low
const priorityOrder = { 'high': 1, 'medium': 2, 'low': 3 };
phaseTools.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
container.innerHTML = phaseTools.map(recTool => {
const toolData = window.aiToolsData.find(t => t.name === recTool.name);
if (!toolData) return '';
const hasValidProjectUrl = toolData.projectUrl && toolData.projectUrl.trim() !== '';
const isOSS = toolData.license !== 'Proprietary';
const hasKnowledgebase = toolData.knowledgebase === true;
const cardClass = hasValidProjectUrl ? 'hosted' : (isOSS ? 'oss' : '');
return ` return `
<div class="tool-recommendation ${cardClass}" onclick="window.showToolDetails('${toolData.name}')"> <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 => {
const hasValidProjectUrl = tool.projectUrl !== undefined &&
tool.projectUrl !== null &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
const priorityColors = {
high: 'var(--color-error)',
medium: 'var(--color-warning)',
low: 'var(--color-accent)'
};
return `
<div class="tool-recommendation ${hasValidProjectUrl ? 'hosted' : (tool.license !== 'Proprietary' ? 'oss' : '')}"
onclick="window.showToolDetails('${tool.name}')">
<div class="tool-rec-header"> <div class="tool-rec-header">
<h5 class="tool-rec-name">${toolData.name}</h5> <h4 class="tool-rec-name">${tool.name}</h4>
<span class="tool-rec-priority ${recTool.priority}">${recTool.priority.toUpperCase()}</span> <span class="tool-rec-priority ${tool.recommendation.priority}"
style="background-color: ${priorityColors[tool.recommendation.priority]};">
${tool.recommendation.priority}
</span>
</div> </div>
<div class="tool-rec-justification"> <div class="tool-rec-justification">
"${recTool.justification}" "${tool.recommendation.justification}"
</div> </div>
<div style="margin-top: auto;">
<div class="tool-rec-metadata"> <div class="tool-rec-metadata">
<div style="display: flex; align-items: center; gap: 0.25rem; margin-bottom: 0.5rem;"> <div style="display: flex; flex-wrap: wrap; gap: 0.25rem; margin-bottom: 0.5rem;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="9" y1="9" x2="15" y2="9"></line>
<line x1="9" y1="15" x2="15" y2="15"></line>
</svg>
<span>${(toolData.platforms || []).join(', ')}</span>
</div>
<div style="display: flex; align-items: center; gap: 0.25rem; margin-bottom: 0.5rem;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<path d="M12 6v6l4 2"></path>
</svg>
<span>${toolData.skillLevel}</span>
</div>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
${hasValidProjectUrl ? '<span class="badge badge-primary">Self-Hosted</span>' : ''} ${hasValidProjectUrl ? '<span class="badge badge-primary">Self-Hosted</span>' : ''}
${isOSS ? '<span class="badge badge-success">Open Source</span>' : ''} ${tool.license !== 'Proprietary' ? '<span class="badge badge-success">Open Source</span>' : ''}
${hasKnowledgebase ? '<span class="badge badge-error">Infos 📖</span>' : ''} <span class="badge" style="background-color: var(--color-bg-tertiary); color: var(--color-text);">${tool.skillLevel}</span>
</div> </div>
<div style="font-size: 0.8125rem; color: var(--color-text-secondary);">
${tool.platforms.join(', ')} • ${tool.license}
</div> </div>
</div> </div>
</div> </div>
`; `;
}).join(''); }).join('')}
}); </div>
</div>
</div>
${index < phaseOrder.length - 1 ? `
<div class="workflow-arrow">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"/>
<polyline points="19,12 12,19 5,12"/>
</svg>
</div>
` : ''}
</div>
`;
}).join('')}
${recommendation.workflow_suggestion ? `
<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); display: flex; align-items: center; gap: 0.5rem;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<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
</h4>
<p style="margin: 0; line-height: 1.6; color: var(--color-text);">${recommendation.workflow_suggestion}</p>
</div>
` : ''}
${recommendation.additional_notes ? `
<div class="card" style="margin-top: 1rem; background-color: var(--color-warning); color: white;">
<h4 style="margin: 0 0 1rem 0; display: flex; align-items: center; gap: 0.5rem;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<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>
Wichtige Hinweise
</h4>
<p style="margin: 0; line-height: 1.6;">${recommendation.additional_notes}</p>
</div>
` : ''}
</div>
`;
aiResults.innerHTML = resultsHTML;
} }
}); });
</script> </script>

View File

@ -156,7 +156,7 @@ const tools = data.tools;
switchToView('ai'); switchToView('ai');
} }
// Function to switch between different views // Function to switch between different views
function switchToView(view) { function switchToView(view) {
// Hide all views first (using non-null assertions since we've already checked) // Hide all views first (using non-null assertions since we've already checked)
toolsGrid!.style.display = 'none'; toolsGrid!.style.display = 'none';
@ -176,6 +176,43 @@ const tools = data.tools;
aiInterface!.style.display = 'block'; aiInterface!.style.display = 'block';
// Keep filters visible in AI mode for view switching // Keep filters visible in AI mode for view switching
filtersSection!.style.display = 'block'; filtersSection!.style.display = 'block';
// Hide filter controls in AI mode - AGGRESSIVE APPROACH
const domainPhaseContainer = document.querySelector('.domain-phase-container') as HTMLElement;
const searchInput = document.getElementById('search-tools') as HTMLElement;
const tagCloud = document.querySelector('.tag-cloud') as HTMLElement;
// Hide all checkbox wrappers
const checkboxWrappers = document.querySelectorAll('.checkbox-wrapper');
// Hide the "Nach Tags filtern" header and button
const tagHeader = document.querySelector('.tag-header') as HTMLElement;
// Hide any elements containing "Proprietäre Software" or "Nach Tags filtern"
const filterLabels = document.querySelectorAll('label, .tag-header, h4, h3');
// Hide ALL input elements in the filters section (more aggressive)
const allInputs = filtersSection!.querySelectorAll('input, select, textarea');
if (domainPhaseContainer) domainPhaseContainer.style.display = 'none';
if (searchInput) searchInput.style.display = 'none';
if (tagCloud) tagCloud.style.display = 'none';
if (tagHeader) tagHeader.style.display = 'none';
// Hide ALL inputs in the filters section
allInputs.forEach(input => {
(input as HTMLElement).style.display = 'none';
});
checkboxWrappers.forEach(wrapper => {
(wrapper as HTMLElement).style.display = 'none';
});
// Hide specific filter section elements by text content
filterLabels.forEach(element => {
const text = element.textContent?.toLowerCase() || '';
if (text.includes('proprietäre') || text.includes('tags filtern') || text.includes('nach tags') || text.includes('suchen') || text.includes('search')) {
(element as HTMLElement).style.display = 'none';
}
});
// Restore previous AI results if they exist // Restore previous AI results if they exist
if ((window as any).restoreAIResults) { if ((window as any).restoreAIResults) {
(window as any).restoreAIResults(); (window as any).restoreAIResults();
@ -189,10 +226,72 @@ const tools = data.tools;
case 'matrix': case 'matrix':
matrixContainer!.style.display = 'block'; matrixContainer!.style.display = 'block';
filtersSection!.style.display = 'block'; filtersSection!.style.display = 'block';
// Show filter controls in matrix mode
const domainPhaseContainerMatrix = document.querySelector('.domain-phase-container') as HTMLElement;
const searchInputMatrix = document.getElementById('search-tools') as HTMLElement;
const tagCloudMatrix = document.querySelector('.tag-cloud') as HTMLElement;
const checkboxWrappersMatrix = document.querySelectorAll('.checkbox-wrapper');
const tagHeaderMatrix = document.querySelector('.tag-header') as HTMLElement;
const filterLabelsMatrix = document.querySelectorAll('label, .tag-header, h4, h3');
const allInputsMatrix = filtersSection!.querySelectorAll('input, select, textarea');
if (domainPhaseContainerMatrix) domainPhaseContainerMatrix.style.display = 'grid';
if (searchInputMatrix) searchInputMatrix.style.display = 'block';
if (tagCloudMatrix) tagCloudMatrix.style.display = 'flex';
if (tagHeaderMatrix) tagHeaderMatrix.style.display = 'flex';
// Restore ALL inputs in the filters section
allInputsMatrix.forEach(input => {
(input as HTMLElement).style.display = 'block';
});
checkboxWrappersMatrix.forEach(wrapper => {
(wrapper as HTMLElement).style.display = 'flex';
});
// Restore filter section elements
filterLabelsMatrix.forEach(element => {
const text = element.textContent?.toLowerCase() || '';
if (text.includes('proprietäre') || text.includes('tags filtern') || text.includes('nach tags') || text.includes('suchen') || text.includes('search')) {
(element as HTMLElement).style.display = 'block';
}
});
break; break;
default: // grid default: // grid
toolsGrid!.style.display = 'block'; toolsGrid!.style.display = 'block';
filtersSection!.style.display = 'block'; filtersSection!.style.display = 'block';
// Show filter controls in grid mode
const domainPhaseContainerGrid = document.querySelector('.domain-phase-container') as HTMLElement;
const searchInputGrid = document.getElementById('search-tools') as HTMLElement;
const tagCloudGrid = document.querySelector('.tag-cloud') as HTMLElement;
const checkboxWrappersGrid = document.querySelectorAll('.checkbox-wrapper');
const tagHeaderGrid = document.querySelector('.tag-header') as HTMLElement;
const filterLabelsGrid = document.querySelectorAll('label, .tag-header, h4, h3');
const allInputsGrid = filtersSection!.querySelectorAll('input, select, textarea');
if (domainPhaseContainerGrid) domainPhaseContainerGrid.style.display = 'grid';
if (searchInputGrid) searchInputGrid.style.display = 'block';
if (tagCloudGrid) tagCloudGrid.style.display = 'flex';
if (tagHeaderGrid) tagHeaderGrid.style.display = 'flex';
// Restore ALL inputs in the filters section
allInputsGrid.forEach(input => {
(input as HTMLElement).style.display = 'block';
});
checkboxWrappersGrid.forEach(wrapper => {
(wrapper as HTMLElement).style.display = 'flex';
});
// Restore filter section elements
filterLabelsGrid.forEach(element => {
const text = element.textContent?.toLowerCase() || '';
if (text.includes('proprietäre') || text.includes('tags filtern') || text.includes('nach tags') || text.includes('suchen') || text.includes('search')) {
(element as HTMLElement).style.display = 'block';
}
});
break; break;
} }