1150 lines
50 KiB
Plaintext
1150 lines
50 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" style="display: none;">
|
|
<div class="ai-query-section">
|
|
<div style="text-align: center; margin-bottom: 2rem;">
|
|
<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;">
|
|
<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>
|
|
Spezifische Software oder Methode
|
|
</span>
|
|
</div>
|
|
|
|
<div class="ai-input-layout">
|
|
<div class="ai-textarea-section">
|
|
<textarea
|
|
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'"
|
|
style="min-height: 220px; resize: vertical; font-size: 0.9375rem; line-height: 1.5;"
|
|
maxlength="2000"
|
|
></textarea>
|
|
</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, um Ihr Szenario zu präzisieren.
|
|
</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" style="display: none;">
|
|
<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" style="display: none;">
|
|
<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" style="display: none;">
|
|
<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>
|
|
|
|
<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, wird für KI-Training verwendet 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>
|
|
</p>
|
|
</div>
|
|
|
|
<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"/>
|
|
<path d="M12 17h.01"/>
|
|
</svg>
|
|
<span id="submit-btn-text">Empfehlungen generieren</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="ai-loading" class="ai-loading" style="display: none; text-align: center; padding: 2rem;">
|
|
<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: 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"/>
|
|
<path d="M12 17h.01"/>
|
|
</svg>
|
|
</div>
|
|
<p id="loading-text" style="color: var(--color-text-secondary);">Analysiere Szenario und generiere Empfehlungen...</p>
|
|
|
|
<div id="queue-status" class="queue-status-card" style="display: none;">
|
|
<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>
|
|
<span class="stat-unit">Anfrage(n)</span>
|
|
</div>
|
|
<div class="queue-stat">
|
|
<span class="stat-label">Geschätzte Zeit:</span>
|
|
<span id="estimated-time" class="stat-value">--</span>
|
|
</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 id="ai-error" class="ai-error" style="display: none; 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. Bitte versuchen Sie es erneut.</p>
|
|
</div>
|
|
</div>
|
|
<br>
|
|
<div id="ai-results" class="ai-results" style="display: none;">
|
|
</div>
|
|
</section>
|
|
|
|
<script define:vars={{ tools, phases, domainAgnosticSoftware }}>
|
|
console.log('[DEBUG] Script loaded, before DOMContentLoaded');
|
|
function sanitizeHTML(html) {
|
|
const div = document.createElement('div');
|
|
div.textContent = html;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function formatDuration(ms) {
|
|
if (ms < 1000) return '< 1s';
|
|
if (ms < 60000) return `${Math.ceil(ms / 1000)}s`;
|
|
return `${Math.ceil(ms / 60000)}m`;
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
console.log('[DEBUG] DOMContentLoaded fired');
|
|
const aiInterface = document.getElementById('ai-interface');
|
|
const aiInput = document.getElementById('ai-query-input');
|
|
const aiSubmitBtn = document.getElementById('ai-submit-btn');
|
|
const submitBtnText = document.getElementById('submit-btn-text');
|
|
const aiLoading = document.getElementById('ai-loading');
|
|
const loadingText = document.getElementById('loading-text');
|
|
const aiError = document.getElementById('ai-error');
|
|
const aiErrorMessage = document.getElementById('ai-error-message');
|
|
const aiResults = document.getElementById('ai-results');
|
|
const aiDescription = document.getElementById('ai-description');
|
|
console.log('[DEBUG] aiInput element found:', !!aiInput);
|
|
|
|
// Smart prompting elements
|
|
const smartPromptingContainer = document.getElementById('smart-prompting-container');
|
|
const promptingStatus = document.getElementById('prompting-status');
|
|
const promptingSpinner = document.getElementById('prompting-spinner');
|
|
const suggestedQuestions = document.getElementById('suggested-questions');
|
|
const questionsList = document.getElementById('questions-list');
|
|
const dismissSuggestions = document.getElementById('dismiss-suggestions');
|
|
|
|
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 currentMode = 'workflow';
|
|
|
|
// Smart prompting state
|
|
let enhancementTimeout;
|
|
let enhancementAbortController;
|
|
|
|
if (!aiInput || !aiSubmitBtn || !aiLoading || !aiError || !aiResults) {
|
|
console.error('AI interface elements not found');
|
|
return;
|
|
}
|
|
|
|
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 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 eine Anwendung 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 Empfehlungen mit detaillierten Erklärungen zur optimalen Anwendung.",
|
|
submitText: "Empfehlungen finden",
|
|
loadingText: "Analysiere Anforderungen und suche passende Methode..."
|
|
}
|
|
};
|
|
|
|
function updateModeUI() {
|
|
const config = modeConfig[currentMode];
|
|
|
|
aiInput.placeholder = config.placeholder;
|
|
aiDescription.textContent = config.description;
|
|
submitBtnText.textContent = config.submitText;
|
|
loadingText.textContent = config.loadingText;
|
|
|
|
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');
|
|
}
|
|
|
|
aiResults.style.display = 'none';
|
|
aiError.style.display = 'none';
|
|
currentRecommendation = null;
|
|
}
|
|
|
|
function switchToMode(mode) {
|
|
if (currentMode !== mode) {
|
|
currentMode = mode;
|
|
resetSmartPrompting();
|
|
updateModeUI();
|
|
}
|
|
}
|
|
|
|
function showPromptingStatus(state) {
|
|
if (!smartPromptingContainer || !promptingStatus || !promptingSpinner) return;
|
|
|
|
const statusText = promptingStatus.querySelector('.status-text');
|
|
if (!statusText) return;
|
|
|
|
switch (state) {
|
|
case 'analyzing':
|
|
smartPromptingContainer.style.display = 'block';
|
|
toggleHintVisibility(false);
|
|
statusText.textContent = 'Analysiere Eingabe...';
|
|
promptingSpinner.style.display = 'inline-block';
|
|
suggestedQuestions.style.display = 'none';
|
|
break;
|
|
case 'suggestions':
|
|
statusText.textContent = 'Verbesserungsvorschläge verfügbar';
|
|
promptingSpinner.style.display = 'none';
|
|
suggestedQuestions.style.display = 'block';
|
|
break;
|
|
case 'rate-limited':
|
|
statusText.textContent = 'Nach Hauptabfrage verfügbar';
|
|
promptingSpinner.style.display = 'none';
|
|
suggestedQuestions.style.display = 'none';
|
|
break;
|
|
case 'error':
|
|
smartPromptingContainer.style.display = 'none';
|
|
toggleHintVisibility(true);
|
|
break;
|
|
case 'hidden':
|
|
smartPromptingContainer.style.display = 'none';
|
|
toggleHintVisibility(true);
|
|
break;
|
|
}
|
|
}
|
|
|
|
function displaySuggestions(suggestions) {
|
|
if (!questionsList || !suggestions || suggestions.length === 0) return;
|
|
|
|
questionsList.innerHTML = '';
|
|
|
|
suggestions.forEach((question) => {
|
|
const questionElement = document.createElement('div');
|
|
questionElement.className = 'suggestion-item';
|
|
questionElement.textContent = question;
|
|
questionsList.appendChild(questionElement);
|
|
});
|
|
|
|
showPromptingStatus('suggestions');
|
|
}
|
|
|
|
async function triggerSmartPrompting() {
|
|
const inputText = aiInput.value.trim();
|
|
|
|
if (inputText.length < 50) {
|
|
showPromptingStatus('hidden');
|
|
return;
|
|
}
|
|
|
|
// Cancel any previous enhancement call
|
|
if (enhancementAbortController) {
|
|
enhancementAbortController.abort();
|
|
}
|
|
|
|
enhancementAbortController = new AbortController();
|
|
|
|
try {
|
|
showPromptingStatus('analyzing');
|
|
|
|
const response = await fetch('/api/ai/enhance-input', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
input: inputText,
|
|
taskId: `enhance_${Date.now()}`
|
|
}),
|
|
signal: enhancementAbortController.signal
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) {
|
|
if (response.status === 429) {
|
|
showPromptingStatus('rate-limited');
|
|
} else {
|
|
showPromptingStatus('error');
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (data.success && data.questions && data.questions.length > 0) {
|
|
displaySuggestions(data.questions);
|
|
} else {
|
|
showPromptingStatus('hidden');
|
|
}
|
|
|
|
} catch (error) {
|
|
if (error.name === 'AbortError') {
|
|
return; // User cancelled, ignore
|
|
}
|
|
|
|
console.warn('Smart prompting failed:', error);
|
|
showPromptingStatus('error');
|
|
}
|
|
}
|
|
|
|
function resetSmartPrompting() {
|
|
clearTimeout(enhancementTimeout);
|
|
if (enhancementAbortController) {
|
|
enhancementAbortController.abort();
|
|
}
|
|
showPromptingStatus('hidden');
|
|
toggleHintVisibility(true);
|
|
}
|
|
|
|
function toggleHintVisibility(show) {
|
|
const hint = document.getElementById('smart-prompting-hint');
|
|
if (hint) {
|
|
hint.style.display = show ? 'flex' : 'none';
|
|
}
|
|
}
|
|
|
|
toggleSwitch.addEventListener('click', () => {
|
|
switchToMode(currentMode === 'workflow' ? 'tool' : 'workflow');
|
|
});
|
|
|
|
workflowLabel.addEventListener('click', () => {
|
|
switchToMode('workflow');
|
|
});
|
|
|
|
toolLabel.addEventListener('click', () => {
|
|
switchToMode('tool');
|
|
});
|
|
|
|
const updateCharacterCount = () => {
|
|
const length = aiInput.value.length;
|
|
const maxLength = 2000;
|
|
|
|
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)';
|
|
}
|
|
};
|
|
|
|
// Smart Prompting Input Handling - Fixed Race Conditions
|
|
aiInput.addEventListener('input', () => {
|
|
console.log('[DEBUG] Input event triggered, length:', aiInput.value.trim().length);
|
|
const inputLength = aiInput.value.trim().length;
|
|
|
|
// Clear ALL existing timeouts and abort controllers
|
|
clearTimeout(enhancementTimeout);
|
|
if (enhancementAbortController) {
|
|
enhancementAbortController.abort();
|
|
enhancementAbortController = null;
|
|
}
|
|
|
|
// Hide suggestions immediately if input is too short
|
|
if (inputLength < 40) {
|
|
showPromptingStatus('hidden');
|
|
return;
|
|
}
|
|
|
|
// Single consolidated timeout for all smart prompting logic
|
|
enhancementTimeout = setTimeout(() => {
|
|
const currentLength = aiInput.value.trim().length;
|
|
|
|
// Double-check length hasn't changed during timeout
|
|
if (currentLength < 40) {
|
|
showPromptingStatus('hidden');
|
|
return;
|
|
}
|
|
|
|
// Show analyzing state first
|
|
if (currentLength >= 50) {
|
|
showPromptingStatus('analyzing');
|
|
|
|
// Trigger enhancement after showing analyzing state
|
|
setTimeout(() => {
|
|
if (aiInput.value.trim().length >= 50) {
|
|
triggerSmartPrompting();
|
|
}
|
|
}, 500);
|
|
}
|
|
}, 1000); // Single timeout instead of multiple
|
|
});
|
|
|
|
aiInput.addEventListener('input', updateCharacterCount);
|
|
updateCharacterCount();
|
|
|
|
// Dismiss suggestions handler
|
|
if (dismissSuggestions) {
|
|
dismissSuggestions.addEventListener('click', () => {
|
|
showPromptingStatus('hidden');
|
|
});
|
|
}
|
|
|
|
function formatWorkflowSuggestion(text) {
|
|
// Improved handling for different list formats
|
|
const numberedListPattern = /(\d+\.\s)/g;
|
|
|
|
if (numberedListPattern.test(text)) {
|
|
const items = text.split(/\d+\.\s/).filter(item => item.trim().length > 0);
|
|
|
|
if (items.length > 1) {
|
|
const listItems = items.map(item =>
|
|
`<li style="margin-bottom: 0.5rem; line-height: 1.6;">${item.trim()}</li>`
|
|
).join('');
|
|
|
|
return `<ol style="margin: 0; padding-left: 1.5rem; color: var(--color-text);">${listItems}</ol>`;
|
|
}
|
|
}
|
|
|
|
// Handle bullet points
|
|
const bulletPattern = /^[\s]*[-\*•]\s/gm;
|
|
if (bulletPattern.test(text)) {
|
|
const items = text.split(/^[\s]*[-\*•]\s/gm).filter(item => item.trim().length > 0);
|
|
|
|
if (items.length > 1) {
|
|
const listItems = items.map(item =>
|
|
`<li style="margin-bottom: 0.5rem; line-height: 1.6;">${item.trim()}</li>`
|
|
).join('');
|
|
|
|
return `<ul style="margin: 0; padding-left: 1.5rem; color: var(--color-text);">${listItems}</ul>`;
|
|
}
|
|
}
|
|
|
|
// Handle line breaks as lists
|
|
if (text.includes('\n')) {
|
|
const lines = text.split('\n').filter(line => line.trim().length > 0);
|
|
if (lines.length > 1) {
|
|
const listItems = lines.map(line =>
|
|
`<li style="margin-bottom: 0.5rem; line-height: 1.6;">${line.trim()}</li>`
|
|
).join('');
|
|
|
|
return `<ul style="margin: 0; padding-left: 1.5rem; color: var(--color-text);">${listItems}</ul>`;
|
|
}
|
|
}
|
|
|
|
// Default paragraph formatting
|
|
return `<p style="margin: 0; line-height: 1.6; color: var(--color-text);">${text}</p>`;
|
|
}
|
|
|
|
function renderBackgroundKnowledge(backgroundKnowledge) {
|
|
if (!backgroundKnowledge || backgroundKnowledge.length === 0) {
|
|
return '';
|
|
}
|
|
|
|
const conceptLinks = backgroundKnowledge.map(concept => `
|
|
<div class="concept-recommendation" 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 class="concept-link"
|
|
onclick="event.stopPropagation(); window.showToolDetails('${concept.concept_name}', 'secondary')"
|
|
style="background: none; border: none; color: var(--color-concept); font-weight: 600; cursor: pointer; text-decoration: underline; font-size: 0.875rem; padding: 0;"
|
|
onmouseover="this.style.color='var(--color-concept-hover)';"
|
|
onmouseout="this.style.color='var(--color-concept)';">
|
|
📚 ${concept.concept_name}
|
|
</button>
|
|
<span class="badge" style="background-color: var(--color-concept); color: white; font-size: 0.625rem;">Hintergrundwissen</span>
|
|
</div>
|
|
<p style="margin: 0; font-size: 0.8125rem; line-height: 1.5; color: var(--color-text-secondary);">
|
|
${concept.relevance}
|
|
</p>
|
|
</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); 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="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/>
|
|
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>
|
|
</svg>
|
|
Empfohlenes Hintergrundwissen
|
|
</h4>
|
|
${conceptLinks}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function renderContextualAnalysis(recommendation, mode) {
|
|
let html = '';
|
|
|
|
// Scenario/Problem Analysis Section
|
|
const analysisField = mode === 'workflow' ? recommendation.scenario_analysis : recommendation.problem_analysis;
|
|
if (analysisField) {
|
|
html += `
|
|
<div class="card contextual-analysis-card scenario">
|
|
<h4 class="analysis-header scenario">
|
|
<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>
|
|
${mode === 'workflow' ? 'Szenario-Analyse' : 'Problem-Analyse'}
|
|
</h4>
|
|
${formatWorkflowSuggestion(analysisField)}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Investigation Approach Section
|
|
if (recommendation.investigation_approach) {
|
|
html += `
|
|
<div class="card contextual-analysis-card approach">
|
|
<h4 class="analysis-header approach">
|
|
<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>
|
|
${mode === 'workflow' ? 'Untersuchungsansatz' : 'Lösungsansatz'}
|
|
</h4>
|
|
${formatWorkflowSuggestion(recommendation.investigation_approach)}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Critical Considerations Section
|
|
if (recommendation.critical_considerations) {
|
|
html += `
|
|
<div class="card contextual-analysis-card critical">
|
|
<h4 class="analysis-header critical">
|
|
<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>
|
|
${mode === 'workflow' ? 'Kritische Überlegungen' : 'Wichtige Voraussetzungen'}
|
|
</h4>
|
|
${formatWorkflowSuggestion(recommendation.critical_considerations)}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
return html;
|
|
}
|
|
|
|
// Keep existing display functions...
|
|
function 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 resultsHTML = `
|
|
<div class="workflow-container">
|
|
<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;">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>
|
|
|
|
${renderContextualAnalysis(recommendation, 'workflow')}
|
|
|
|
${renderBackgroundKnowledge(recommendation.background_knowledge)}
|
|
|
|
${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 => {
|
|
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 ${tool.type === 'method' ? 'method' : hasValidProjectUrl ? 'hosted' : (tool.license !== 'Proprietary' ? 'oss' : '')}"
|
|
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]};">
|
|
${tool.recommendation.priority}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="tool-rec-justification">
|
|
"${tool.recommendation.justification}"
|
|
</div>
|
|
|
|
<div class="tool-rec-metadata">
|
|
<div style="display: flex; flex-wrap: wrap; gap: 0.25rem; margin-bottom: 0.5rem;">
|
|
${tool.type !== 'method' && hasValidProjectUrl ? '<span class="badge badge-primary">CC24-Server</span>' : ''}
|
|
${tool.knowledgebase === true ? '<span class="badge badge-error">📖</span>' : ''}
|
|
<span class="badge" style="background-color: var(--color-bg-tertiary); color: var(--color-text);">${tool.skillLevel}</span>
|
|
</div>
|
|
<div style="font-size: 0.8125rem; color: var(--color-text-secondary);">
|
|
${tool.type === 'method' ? 'Methode' : tool.platforms.join(', ') + ' • ' + tool.license}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).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>
|
|
${formatWorkflowSuggestion(recommendation.workflow_suggestion)}
|
|
</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>
|
|
<div style="color: white;">
|
|
${formatWorkflowSuggestion(recommendation.additional_notes).replace(/color: var\(--color-text\)/g, 'color: white')}
|
|
</div>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
|
|
aiResults.innerHTML = '';
|
|
const tempDiv = document.createElement('div');
|
|
tempDiv.innerHTML = resultsHTML;
|
|
aiResults.appendChild(tempDiv);
|
|
}
|
|
|
|
function displayToolResults(recommendation, originalQuery) {
|
|
function getSuitabilityText(score) {
|
|
const suitabilityTexts = {
|
|
high: 'GUT GEEIGNET',
|
|
medium: 'GEEIGNET',
|
|
low: 'VIELLEICHT GEEIGNET'
|
|
};
|
|
return suitabilityTexts[score] || 'GEEIGNET';
|
|
}
|
|
|
|
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-primary) 0%, #525252 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 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>
|
|
|
|
${renderContextualAnalysis(recommendation, 'tool')}
|
|
|
|
${renderBackgroundKnowledge(recommendation.background_knowledge)}
|
|
|
|
<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 isMethod = fullTool.type === 'method';
|
|
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 ${isMethod ? 'card-method' : 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>
|
|
${isMethod ? '<span class="badge" style="background-color: var(--color-method); color: white;">Methode</span>' : ''}
|
|
${!isMethod && hasValidProjectUrl ? '<span class="badge badge-primary">CC24-Server</span>' : ''}
|
|
${!isMethod && 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 diese Methode für Ihr Szenario?
|
|
</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;">
|
|
${!isMethod ? '<div><strong>Plattformen:</strong> ' + fullTool.platforms.join(', ') + '</div>' : ''}
|
|
<div><strong>Skill Level:</strong> ${fullTool.skillLevel}</div>
|
|
${!isMethod ? '<div><strong>Lizenz:</strong> ' + fullTool.license + '</div>' : ''}
|
|
<div><strong>Typ:</strong> ${isMethod ? 'Methode' : 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);
|
|
}
|
|
|
|
const handleSubmit = async () => {
|
|
const query = aiInput.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;
|
|
}
|
|
|
|
// Reset smart prompting when submitting
|
|
resetSmartPrompting();
|
|
|
|
const taskId = `ai_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
|
|
|
|
aiResults.style.display = 'none';
|
|
aiError.style.display = 'none';
|
|
aiLoading.style.display = 'block';
|
|
|
|
const queueStatus = document.getElementById('queue-status');
|
|
const taskIdDisplay = document.getElementById('current-task-id');
|
|
if (queueStatus && taskIdDisplay) {
|
|
queueStatus.style.display = 'block';
|
|
taskIdDisplay.textContent = taskId;
|
|
}
|
|
|
|
aiSubmitBtn.disabled = true;
|
|
submitBtnText.textContent = currentMode === 'workflow' ? 'Generiere Empfehlungen...' : 'Suche passende Methode...';
|
|
|
|
let statusInterval;
|
|
let startTime = Date.now();
|
|
|
|
const updateQueueStatus = async () => {
|
|
try {
|
|
const response = await fetch(`/api/ai/queue-status?taskId=${taskId}`);
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
const queueLength = document.getElementById('queue-length');
|
|
const estimatedTime = document.getElementById('estimated-time');
|
|
const positionBadge = document.getElementById('queue-position-badge');
|
|
const progressBar = document.getElementById('queue-progress');
|
|
|
|
if (queueLength) queueLength.textContent = data.queueLength;
|
|
|
|
if (estimatedTime) {
|
|
if (data.estimatedWaitTime > 0) {
|
|
estimatedTime.textContent = formatDuration(data.estimatedWaitTime);
|
|
} else {
|
|
estimatedTime.textContent = 'Verarbeitung läuft...';
|
|
}
|
|
}
|
|
|
|
if (positionBadge && data.currentPosition) {
|
|
positionBadge.textContent = data.currentPosition;
|
|
|
|
if (progressBar && data.queueLength > 0) {
|
|
const progress = Math.max(0, ((data.queueLength - data.currentPosition + 1) / data.queueLength) * 100);
|
|
progressBar.style.width = `${progress}%`;
|
|
}
|
|
}
|
|
|
|
if (data.isProcessing && !data.currentPosition) {
|
|
if (positionBadge) positionBadge.textContent = '⚡';
|
|
if (progressBar) progressBar.style.width = '100%';
|
|
if (estimatedTime) estimatedTime.textContent = 'Verarbeitung läuft...';
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.warn('Queue status update failed:', error);
|
|
}
|
|
};
|
|
|
|
updateQueueStatus();
|
|
|
|
statusInterval = setInterval(updateQueueStatus, 500);
|
|
|
|
try {
|
|
const response = await fetch('/api/ai/query', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
query,
|
|
mode: currentMode,
|
|
taskId
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (statusInterval) clearInterval(statusInterval);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(data.error || `HTTP ${response.status}`);
|
|
}
|
|
|
|
if (!data.success) {
|
|
throw new Error(data.error || 'Unknown error');
|
|
}
|
|
|
|
currentRecommendation = data.recommendation;
|
|
|
|
if (currentMode === 'workflow') {
|
|
displayWorkflowResults(data.recommendation, query);
|
|
} else {
|
|
displayToolResults(data.recommendation, query);
|
|
}
|
|
|
|
aiLoading.style.display = 'none';
|
|
aiResults.style.display = 'block';
|
|
|
|
} catch (error) {
|
|
console.error('AI query failed:', error);
|
|
|
|
if (statusInterval) clearInterval(statusInterval);
|
|
|
|
aiLoading.style.display = 'none';
|
|
aiError.style.display = 'block';
|
|
|
|
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 {
|
|
aiSubmitBtn.disabled = false;
|
|
const config = modeConfig[currentMode];
|
|
submitBtnText.textContent = config.submitText;
|
|
|
|
if (queueStatus) queueStatus.style.display = 'none';
|
|
if (statusInterval) clearInterval(statusInterval);
|
|
}
|
|
};
|
|
|
|
aiSubmitBtn.addEventListener('click', handleSubmit);
|
|
|
|
aiInput.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
|
e.preventDefault();
|
|
handleSubmit();
|
|
}
|
|
});
|
|
|
|
window.restoreAIResults = () => {
|
|
if (currentRecommendation) {
|
|
aiResults.style.display = 'block';
|
|
aiLoading.style.display = 'none';
|
|
aiError.style.display = 'none';
|
|
}
|
|
};
|
|
|
|
updateModeUI();
|
|
});
|
|
</script> |