new AI features

This commit is contained in:
overcuriousity 2025-07-19 16:34:42 +02:00
parent fbd95726a0
commit 2d01db907a
3 changed files with 642 additions and 50 deletions

View File

@ -1,13 +1,12 @@
--- ---
import { getToolsData } from '../utils/dataService.js'; import { getToolsData } from '../utils/dataService.js';
// Load tools data for validation // Load tools data for validation
const data = await getToolsData(); const data = await getToolsData();
const tools = data.tools; const tools = data.tools;
const phases = data.phases; const phases = data.phases;
const domainAgnosticSoftware = data['domain-agnostic-software'] || []; // Add this line const domainAgnosticSoftware = data['domain-agnostic-software'] || [];
--- ---
<!-- AI Query Interface --> <!-- AI Query Interface -->
@ -21,13 +20,35 @@ const domainAgnosticSoftware = data['domain-agnostic-software'] || []; // Add th
</svg> </svg>
KI-gestützte Tool-Empfehlungen KI-gestützte Tool-Empfehlungen
</h2> </h2>
<p class="text-muted" style="max-width: 700px; margin: 0 auto; line-height: 1.6;"> <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 Tool-Empfehlungen Beschreiben Sie Ihr forensisches Szenario und erhalten Sie maßgeschneiderte Tool-Empfehlungen
basierend auf bewährten DFIR-Workflows und der verfügbaren Software-Datenbank. 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;"> <div class="ai-input-container" style="max-width: 800px; margin: 0 auto;">
<!-- Mode Toggle -->
<div class="ai-mode-toggle" style="display: flex; align-items: center; justify-content: center; gap: 1rem; margin-bottom: 1.5rem; padding: 1rem; background-color: var(--color-bg-secondary); border-radius: 0.75rem; border: 1px solid var(--color-border);">
<span id="workflow-label" class="toggle-label active" style="font-weight: 500; color: var(--color-primary); cursor: pointer; transition: var(--transition-fast);">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem; vertical-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" style="position: relative; width: 50px; height: 24px; background-color: var(--color-primary); border-radius: 12px; cursor: pointer; transition: var(--transition-fast);">
<div class="toggle-slider" style="position: absolute; top: 2px; left: 2px; width: 20px; height: 20px; background-color: white; border-radius: 50%; transition: var(--transition-fast); transform: translateX(0);"></div>
</div>
<span id="tool-label" class="toggle-label" style="font-weight: 500; color: var(--color-text-secondary); cursor: pointer; transition: var(--transition-fast);">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem; vertical-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>
Spezifisches Tool
</span>
</div>
<textarea <textarea
id="ai-query-input" id="ai-query-input"
placeholder="Beschreiben Sie Ihr forensisches Szenario... z.B. 'Verdacht auf Ransomware-Angriff auf Windows-Domänencontroller mit verschlüsselten Dateien und verdächtigen Netzwerkverbindungen'" placeholder="Beschreiben Sie Ihr forensisches Szenario... z.B. 'Verdacht auf Ransomware-Angriff auf Windows-Domänencontroller mit verschlüsselten Dateien und verdächtigen Netzwerkverbindungen'"
@ -55,7 +76,7 @@ const domainAgnosticSoftware = data['domain-agnostic-software'] || []; // Add th
<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="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"/> <path d="M12 17h.01"/>
</svg> </svg>
Empfehlungen generieren <span id="submit-btn-text">Empfehlungen generieren</span>
</button> </button>
</div> </div>
</div> </div>
@ -70,7 +91,7 @@ const domainAgnosticSoftware = data['domain-agnostic-software'] || []; // Add th
<path d="M12 17h.01"/> <path d="M12 17h.01"/>
</svg> </svg>
</div> </div>
<p style="color: var(--color-text-secondary);">Analysiere Szenario und generiere Empfehlungen...</p> <p id="loading-text" style="color: var(--color-text-secondary);">Analysiere Szenario und generiere Empfehlungen...</p>
</div> </div>
<!-- Error State --> <!-- Error State -->
@ -98,24 +119,108 @@ document.addEventListener('DOMContentLoaded', () => {
const aiInterface = document.getElementById('ai-interface'); const aiInterface = document.getElementById('ai-interface');
const aiInput = document.getElementById('ai-query-input'); const aiInput = document.getElementById('ai-query-input');
const aiSubmitBtn = document.getElementById('ai-submit-btn'); const aiSubmitBtn = document.getElementById('ai-submit-btn');
const submitBtnText = document.getElementById('submit-btn-text');
const aiLoading = document.getElementById('ai-loading'); const aiLoading = document.getElementById('ai-loading');
const loadingText = document.getElementById('loading-text');
const aiError = document.getElementById('ai-error'); const aiError = document.getElementById('ai-error');
const aiErrorMessage = document.getElementById('ai-error-message'); const aiErrorMessage = document.getElementById('ai-error-message');
const aiResults = document.getElementById('ai-results'); const aiResults = document.getElementById('ai-results');
const aiDescription = document.getElementById('ai-description');
// Toggle elements
const toggleSwitch = document.querySelector('.toggle-switch');
const toggleSlider = document.querySelector('.toggle-slider');
const workflowLabel = document.getElementById('workflow-label');
const toolLabel = document.getElementById('tool-label');
let currentRecommendation = null; let currentRecommendation = null;
let currentMode = 'workflow'; // 'workflow' or 'tool'
if (!aiInput || !aiSubmitBtn || !aiLoading || !aiError || !aiResults) { if (!aiInput || !aiSubmitBtn || !aiLoading || !aiError || !aiResults) {
console.error('AI interface elements not found'); console.error('AI interface elements not found');
return; return;
} }
// Mode content configuration
const modeConfig = {
workflow: {
placeholder: "Beschreiben Sie Ihr forensisches Szenario... z.B. 'Verdacht auf Ransomware-Angriff auf Windows-Domänencontroller mit verschlüsselten Dateien und verdächtigen Netzwerkverbindungen'",
description: "Beschreiben Sie Ihr forensisches Szenario und erhalten Sie maßgeschneiderte Tool-Empfehlungen basierend auf bewährten DFIR-Workflows und der verfügbaren Software-Datenbank.",
submitText: "Empfehlungen generieren",
loadingText: "Analysiere Szenario und generiere Empfehlungen..."
},
tool: {
placeholder: "Beschreiben Sie Ihr spezifisches Problem oder Ihre Anforderung... z.B. 'Ich benötige ein Tool zur Analyse von Android-Backups mit WhatsApp-Nachrichten und GPS-Daten'",
description: "Beschreiben Sie Ihr spezifisches Problem oder Ihre Anforderung und erhalten Sie 1-3 gezielt passende Tool-Empfehlungen mit detaillierten Erklärungen zur optimalen Anwendung.",
submitText: "Tool-Empfehlungen finden",
loadingText: "Analysiere Anforderungen und suche passende Tools..."
}
};
// Update UI based on current mode
function updateModeUI() {
const config = modeConfig[currentMode];
// Update placeholder
aiInput.placeholder = config.placeholder;
// Update description
aiDescription.textContent = config.description;
// Update submit button text
submitBtnText.textContent = config.submitText;
// Update loading text
loadingText.textContent = config.loadingText;
// Update toggle visual state
if (currentMode === 'workflow') {
toggleSlider.style.transform = 'translateX(0)';
toggleSwitch.style.backgroundColor = 'var(--color-primary)';
workflowLabel.style.color = 'var(--color-primary)';
workflowLabel.classList.add('active');
toolLabel.style.color = 'var(--color-text-secondary)';
toolLabel.classList.remove('active');
} else {
toggleSlider.style.transform = 'translateX(26px)';
toggleSwitch.style.backgroundColor = 'var(--color-accent)';
toolLabel.style.color = 'var(--color-accent)';
toolLabel.classList.add('active');
workflowLabel.style.color = 'var(--color-text-secondary)';
workflowLabel.classList.remove('active');
}
// Clear previous results when switching modes
aiResults.style.display = 'none';
aiError.style.display = 'none';
currentRecommendation = null;
}
// Toggle mode handlers
function switchToMode(mode) {
if (currentMode !== mode) {
currentMode = mode;
updateModeUI();
}
}
toggleSwitch.addEventListener('click', () => {
switchToMode(currentMode === 'workflow' ? 'tool' : 'workflow');
});
workflowLabel.addEventListener('click', () => {
switchToMode('workflow');
});
toolLabel.addEventListener('click', () => {
switchToMode('tool');
});
// Character counter for input // Character counter for input
const updateCharacterCount = () => { const updateCharacterCount = () => {
const length = aiInput.value.length; const length = aiInput.value.length;
const maxLength = 2000; const maxLength = 2000;
// Find or create character counter
let counter = document.getElementById('ai-char-counter'); let counter = document.getElementById('ai-char-counter');
if (!counter) { if (!counter) {
counter = document.createElement('div'); counter = document.createElement('div');
@ -133,14 +238,14 @@ document.addEventListener('DOMContentLoaded', () => {
}; };
aiInput.addEventListener('input', updateCharacterCount); aiInput.addEventListener('input', updateCharacterCount);
updateCharacterCount(); // Initial count updateCharacterCount();
// Submit handler // Submit handler
const handleSubmit = async () => { const handleSubmit = async () => {
const query = aiInput.value.trim(); const query = aiInput.value.trim();
if (!query) { if (!query) {
alert('Bitte geben Sie eine Beschreibung Ihres Szenarios ein.'); alert('Bitte geben Sie eine Beschreibung ein.');
return; return;
} }
@ -156,7 +261,7 @@ document.addEventListener('DOMContentLoaded', () => {
// Disable submit button // Disable submit button
aiSubmitBtn.disabled = true; aiSubmitBtn.disabled = true;
aiSubmitBtn.textContent = 'Generiere Empfehlungen...'; submitBtnText.textContent = currentMode === 'workflow' ? 'Generiere Empfehlungen...' : 'Suche passende Tools...';
try { try {
const response = await fetch('/api/ai/query', { const response = await fetch('/api/ai/query', {
@ -164,7 +269,10 @@ document.addEventListener('DOMContentLoaded', () => {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ query }) body: JSON.stringify({
query,
mode: currentMode
})
}); });
const data = await response.json(); const data = await response.json();
@ -180,8 +288,13 @@ document.addEventListener('DOMContentLoaded', () => {
// Store recommendation for restoration // Store recommendation for restoration
currentRecommendation = data.recommendation; currentRecommendation = data.recommendation;
// Display results // Display results based on mode
displayResults(data.recommendation, query); if (currentMode === 'workflow') {
displayWorkflowResults(data.recommendation, query);
} else {
displayToolResults(data.recommendation, query);
}
aiLoading.style.display = 'none'; aiLoading.style.display = 'none';
aiResults.style.display = 'block'; aiResults.style.display = 'block';
@ -203,14 +316,8 @@ document.addEventListener('DOMContentLoaded', () => {
} finally { } finally {
// Re-enable submit button // Re-enable submit button
aiSubmitBtn.disabled = false; aiSubmitBtn.disabled = false;
aiSubmitBtn.innerHTML = ` const config = modeConfig[currentMode];
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;"> submitBtnText.textContent = config.submitText;
<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
`;
} }
}; };
@ -235,11 +342,9 @@ document.addEventListener('DOMContentLoaded', () => {
// Helper function to format workflow suggestions as proper lists // Helper function to format workflow suggestions as proper lists
function formatWorkflowSuggestion(text) { function formatWorkflowSuggestion(text) {
// Check if text contains numbered list items (1., 2., 3., etc.)
const numberedListPattern = /(\d+\.\s)/g; const numberedListPattern = /(\d+\.\s)/g;
if (numberedListPattern.test(text)) { if (numberedListPattern.test(text)) {
// Split by numbered items and clean up
const items = text.split(/\d+\.\s/).filter(item => item.trim().length > 0); const items = text.split(/\d+\.\s/).filter(item => item.trim().length > 0);
if (items.length > 1) { if (items.length > 1) {
@ -251,7 +356,6 @@ document.addEventListener('DOMContentLoaded', () => {
} }
} }
// Check for bullet points (-, *, •)
const bulletPattern = /^[\s]*[-\*•]\s/gm; const bulletPattern = /^[\s]*[-\*•]\s/gm;
if (bulletPattern.test(text)) { if (bulletPattern.test(text)) {
const items = text.split(/^[\s]*[-\*•]\s/gm).filter(item => item.trim().length > 0); const items = text.split(/^[\s]*[-\*•]\s/gm).filter(item => item.trim().length > 0);
@ -265,7 +369,6 @@ document.addEventListener('DOMContentLoaded', () => {
} }
} }
// Check for line breaks that might indicate separate points
if (text.includes('\n')) { if (text.includes('\n')) {
const lines = text.split('\n').filter(line => line.trim().length > 0); const lines = text.split('\n').filter(line => line.trim().length > 0);
if (lines.length > 1) { if (lines.length > 1) {
@ -277,11 +380,11 @@ document.addEventListener('DOMContentLoaded', () => {
} }
} }
// Fallback to regular paragraph if no list format detected
return `<p style="margin: 0; line-height: 1.6; color: var(--color-text);">${text}</p>`; return `<p style="margin: 0; line-height: 1.6; color: var(--color-text);">${text}</p>`;
} }
function displayResults(recommendation, originalQuery) { // Display results for workflow mode (existing functionality)
function displayWorkflowResults(recommendation, originalQuery) {
// Group tools by phase // Group tools by phase
const toolsByPhase = {}; const toolsByPhase = {};
@ -299,7 +402,6 @@ document.addEventListener('DOMContentLoaded', () => {
// Group recommended tools by phase // Group recommended tools by phase
recommendation.recommended_tools?.forEach(recTool => { recommendation.recommended_tools?.forEach(recTool => {
if (toolsByPhase[recTool.phase]) { if (toolsByPhase[recTool.phase]) {
// Find full tool data
const fullTool = tools.find(t => t.name === recTool.name); const fullTool = tools.find(t => t.name === recTool.name);
if (fullTool) { if (fullTool) {
toolsByPhase[recTool.phase].push({ toolsByPhase[recTool.phase].push({
@ -428,11 +530,229 @@ document.addEventListener('DOMContentLoaded', () => {
` : ''} ` : ''}
</div> </div>
`; `;
aiResults.innerHTML = ''; // Clear previous results first
aiResults.innerHTML = '';
const tempDiv = document.createElement('div'); const tempDiv = document.createElement('div');
tempDiv.innerHTML = resultsHTML; tempDiv.innerHTML = resultsHTML;
// Sanitize any dynamic content before inserting
aiResults.appendChild(tempDiv); aiResults.appendChild(tempDiv);
} }
// Display results for tool-specific mode (new functionality)
function displayToolResults(recommendation, originalQuery) {
function getSuitabilityText(score) {
const suitabilityTexts = {
high: 'GUT GEEIGNET',
medium: 'GEEIGNET',
low: 'VIELLEICHT GEEIGNET'
};
return suitabilityTexts[score] || 'GEEIGNET';
}
// Helper function to get phase names for a tool
function getToolPhases(tool) {
if (!tool.phases || tool.phases.length === 0) return '';
const phaseNames = phases.reduce((acc, phase) => {
acc[phase.id] = phase.name;
return acc;
}, {});
const domainAgnosticNames = domainAgnosticSoftware.reduce((acc, section) => {
acc[section.id] = section.name;
return acc;
}, {});
const allPhaseNames = { ...phaseNames, ...domainAgnosticNames };
return tool.phases.map(phaseId => allPhaseNames[phaseId]).filter(Boolean).join(', ');
}
const resultsHTML = `
<div class="tool-results-container">
<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;">
<h3 style="margin: 0 0 0.75rem 0; font-size: 1.5rem;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem; vertical-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>
Passende Tool-Empfehlungen
</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>
${recommendation.problem_analysis ? `
<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">
<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>
Problem-Analyse
</h4>
${formatWorkflowSuggestion(recommendation.problem_analysis)}
</div>
` : ''}
<div class="tool-recommendations-grid" style="display: grid; gap: 1.5rem;">
${recommendation.recommended_tools?.map((toolRec, index) => {
const fullTool = tools.find(t => t.name === toolRec.name);
if (!fullTool) return '';
const hasValidProjectUrl = fullTool.projectUrl !== undefined &&
fullTool.projectUrl !== null &&
fullTool.projectUrl !== "" &&
fullTool.projectUrl.trim() !== "";
const suitabilityColors = {
high: 'var(--color-accent)',
medium: 'var(--color-warning)',
low: 'var(--color-text-secondary)'
};
const rankColors = {
1: 'var(--color-accent)',
2: 'var(--color-primary)',
3: 'var(--color-warning)'
};
return `
<div class="tool-detailed-recommendation card ${hasValidProjectUrl ? 'card-hosted' : (fullTool.license !== 'Proprietary' ? 'card-oss' : '')}"
style="cursor: pointer; position: relative;"
onclick="window.showToolDetails('${fullTool.name}')">
<div class="tool-rank-badge" style="position: absolute; top: -8px; right: -8px; width: 32px; height: 32px; background-color: ${rankColors[toolRec.rank]}; color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 1.125rem; box-shadow: var(--shadow-md);">
${toolRec.rank}
</div>
<div class="tool-rec-header" style="margin-bottom: 1rem;">
<h3 style="margin: 0 0 0.5rem 0; color: var(--color-text); font-size: 1.25rem;">${fullTool.name}</h3>
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center;">
<span class="badge" style="background-color: ${suitabilityColors[toolRec.suitability_score]}; color: white; font-size: 0.8125rem;">
${getSuitabilityText(toolRec.suitability_score)}
</span>
${hasValidProjectUrl ? '<span class="badge badge-primary">Self-Hosted</span>' : ''}
${fullTool.license !== 'Proprietary' ? '<span class="badge badge-success">Open Source</span>' : ''}
${fullTool.knowledgebase === true ? '<span class="badge badge-error">📖</span>' : ''}
</div>
</div>
<div>
${(() => {
const toolPhases = getToolPhases(fullTool);
return toolPhases ? `
<div style="margin-top: 0.75rem;">
<div style="display: flex; align-items: center; gap: 0.5rem; font-size: 0.8125rem; color: var(--color-text-secondary);">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="flex-shrink: 0;">
<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>
<span><strong>Anwendbare Phasen:</strong> ${toolPhases}</span>
</div>
</div>
` : '';
})()}
</div>
<div class="tool-detailed-explanation" style="margin-bottom: 1.5rem;">
<h4 style="margin: 0.8rem 0 0.75rem 0; color: var(--color-accent); font-size: 1rem;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem; 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>
Warum dieses Tool?
</h4>
${formatWorkflowSuggestion(toolRec.detailed_explanation)}
${toolRec.implementation_approach ? `
<h4 style="margin: 0.8rem 0 0.75rem 0; color: var(--color-primary); font-size: 1rem;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem; vertical-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>
Anwendungsansatz
</h4>
${formatWorkflowSuggestion(toolRec.implementation_approach)}
` : ''}
</div>
${(toolRec.pros && toolRec.pros.length > 0) || (toolRec.cons && toolRec.cons.length > 0) ? `
<div class="pros-cons-section" style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1.5rem;">
${toolRec.pros && toolRec.pros.length > 0 ? `
<div class="pros" 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; text-transform: uppercase;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.25rem; vertical-align: middle;">
<polyline points="20,6 9,17 4,12"/>
</svg>
Vorteile
</h5>
<ul style="margin: 0; padding-left: 1rem; font-size: 0.875rem; line-height: 1.5;">
${toolRec.pros.map(pro => `<li style="margin-bottom: 0.25rem;">${pro}</li>`).join('')}
</ul>
</div>
` : ''}
${toolRec.cons && toolRec.cons.length > 0 ? `
<div class="cons" 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; text-transform: uppercase;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.25rem; vertical-align: middle;">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
Nachteile
</h5>
<ul style="margin: 0; padding-left: 1rem; font-size: 0.875rem; line-height: 1.5;">
${toolRec.cons.map(con => `<li style="margin-bottom: 0.25rem;">${con}</li>`).join('')}
</ul>
</div>
` : ''}
</div>
` : ''}
<div class="tool-metadata" 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;">
<div><strong>Plattformen:</strong> ${fullTool.platforms.join(', ')}</div>
<div><strong>Skill Level:</strong> ${fullTool.skillLevel}</div>
<div><strong>Lizenz:</strong> ${fullTool.license}</div>
<div><strong>Typ:</strong> ${fullTool.accessType}</div>
</div>
${toolRec.alternatives ? `
<div class="alternatives" 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;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.25rem; vertical-align: middle;">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
<polyline points="3.27,6.96 12,12.01 20.73,6.96"/>
<line x1="12" y1="22.08" x2="12" y2="12"/>
</svg>
Alternative Ansätze
</h5>
${formatWorkflowSuggestion(toolRec.alternatives)}
</div>
` : ''}
</div>
`;
}).join('')}
</div>
${recommendation.additional_considerations ? `
<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); 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>
Zusätzliche Überlegungen
</h4>
${formatWorkflowSuggestion(recommendation.additional_considerations)}
</div>
` : ''}
</div>
`;
aiResults.innerHTML = '';
const tempDiv = document.createElement('div');
tempDiv.innerHTML = resultsHTML;
aiResults.appendChild(tempDiv);
}
// Initialize UI
updateModeUI();
}); });
</script> </script>

View File

@ -1,10 +1,8 @@
// src/pages/api/ai/query.ts // src/pages/api/ai/query.ts
// src/pages/api/ai/query.ts
import type { APIRoute } from 'astro'; import type { APIRoute } from 'astro';
import { getSessionFromRequest, verifySession } from '../../../utils/auth.js'; import { getSessionFromRequest, verifySession } from '../../../utils/auth.js';
import { getCompressedToolsDataForAI } from '../../../utils/dataService.js'; import { getCompressedToolsDataForAI } from '../../../utils/dataService.js';
export const prerender = false; export const prerender = false;
function getEnv(key: string): string { function getEnv(key: string): string {
@ -84,8 +82,8 @@ async function loadToolsDatabase() {
} }
} }
// Create system prompt // Create system prompt for workflow mode
function createSystemPrompt(toolsData: any): string { function createWorkflowSystemPrompt(toolsData: any): string {
const toolsList = toolsData.tools.map((tool: any) => ({ const toolsList = toolsData.tools.map((tool: any) => ({
name: tool.name, name: tool.name,
description: tool.description, description: tool.description,
@ -179,6 +177,58 @@ ANTWORT-FORMAT (strict JSON):
Antworte NUR mit validen JSON. Keine zusätzlichen Erklärungen außerhalb des JSON.`; Antworte NUR mit validen JSON. Keine zusätzlichen Erklärungen außerhalb des JSON.`;
} }
// Create system prompt for tool-specific mode
function createToolSystemPrompt(toolsData: any): string {
const toolsList = toolsData.tools.map((tool: any) => ({
name: tool.name,
description: tool.description,
domains: tool.domains,
phases: tool.phases,
platforms: tool.platforms,
skillLevel: tool.skillLevel,
license: tool.license,
tags: tool.tags,
url: tool.url,
projectUrl: tool.projectUrl
}));
return `Du bist ein DFIR (Digital Forensics and Incident Response) Experte, der bei der Auswahl spezifischer Tools für konkrete Probleme hilft.
VERFÜGBARE TOOLS DATABASE:
${JSON.stringify(toolsList, null, 2)}
WICHTIGE REGELN:
1. Analysiere das spezifische Problem/die Anforderung sorgfältig
2. Empfehle 1-3 Tools, sortiert nach Eignung (beste Empfehlung zuerst)
3. Gib detaillierte Erklärungen, WARUM und WIE jedes Tool das Problem löst
4. Berücksichtige praktische Aspekte: Skill Level, Plattformen, Verfügbarkeit
5. Deutsche Antworten für deutsche Anfragen, English for English queries
6. Gib konkrete Anwendungshinweise, nicht nur allgemeine Beschreibungen
7. Erwähne sowohl Stärken als auch Schwächen/Limitationen
8. Schlage alternative Ansätze vor, wenn sinnvoll
9. Gib grundsätzliche Hinweise, WIE das Tool konkret eingesetzt wird
ANTWORT-FORMAT (strict JSON):
{
"problem_analysis": "Detaillierte Analyse des Problems/der Anforderung",
"recommended_tools": [
{
"name": "EXAKTER Name aus der Database",
"rank": 1,
"suitability_score": "high|medium|low",
"detailed_explanation": "Detaillierte Erklärung, warum dieses Tool das Problem löst",
"implementation_approach": "Konkrete Schritte/Ansatz zur Anwendung für dieses spezifische Problem",
"pros": ["Spezifische Vorteile für diesen Anwendungsfall", "Weitere Vorteile"],
"cons": ["Potentielle Nachteile oder Limitationen", "Weitere Einschränkungen"],
"alternatives": "Alternative Ansätze oder ergänzende Tools, falls relevant"
}
],
"additional_considerations": "Wichtige Überlegungen, Voraussetzungen oder Warnungen"
}
Antworte NUR mit validen JSON. Keine zusätzlichen Erklärungen außerhalb des JSON.`;
}
export const POST: APIRoute = async ({ request }) => { export const POST: APIRoute = async ({ request }) => {
try { try {
// Check if authentication is required // Check if authentication is required
@ -216,7 +266,7 @@ export const POST: APIRoute = async ({ request }) => {
// Parse request body // Parse request body
const body = await request.json(); const body = await request.json();
const { query } = body; const { query, mode = 'workflow' } = body;
if (!query || typeof query !== 'string') { if (!query || typeof query !== 'string') {
return new Response(JSON.stringify({ error: 'Query required' }), { return new Response(JSON.stringify({ error: 'Query required' }), {
@ -225,6 +275,13 @@ export const POST: APIRoute = async ({ request }) => {
}); });
} }
if (!['workflow', 'tool'].includes(mode)) {
return new Response(JSON.stringify({ error: 'Invalid mode. Must be "workflow" or "tool"' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
// Sanitize input // Sanitize input
const sanitizedQuery = sanitizeInput(query); const sanitizedQuery = sanitizeInput(query);
if (sanitizedQuery.includes('[FILTERED]')) { if (sanitizedQuery.includes('[FILTERED]')) {
@ -237,8 +294,10 @@ export const POST: APIRoute = async ({ request }) => {
// Load tools database // Load tools database
const toolsData = await loadToolsDatabase(); const toolsData = await loadToolsDatabase();
// Create AI request // Create appropriate system prompt based on mode
const systemPrompt = createSystemPrompt(toolsData); const systemPrompt = mode === 'workflow'
? createWorkflowSystemPrompt(toolsData)
: createToolSystemPrompt(toolsData);
const aiResponse = await fetch(process.env.AI_API_ENDPOINT + '/v1/chat/completions', { const aiResponse = await fetch(process.env.AI_API_ENDPOINT + '/v1/chat/completions', {
method: 'POST', method: 'POST',
@ -247,7 +306,7 @@ export const POST: APIRoute = async ({ request }) => {
'Authorization': `Bearer ${process.env.AI_API_KEY}` 'Authorization': `Bearer ${process.env.AI_API_KEY}`
}, },
body: JSON.stringify({ body: JSON.stringify({
model: AI_MODEL, // or whatever model is available model: AI_MODEL,
messages: [ messages: [
{ {
role: 'system', role: 'system',
@ -263,7 +322,6 @@ export const POST: APIRoute = async ({ request }) => {
}) })
}); });
if (!aiResponse.ok) { if (!aiResponse.ok) {
console.error('AI API error:', await aiResponse.text()); console.error('AI API error:', await aiResponse.text());
return new Response(JSON.stringify({ error: 'AI service unavailable' }), { return new Response(JSON.stringify({ error: 'AI service unavailable' }), {
@ -295,9 +353,13 @@ export const POST: APIRoute = async ({ request }) => {
}); });
} }
// Validate tool names against database // Validate tool names against database based on mode
const validToolNames = new Set(toolsData.tools.map((t: any) => t.name)); const validToolNames = new Set(toolsData.tools.map((t: any) => t.name));
const validatedRecommendation = { let validatedRecommendation;
if (mode === 'workflow') {
// Existing validation for workflow mode
validatedRecommendation = {
...recommendation, ...recommendation,
recommended_tools: recommendation.recommended_tools?.filter((tool: any) => { recommended_tools: recommendation.recommended_tools?.filter((tool: any) => {
if (!validToolNames.has(tool.name)) { if (!validToolNames.has(tool.name)) {
@ -307,12 +369,32 @@ export const POST: APIRoute = async ({ request }) => {
return true; return true;
}) || [] }) || []
}; };
} else {
// New validation for tool mode
validatedRecommendation = {
...recommendation,
recommended_tools: recommendation.recommended_tools?.filter((tool: any) => {
if (!validToolNames.has(tool.name)) {
console.warn(`AI recommended unknown tool: ${tool.name}`);
return false;
}
return true;
}).map((tool: any, index: number) => ({
...tool,
rank: tool.rank || (index + 1), // Ensure rank is set
suitability_score: tool.suitability_score || 'medium', // Default suitability
pros: Array.isArray(tool.pros) ? tool.pros : [],
cons: Array.isArray(tool.cons) ? tool.cons : []
})) || []
};
}
// Log successful query // Log successful query
console.log(`[AI Query] User: ${userId}, Query length: ${sanitizedQuery.length}, Tools: ${validatedRecommendation.recommended_tools.length}`); console.log(`[AI Query] Mode: ${mode}, User: ${userId}, Query length: ${sanitizedQuery.length}, Tools: ${validatedRecommendation.recommended_tools.length}`);
return new Response(JSON.stringify({ return new Response(JSON.stringify({
success: true, success: true,
mode,
recommendation: validatedRecommendation, recommendation: validatedRecommendation,
query: sanitizedQuery query: sanitizedQuery
}), { }), {

View File

@ -825,6 +825,62 @@ Collaboration Section Collapse */
animation: fadeIn 0.3s ease-in; animation: fadeIn 0.3s ease-in;
} }
.ai-mode-toggle {
user-select: none;
}
.toggle-label {
display: flex;
align-items: center;
font-size: 0.875rem;
transition: var(--transition-fast);
}
.toggle-label:hover {
color: var(--color-primary) !important;
}
.toggle-label.active {
font-weight: 600;
}
.toggle-switch {
position: relative;
cursor: pointer;
transition: var(--transition-fast);
}
.toggle-switch:hover {
transform: scale(1.05);
}
.toggle-slider {
box-shadow: var(--shadow-sm);
transition: var(--transition-fast);
}
/* Dark mode adjustments for toggle */
[data-theme="dark"] .toggle-slider {
box-shadow: 0 2px 4px 0 rgb(255 255 255 / 10%);
}
[data-theme="dark"] .tool-rank-badge {
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 50%);
}
/* Focus states for accessibility */
.toggle-switch:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
border-radius: 12px;
}
.toggle-label:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
border-radius: 0.25rem;
}
.workflow-container { .workflow-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -961,6 +1017,117 @@ Collaboration Section Collapse */
color: var(--color-text-secondary); color: var(--color-text-secondary);
} }
/* Tool Results Styles */
.tool-results-container {
max-width: 1000px;
margin: 0 auto;
}
.tool-recommendations-grid {
display: grid;
gap: 1.5rem;
}
.tool-detailed-recommendation {
position: relative;
transition: var(--transition-fast);
border: 2px solid transparent;
animation: fadeInUp 0.5s ease-out;
}
.tool-detailed-recommendation:nth-child(2) {
animation-delay: 0.1s;
}
.tool-detailed-recommendation:nth-child(3) {
animation-delay: 0.2s;
}
.tool-detailed-recommendation:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
border-color: var(--color-primary);
}
.tool-detailed-recommendation.card-hosted {
background-color: var(--color-hosted-bg);
border-color: var(--color-hosted);
}
.tool-detailed-recommendation.card-hosted:hover {
border-color: var(--color-hosted);
box-shadow: 0 0 0 1px var(--color-hosted), var(--shadow-lg);
}
.tool-detailed-recommendation.card-oss {
background-color: var(--color-oss-bg);
border-color: var(--color-oss);
}
.tool-detailed-recommendation.card-oss:hover {
border-color: var(--color-oss);
}
.tool-rank-badge {
animation: fadeInUp 0.6s ease-out;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-weight: 700;
}
.tool-detailed-explanation h4 {
display: flex;
align-items: center;
font-size: 1rem;
margin-bottom: 0.75rem;
}
.pros-cons-section {
animation: fadeIn 0.4s ease-in;
}
.pros, .cons {
font-size: 0.875rem;
}
.pros ul, .cons ul {
list-style-type: none;
padding-left: 0;
}
.pros li, .cons li {
position: relative;
padding-left: 1.5rem;
margin-bottom: 0.375rem;
line-height: 1.4;
}
.pros li::before {
content: '✓';
position: absolute;
left: 0;
color: var(--color-accent);
font-weight: bold;
}
.cons li::before {
content: '⚠';
position: absolute;
left: 0;
color: var(--color-warning);
font-weight: bold;
}
.tool-metadata {
background-color: var(--color-bg-secondary);
padding: 1rem;
border-radius: 0.5rem;
border-left: 3px solid var(--color-primary);
}
.alternatives {
border-left: 3px solid var(--color-text-secondary);
}
/* Knowledgebase */ /* Knowledgebase */
.kb-entry { .kb-entry {
margin-bottom: 1rem; margin-bottom: 1rem;
@ -1138,6 +1305,29 @@ footer {
font-size: 0.8125rem; font-size: 0.8125rem;
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
} }
.ai-mode-toggle {
flex-direction: column;
gap: 0.75rem;
text-align: center;
}
.toggle-label {
font-size: 0.8125rem;
}
.toggle-switch {
align-self: center;
}
.pros-cons-section {
grid-template-columns: 1fr;
}
.tool-metadata {
grid-template-columns: 1fr;
gap: 0.5rem;
}
} }
@media (width <= 480px) { @media (width <= 480px) {