1915 lines
76 KiB
Plaintext
1915 lines
76 KiB
Plaintext
---
|
|
// src/components/AIQueryInterface.astro
|
|
|
|
import { getToolsData } from '../utils/dataService.js';
|
|
|
|
|
|
const data = await getToolsData();
|
|
const tools = data.tools;
|
|
const phases = data.phases;
|
|
const domainAgnosticSoftware = data['domain-agnostic-software'] || [];
|
|
---
|
|
|
|
<section id="ai-interface" class="ai-interface hidden">
|
|
<div class="ai-query-section">
|
|
<div class="content-center-lg">
|
|
<h2 style="margin-bottom: 1rem; color: var(--color-primary);">
|
|
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.75rem; vertical-align: middle;">
|
|
<path d="M9 11H5a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7a2 2 0 0 0-2-2h-4"/>
|
|
<path d="M9 11V7a3 3 0 0 1 6 0v4"/>
|
|
</svg>
|
|
Forensic AI
|
|
</h2>
|
|
<p id="ai-description" class="text-muted" style="max-width: 700px; margin: 0 auto; line-height: 1.6;">
|
|
Beschreiben Sie Ihr forensisches Szenario und erhalten Sie maßgeschneiderte Workflow-Empfehlungen
|
|
basierend auf bewährten DFIR-Workflows und der verfügbaren Software-Datenbank.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="ai-input-container" style="max-width: 1000px; margin: 0 auto;">
|
|
<!-- Mode Toggle -->
|
|
<div class="ai-mode-toggle">
|
|
<span id="workflow-label" class="toggle-label active">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="mr-2 align-middle">
|
|
<polyline points="9,11 12,14 22,4"/>
|
|
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>
|
|
</svg>
|
|
Workflow-Empfehlung
|
|
</span>
|
|
|
|
<div class="toggle-switch">
|
|
<div class="toggle-slider"></div>
|
|
</div>
|
|
|
|
<span id="tool-label" class="toggle-label">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="mr-2 align-middle">
|
|
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>
|
|
</svg>
|
|
Spezifische Software oder Methode
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Input Layout -->
|
|
<div class="ai-input-layout">
|
|
<div class="ai-textarea-section">
|
|
<textarea
|
|
id="ai-query-input"
|
|
placeholder="Beschreiben Sie Ihr forensisches Szenario..."
|
|
style="min-height: 220px; resize: vertical; font-size: 0.9375rem; line-height: 1.5;"
|
|
maxlength="2000"
|
|
></textarea>
|
|
<div id="ai-char-counter" style="font-size: 0.75rem; color: var(--color-text-secondary); text-align: right; margin-top: 0.25rem;">0/2000</div>
|
|
</div>
|
|
|
|
<div class="ai-suggestions-section">
|
|
<div id="smart-prompting-hint" class="smart-prompting-hint">
|
|
<div class="hint-card">
|
|
<div class="hint-icon">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--color-accent)" stroke-width="2">
|
|
<circle cx="12" cy="12" r="10"/>
|
|
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
|
|
<line x1="12" y1="17" x2="12.01" y2="17"/>
|
|
</svg>
|
|
</div>
|
|
<div class="hint-content">
|
|
<h4 class="hint-title">Intelligente Hilfe</h4>
|
|
<p class="hint-description">
|
|
Während Sie tippen, analysiert die KI Ihre Eingabe und schlägt gezielten Fragen vor.
|
|
</p>
|
|
<div class="hint-trigger">
|
|
<span class="hint-label">Aktiviert ab:</span>
|
|
<span class="hint-value">40+ Zeichen</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="smart-prompting-container" class="smart-prompting-container hidden">
|
|
<div class="prompting-card">
|
|
<div id="prompting-status" class="prompting-status">
|
|
<div class="status-icon">💡</div>
|
|
<span class="status-text">Analysiere Eingabe...</span>
|
|
<div id="prompting-spinner" class="prompting-spinner hidden">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--color-accent)" stroke-width="2">
|
|
<path d="M21 12a9 9 0 11-6.219-8.56"/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="suggested-questions" class="suggested-questions hidden">
|
|
<div class="suggestions-header">
|
|
<span class="suggestions-label">Zur besseren Analyse:</span>
|
|
</div>
|
|
<div id="questions-list" class="questions-list"></div>
|
|
<button id="dismiss-suggestions" class="dismiss-button">
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<line x1="18" y1="6" x2="6" y2="18"/>
|
|
<line x1="6" y1="6" x2="18" y2="18"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Privacy Notice -->
|
|
<div style="margin-top: 0.5rem; margin-bottom: 1rem;">
|
|
<p style="font-size: 0.75rem; color: var(--color-text-secondary); text-align: center; line-height: 1.4;">
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.25rem; vertical-align: middle;">
|
|
<circle cx="12" cy="12" r="10"/>
|
|
<line x1="12" y1="8" x2="12" y2="12"/>
|
|
<line x1="12" y1="16" x2="12.01" y2="16"/>
|
|
</svg>
|
|
Ihre Anfrage wird über die API von mistral.ai übertragen.
|
|
<a href="https://mistral.ai/privacy-policy/" target="_blank" rel="noopener noreferrer" style="color: var(--color-primary);">Datenschutzrichtlinien</a>
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Submit Button -->
|
|
<div style="display: flex; justify-content: center; gap: 1rem;">
|
|
<button id="ai-submit-btn" class="btn btn-accent" style="padding: 0.75rem 2rem;">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
|
|
<path d="M14.828 14.828a4 4 0 0 1-5.656 0"/>
|
|
<path d="M9 9a3 3 0 1 1 6 0c0 .749-.269 1.433-.73 1.96L11 14v1a1 1 0 0 1-1 1h-1a1 1 0 0 1-1-1v-1l-3.27-3.04A3 3 0 0 1 5 9a3 3 0 0 1 6 0"/>
|
|
</svg>
|
|
<span id="submit-btn-text">Empfehlungen generieren</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading State -->
|
|
<div id="ai-loading" class="ai-loading hidden">
|
|
<div style="text-align: center; padding: 2rem;">
|
|
<div style="margin-bottom: 1rem;">
|
|
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" stroke-width="2" style="animation: pulse 2s ease-in-out infinite;">
|
|
<path d="M14.828 14.828a4 4 0 0 1-5.656 0"/>
|
|
<path d="M9 9a3 3 0 1 1 6 0c0 .749-.269 1.433-.73 1.96L11 14v1a1 1 0 0 1-1 1h-1a1 1 0 0 1-1-1v-1l-3.27-3.04A3 3 0 0 1 5 9a3 3 0 0 1 6 0"/>
|
|
</svg>
|
|
</div>
|
|
<p id="loading-text" class="text-secondary">Analysiere Szenario und generiere Empfehlungen...</p>
|
|
|
|
<!-- Queue Status -->
|
|
<div id="queue-status" class="queue-status-card hidden">
|
|
<div class="queue-header">
|
|
<div class="queue-position-display">
|
|
<div id="queue-position-badge" class="position-badge">1</div>
|
|
<span class="position-label">Position in Warteschlange</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="queue-details">
|
|
<div class="queue-stat">
|
|
<span class="stat-label">Warteschlange:</span>
|
|
<span id="queue-length" class="stat-value">0</span>
|
|
</div>
|
|
<div class="queue-stat">
|
|
<span class="stat-label">Geschätzte Zeit:</span>
|
|
<span id="estimated-time" class="stat-value">--</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Micro-task Progress -->
|
|
<div id="micro-task-progress" class="micro-task-progress hidden">
|
|
<div class="micro-task-header">
|
|
<span class="micro-task-label">🔬 micro-Agent-Analysis</span>
|
|
<span id="micro-task-counter" class="micro-task-counter">1/6</span>
|
|
</div>
|
|
<div class="micro-task-steps">
|
|
<div class="micro-step" data-step="scenario">📋 Problemanalyse</div>
|
|
<div class="micro-step" data-step="approach">🎯 Ermittlungsansatz</div>
|
|
<div class="micro-step" data-step="considerations">⚠️ Herausforderungen</div>
|
|
<div class="micro-step" data-step="tools">🔧 Methoden</div>
|
|
<div class="micro-step" data-step="knowledge">📚 Evaluation</div>
|
|
<div class="micro-step" data-step="final">✅ Audit-Trail</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="queue-progress-container">
|
|
<div class="queue-progress-track">
|
|
<div id="queue-progress" class="queue-progress-fill"></div>
|
|
</div>
|
|
<div class="task-id-display">
|
|
<span class="task-label">Task:</span>
|
|
<code id="current-task-id" class="task-id">--</code>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error State -->
|
|
<div id="ai-error" class="ai-error hidden">
|
|
<div style="text-align: center; padding: 2rem;">
|
|
<div style="background-color: var(--color-error); color: white; padding: 1rem; border-radius: 0.5rem; max-width: 600px; margin: 0 auto;">
|
|
<h3 style="margin-bottom: 0.5rem;">Fehler bei der KI-Anfrage</h3>
|
|
<p id="ai-error-message" style="margin: 0;">Ein unerwarteter Fehler ist aufgetreten.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Results -->
|
|
<div id="ai-results" class="ai-results hidden">
|
|
<div id="audit-trail-container"></div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<script type="module" define:vars={{ tools, phases, domainAgnosticSoftware }}>
|
|
|
|
function showElement(element) {
|
|
if (element) {
|
|
element.style.display = 'block';
|
|
element.classList.remove('hidden');
|
|
}
|
|
}
|
|
|
|
function hideElement(element) {
|
|
if (element) {
|
|
element.style.display = 'none';
|
|
element.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
function sanitizeText(text) {
|
|
if (typeof text !== 'string') return '';
|
|
|
|
return text
|
|
.replace(/^#{1,6}\s+/gm, '')
|
|
.replace(/^\s*[-*+]\s+/gm, '')
|
|
.replace(/^\s*\d+\.\s+/gm, '')
|
|
.replace(/\*\*(.+?)\*\*/g, '$1')
|
|
.replace(/\*(.+?)\*/g, '$1')
|
|
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|
.replace(/```[\s\S]*?```/g, '[CODE BLOCK]')
|
|
.replace(/`([^`]+)`/g, '$1')
|
|
.replace(/<[^>]+>/g, '')
|
|
.replace(/\n\s*\n\s*\n/g, '\n\n')
|
|
.trim();
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
if (typeof text !== 'string') return String(text);
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function truncateText(text, maxLength) {
|
|
if (!text || text.length <= maxLength) return text;
|
|
return text.slice(0, maxLength) + '...';
|
|
}
|
|
|
|
function formatDuration(ms) {
|
|
if (ms < 1000) return '< 1s';
|
|
if (ms < 60000) return `${Math.ceil(ms / 1000)}s`;
|
|
const minutes = Math.floor(ms / 60000);
|
|
const seconds = Math.ceil((ms % 60000) / 1000);
|
|
return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
|
|
}
|
|
|
|
function getConfidenceColor(confidence) {
|
|
if (confidence >= 80) return 'var(--color-accent)';
|
|
if (confidence >= 60) return 'var(--color-warning)';
|
|
return 'var(--color-error)';
|
|
}
|
|
|
|
function isToolHosted(tool) {
|
|
return tool.projectUrl !== undefined &&
|
|
tool.projectUrl !== null &&
|
|
tool.projectUrl !== "" &&
|
|
tool.projectUrl.trim() !== "";
|
|
}
|
|
|
|
function summarizeData(data) {
|
|
if (data === null || data === undefined) return 'null';
|
|
if (typeof data === 'string') {
|
|
return data.length > 100 ? data.slice(0, 100) + '...' : data;
|
|
}
|
|
if (typeof data === 'number' || typeof data === 'boolean') {
|
|
return data.toString();
|
|
}
|
|
if (Array.isArray(data)) {
|
|
if (data.length === 0) return '[]';
|
|
if (data.length <= 3) return JSON.stringify(data);
|
|
return `[${data.slice(0, 3).map(i => typeof i === 'string' ? i : JSON.stringify(i)).join(', ')}, ...+${data.length - 3}]`;
|
|
}
|
|
if (typeof data === 'object') {
|
|
const keys = Object.keys(data);
|
|
if (keys.length === 0) return '{}';
|
|
if (keys.length <= 3) {
|
|
return '{' + keys.map(k => `${k}: ${typeof data[k] === 'string' ? data[k].slice(0, 20) + (data[k].length > 20 ? '...' : '') : JSON.stringify(data[k])}`).join(', ') + '}';
|
|
}
|
|
return `{${keys.slice(0, 3).join(', ')}, ...+${keys.length - 3} keys}`;
|
|
}
|
|
return String(data);
|
|
}
|
|
|
|
const Utils = {
|
|
phaseConfig: {
|
|
'initialization': { icon: '🚀', displayName: 'Initialisierung' },
|
|
'retrieval': { icon: '🔍', displayName: 'Datensuche' },
|
|
'selection': { icon: '🎯', displayName: 'Tool-Auswahl' },
|
|
'micro-task': { icon: '⚡', displayName: 'Detail-Analyse' },
|
|
'validation': { icon: '✓', displayName: 'Validierung' },
|
|
'completion': { icon: '✅', displayName: 'Finalisierung' }
|
|
},
|
|
|
|
actionTranslations: {
|
|
'pipeline-start': 'Analyse gestartet',
|
|
'embeddings-search': 'Ähnliche Tools gesucht',
|
|
'ai-tool-selection': 'Tools automatisch ausgewählt',
|
|
'ai-analysis': 'KI-Analyse durchgeführt',
|
|
'phase-tool-selection': 'Phasen-Tools evaluiert',
|
|
'tool-evaluation': 'Tool-Bewertung erstellt',
|
|
'background-knowledge-selection': 'Hintergrundwissen ausgewählt',
|
|
'confidence-scoring': 'Vertrauenswertung berechnet',
|
|
'pipeline-end': 'Analyse abgeschlossen'
|
|
},
|
|
|
|
getActionDisplayName(action) {
|
|
return this.actionTranslations[action] || action;
|
|
}
|
|
};
|
|
|
|
class AIQueryInterface {
|
|
constructor() {
|
|
this.currentMode = 'workflow';
|
|
this.currentRecommendation = null;
|
|
this.enhancementTimeout = null;
|
|
this.enhancementAbortController = null;
|
|
this.statusInterval = null;
|
|
this.microTaskInterval = null;
|
|
this.currentMicroTaskStep = 0;
|
|
|
|
console.log('[AI Interface] Initializing...');
|
|
this.initializeElements();
|
|
this.setupEventListeners();
|
|
this.updateModeUI();
|
|
console.log('[AI Interface] Initialized successfully');
|
|
}
|
|
|
|
initializeElements() {
|
|
this.elements = {
|
|
input: document.getElementById('ai-query-input'),
|
|
submitBtn: document.getElementById('ai-submit-btn'),
|
|
submitBtnText: document.getElementById('submit-btn-text'),
|
|
description: document.getElementById('ai-description'),
|
|
loading: document.getElementById('ai-loading'),
|
|
loadingText: document.getElementById('loading-text'),
|
|
error: document.getElementById('ai-error'),
|
|
errorMessage: document.getElementById('ai-error-message'),
|
|
results: document.getElementById('ai-results'),
|
|
charCounter: document.getElementById('ai-char-counter'),
|
|
|
|
toggleSwitch: document.querySelector('.toggle-switch'),
|
|
toggleSlider: document.querySelector('.toggle-slider'),
|
|
workflowLabel: document.getElementById('workflow-label'),
|
|
toolLabel: document.getElementById('tool-label'),
|
|
|
|
promptingContainer: document.getElementById('smart-prompting-container'),
|
|
promptingStatus: document.getElementById('prompting-status'),
|
|
promptingSpinner: document.getElementById('prompting-spinner'),
|
|
suggestedQuestions: document.getElementById('suggested-questions'),
|
|
questionsList: document.getElementById('questions-list'),
|
|
dismissSuggestions: document.getElementById('dismiss-suggestions'),
|
|
smartHint: document.getElementById('smart-prompting-hint'),
|
|
|
|
queueStatus: document.getElementById('queue-status'),
|
|
queueLength: document.getElementById('queue-length'),
|
|
estimatedTime: document.getElementById('estimated-time'),
|
|
positionBadge: document.getElementById('queue-position-badge'),
|
|
progressBar: document.getElementById('queue-progress'),
|
|
taskIdDisplay: document.getElementById('current-task-id'),
|
|
microTaskProgress: document.getElementById('micro-task-progress'),
|
|
microTaskCounter: document.getElementById('micro-task-counter')
|
|
};
|
|
|
|
console.log('[AI Interface] Elements initialized:', {
|
|
submitBtn: !!this.elements.submitBtn,
|
|
input: !!this.elements.input,
|
|
results: !!this.elements.results
|
|
});
|
|
|
|
return Object.values(this.elements).some(el => el !== null);
|
|
}
|
|
|
|
setupEventListeners() {
|
|
console.log('[AI Interface] Setting up event listeners...');
|
|
|
|
this.elements.toggleSwitch?.addEventListener('click', () => this.toggleMode());
|
|
this.elements.workflowLabel?.addEventListener('click', () => this.setMode('workflow'));
|
|
this.elements.toolLabel?.addEventListener('click', () => this.setMode('tool'));
|
|
|
|
this.elements.input?.addEventListener('input', () => this.handleInputChange());
|
|
this.elements.input?.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
|
e.preventDefault();
|
|
this.handleSubmit();
|
|
}
|
|
});
|
|
|
|
if (this.elements.submitBtn) {
|
|
console.log('[AI Interface] Attaching click handler to submit button');
|
|
this.elements.submitBtn.addEventListener('click', () => {
|
|
console.log('[AI Interface] Submit button clicked!');
|
|
this.handleSubmit();
|
|
});
|
|
} else {
|
|
console.error('[AI Interface] Submit button not found!');
|
|
}
|
|
|
|
this.elements.dismissSuggestions?.addEventListener('click', () => this.hideSmartPrompting());
|
|
|
|
console.log('[AI Interface] Event listeners setup complete');
|
|
}
|
|
|
|
getModeConfig() {
|
|
return {
|
|
workflow: {
|
|
placeholder: "Beschreiben Sie Ihr forensisches Szenario... z.B. 'Verdacht auf Ransomware-Angriff auf Windows-Domänencontroller'",
|
|
description: "Beschreiben Sie Ihre Untersuchungssituation und erhalten Empfehlungen für alle Phasen der Untersuchung.",
|
|
submitText: "Empfehlungen generieren",
|
|
loadingText: "Analysiere Szenario und generiere Empfehlungen..."
|
|
},
|
|
tool: {
|
|
placeholder: "Beschreiben Sie Ihr Problem... z.B. 'Analyse von Android-Backups mit WhatsApp-Nachrichten'",
|
|
description: "Beschreiben Sie Ihre Untersuchungssituation und erhalten Empfehlungen für eine spezifische Aufgabenstellung.",
|
|
submitText: "Empfehlungen finden",
|
|
loadingText: "Analysiere Anforderungen und suche passende Methode..."
|
|
}
|
|
};
|
|
}
|
|
|
|
toggleMode() {
|
|
this.setMode(this.currentMode === 'workflow' ? 'tool' : 'workflow');
|
|
}
|
|
|
|
setMode(mode) {
|
|
if (this.currentMode === mode) return;
|
|
|
|
this.currentMode = mode;
|
|
this.hideSmartPrompting();
|
|
this.hideResults();
|
|
this.updateModeUI();
|
|
}
|
|
|
|
updateModeUI() {
|
|
const config = this.getModeConfig()[this.currentMode];
|
|
|
|
if (this.elements.input) this.elements.input.placeholder = config.placeholder;
|
|
if (this.elements.description) this.elements.description.textContent = config.description;
|
|
if (this.elements.submitBtnText) this.elements.submitBtnText.textContent = config.submitText;
|
|
if (this.elements.loadingText) this.elements.loadingText.textContent = config.loadingText;
|
|
|
|
const isWorkflow = this.currentMode === 'workflow';
|
|
const primaryColor = isWorkflow ? 'var(--color-primary)' : 'var(--color-accent)';
|
|
const secondaryColor = 'var(--color-text-secondary)';
|
|
|
|
if (this.elements.toggleSlider) {
|
|
this.elements.toggleSlider.style.transform = isWorkflow ? 'translateX(0)' : 'translateX(26px)';
|
|
}
|
|
if (this.elements.toggleSwitch) {
|
|
this.elements.toggleSwitch.style.backgroundColor = primaryColor;
|
|
}
|
|
|
|
if (this.elements.workflowLabel) {
|
|
this.elements.workflowLabel.style.color = isWorkflow ? primaryColor : secondaryColor;
|
|
this.elements.workflowLabel.classList.toggle('active', isWorkflow);
|
|
}
|
|
if (this.elements.toolLabel) {
|
|
this.elements.toolLabel.style.color = isWorkflow ? secondaryColor : primaryColor;
|
|
this.elements.toolLabel.classList.toggle('active', !isWorkflow);
|
|
}
|
|
}
|
|
|
|
handleInputChange() {
|
|
this.updateCharacterCount();
|
|
|
|
clearTimeout(this.enhancementTimeout);
|
|
if (this.enhancementAbortController) {
|
|
this.enhancementAbortController.abort();
|
|
}
|
|
|
|
const inputLength = this.elements.input.value.trim().length;
|
|
|
|
if (inputLength < 40) {
|
|
this.hideSmartPrompting();
|
|
return;
|
|
}
|
|
|
|
this.enhancementTimeout = setTimeout(() => {
|
|
if (this.elements.input.value.trim().length >= 50) {
|
|
this.triggerSmartPrompting();
|
|
}
|
|
}, 1000);
|
|
}
|
|
|
|
updateCharacterCount() {
|
|
if (!this.elements.input || !this.elements.charCounter) return;
|
|
|
|
const length = this.elements.input.value.length;
|
|
const maxLength = 2000;
|
|
|
|
this.elements.charCounter.textContent = `${length}/${maxLength}`;
|
|
this.elements.charCounter.style.color = length > maxLength * 0.9 ?
|
|
'var(--color-warning)' : 'var(--color-text-secondary)';
|
|
}
|
|
|
|
async triggerSmartPrompting() {
|
|
const inputText = this.elements.input.value.trim();
|
|
|
|
if (inputText.length < 50) {
|
|
this.hideSmartPrompting();
|
|
return;
|
|
}
|
|
|
|
this.enhancementAbortController = new AbortController();
|
|
|
|
try {
|
|
this.showPromptingStatus('analyzing');
|
|
|
|
const response = await fetch('/api/ai/enhance-input', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ input: inputText }),
|
|
signal: this.enhancementAbortController.signal
|
|
});
|
|
|
|
if (!response.ok) {
|
|
this.showPromptingStatus(response.status === 429 ? 'rate-limited' : 'error');
|
|
return;
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success && data.questions?.length > 0) {
|
|
this.displaySuggestions(data.questions);
|
|
} else {
|
|
this.hideSmartPrompting();
|
|
}
|
|
|
|
} catch (error) {
|
|
if (error.name !== 'AbortError') {
|
|
console.warn('[AI Interface] Smart prompting failed:', error);
|
|
this.showPromptingStatus('error');
|
|
}
|
|
}
|
|
}
|
|
|
|
showPromptingStatus(state) {
|
|
if (!this.elements.promptingContainer || !this.elements.promptingStatus) return;
|
|
|
|
const statusText = this.elements.promptingStatus.querySelector('.status-text');
|
|
if (!statusText) return;
|
|
|
|
const statusMap = {
|
|
analyzing: {
|
|
text: 'Analysiere Eingabe...',
|
|
showSpinner: true,
|
|
showSuggestions: false,
|
|
showContainer: true,
|
|
hideHint: true
|
|
},
|
|
suggestions: {
|
|
text: 'Verbesserungsvorschläge verfügbar',
|
|
showSpinner: false,
|
|
showSuggestions: true,
|
|
showContainer: true,
|
|
hideHint: true
|
|
},
|
|
'rate-limited': {
|
|
text: 'Nach Hauptabfrage verfügbar',
|
|
showSpinner: false,
|
|
showSuggestions: false,
|
|
showContainer: true,
|
|
hideHint: true
|
|
},
|
|
error: {
|
|
showContainer: false,
|
|
hideHint: false
|
|
}
|
|
};
|
|
|
|
const config = statusMap[state];
|
|
if (!config) return;
|
|
|
|
statusText.textContent = config.text || '';
|
|
|
|
if (config.showContainer) showElement(this.elements.promptingContainer);
|
|
else hideElement(this.elements.promptingContainer);
|
|
|
|
if (config.hideHint) hideElement(this.elements.smartHint);
|
|
else showElement(this.elements.smartHint);
|
|
|
|
if (config.showSpinner) showElement(this.elements.promptingSpinner);
|
|
else hideElement(this.elements.promptingSpinner);
|
|
|
|
if (config.showSuggestions) showElement(this.elements.suggestedQuestions);
|
|
else hideElement(this.elements.suggestedQuestions);
|
|
}
|
|
|
|
displaySuggestions(suggestions) {
|
|
if (!this.elements.questionsList || !suggestions?.length) return;
|
|
|
|
this.elements.questionsList.innerHTML = suggestions.map(question =>
|
|
`<div class="suggestion-item">${escapeHtml(question)}</div>`
|
|
).join('');
|
|
|
|
this.showPromptingStatus('suggestions');
|
|
}
|
|
|
|
hideSmartPrompting() {
|
|
if (this.elements.promptingContainer) hideElement(this.elements.promptingContainer);
|
|
if (this.elements.smartHint) showElement(this.elements.smartHint);
|
|
}
|
|
|
|
async handleSubmit() {
|
|
console.log('[AI Interface] handleSubmit called');
|
|
|
|
const query = this.elements.input.value.trim();
|
|
|
|
if (!query) {
|
|
alert('Bitte geben Sie eine Beschreibung ein.');
|
|
return;
|
|
}
|
|
|
|
if (query.length < 10) {
|
|
alert('Bitte geben Sie eine detailliertere Beschreibung ein (mindestens 10 Zeichen).');
|
|
return;
|
|
}
|
|
|
|
const taskId = `ai_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
|
|
|
|
console.log('[AI Interface] Starting AI query with taskId:', taskId);
|
|
|
|
this.hideSmartPrompting();
|
|
this.hideResults();
|
|
this.hideError();
|
|
this.showLoading();
|
|
this.startQueueMonitoring(taskId);
|
|
|
|
this.elements.submitBtn.disabled = true;
|
|
this.elements.submitBtnText.textContent = this.currentMode === 'workflow' ?
|
|
'Generiere Empfehlungen...' : 'Suche passende Methode...';
|
|
|
|
try {
|
|
console.log('[AI Interface] Sending request to /api/ai/query');
|
|
|
|
const response = await fetch('/api/ai/query', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
query,
|
|
mode: this.currentMode,
|
|
taskId
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
console.log('[AI Interface] Received response:', {
|
|
ok: response.ok,
|
|
status: response.status,
|
|
success: data.success
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(data.error || `HTTP ${response.status}`);
|
|
}
|
|
|
|
if (!data.success) {
|
|
throw new Error(data.error || 'Unknown error');
|
|
}
|
|
|
|
this.currentRecommendation = data.recommendation;
|
|
await this.displayResults(data.recommendation, query);
|
|
|
|
} catch (error) {
|
|
console.error('[AI Interface] Request failed:', error);
|
|
this.showError(this.getErrorMessage(error));
|
|
} finally {
|
|
this.stopQueueMonitoring();
|
|
this.hideLoading();
|
|
this.resetSubmitButton();
|
|
}
|
|
}
|
|
|
|
getErrorMessage(error) {
|
|
const errorMap = {
|
|
'429': 'Zu viele Anfragen. Bitte warten Sie einen Moment.',
|
|
'401': 'Authentifizierung erforderlich. Bitte melden Sie sich an.',
|
|
'503': 'KI-Service vorübergehend nicht verfügbar.'
|
|
};
|
|
|
|
for (const [code, message] of Object.entries(errorMap)) {
|
|
if (error.message.includes(code)) return message;
|
|
}
|
|
|
|
return `Fehler: ${error.message}`;
|
|
}
|
|
|
|
resetSubmitButton() {
|
|
if (this.elements.submitBtn) this.elements.submitBtn.disabled = false;
|
|
if (this.elements.submitBtnText) {
|
|
const config = this.getModeConfig()[this.currentMode];
|
|
this.elements.submitBtnText.textContent = config.submitText;
|
|
}
|
|
}
|
|
|
|
startQueueMonitoring(taskId) {
|
|
if (this.elements.queueStatus) showElement(this.elements.queueStatus);
|
|
if (this.elements.taskIdDisplay) {
|
|
this.elements.taskIdDisplay.textContent = taskId.slice(-8);
|
|
}
|
|
|
|
this.startMicroTaskProgress();
|
|
|
|
setTimeout(() => {
|
|
this.updateQueueStatus(taskId);
|
|
this.statusInterval = setInterval(() => this.updateQueueStatus(taskId), 1000);
|
|
}, 500);
|
|
}
|
|
|
|
async updateQueueStatus(taskId) {
|
|
try {
|
|
const response = await fetch(`/api/ai/queue-status?taskId=${taskId}`);
|
|
if (!response.ok) return;
|
|
|
|
const data = await response.json();
|
|
|
|
if (this.elements.queueLength) this.elements.queueLength.textContent = data.queueLength || 0;
|
|
if (this.elements.estimatedTime) {
|
|
this.elements.estimatedTime.textContent = data.estimatedWaitTime > 0 ?
|
|
formatDuration(data.estimatedWaitTime) : 'Verarbeitung läuft...';
|
|
}
|
|
|
|
if (this.elements.positionBadge) {
|
|
if (data.currentPosition) {
|
|
this.elements.positionBadge.textContent = data.currentPosition;
|
|
if (this.elements.progressBar && data.queueLength > 0) {
|
|
const progress = Math.max(0, ((data.queueLength - data.currentPosition + 1) / data.queueLength) * 100);
|
|
this.elements.progressBar.style.width = `${progress}%`;
|
|
}
|
|
} else {
|
|
const stateIcons = { processing: '⚡', completed: '✅', failed: '❌' };
|
|
this.elements.positionBadge.textContent = stateIcons[data.taskStatus] || '?';
|
|
if (this.elements.progressBar) {
|
|
this.elements.progressBar.style.width = data.taskStatus === 'processing' ? '100%' : '0%';
|
|
}
|
|
}
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('[AI Interface] Queue status update failed:', error);
|
|
}
|
|
}
|
|
|
|
stopQueueMonitoring() {
|
|
if (this.statusInterval) {
|
|
clearInterval(this.statusInterval);
|
|
this.statusInterval = null;
|
|
}
|
|
|
|
if (this.microTaskInterval) {
|
|
clearInterval(this.microTaskInterval);
|
|
this.microTaskInterval = null;
|
|
}
|
|
|
|
if (this.elements.queueStatus) hideElement(this.elements.queueStatus);
|
|
if (this.elements.microTaskProgress) hideElement(this.elements.microTaskProgress);
|
|
}
|
|
|
|
startMicroTaskProgress() {
|
|
if (!this.elements.microTaskProgress) return;
|
|
|
|
showElement(this.elements.microTaskProgress);
|
|
this.currentMicroTaskStep = 0;
|
|
|
|
const steps = ['scenario', 'approach', 'considerations', 'tools', 'knowledge', 'final'];
|
|
const stepElements = this.elements.microTaskProgress.querySelectorAll('.micro-step');
|
|
|
|
stepElements.forEach(step => {
|
|
step.classList.remove('active', 'completed', 'failed');
|
|
});
|
|
|
|
this.microTaskInterval = setInterval(() => {
|
|
if (this.currentMicroTaskStep < steps.length) {
|
|
if (this.currentMicroTaskStep > 0) {
|
|
const prevStep = this.elements.microTaskProgress.querySelector(`[data-step="${steps[this.currentMicroTaskStep - 1]}"]`);
|
|
if (prevStep) {
|
|
prevStep.classList.remove('active');
|
|
prevStep.classList.add('completed');
|
|
}
|
|
}
|
|
|
|
const currentStep = this.elements.microTaskProgress.querySelector(`[data-step="${steps[this.currentMicroTaskStep]}"]`);
|
|
if (currentStep) {
|
|
currentStep.classList.add('active');
|
|
}
|
|
|
|
if (this.elements.microTaskCounter) {
|
|
this.elements.microTaskCounter.textContent = `${this.currentMicroTaskStep + 1}/${steps.length}`;
|
|
}
|
|
|
|
this.currentMicroTaskStep++;
|
|
} else {
|
|
const lastStep = this.elements.microTaskProgress.querySelector(`[data-step="${steps[steps.length - 1]}"]`);
|
|
if (lastStep) {
|
|
lastStep.classList.remove('active');
|
|
lastStep.classList.add('completed');
|
|
}
|
|
|
|
if (this.elements.microTaskCounter) {
|
|
this.elements.microTaskCounter.textContent = `${steps.length}/${steps.length}`;
|
|
}
|
|
|
|
clearInterval(this.microTaskInterval);
|
|
this.microTaskInterval = null;
|
|
}
|
|
}, 2000);
|
|
}
|
|
|
|
async displayResults(recommendation, originalQuery) {
|
|
console.log('[AI Interface] Displaying results for mode:', this.currentMode);
|
|
|
|
if (this.currentMode === 'workflow') {
|
|
await this.displayWorkflowResults(recommendation, originalQuery);
|
|
} else {
|
|
await this.displayToolResults(recommendation, originalQuery);
|
|
}
|
|
|
|
this.showResults();
|
|
|
|
setTimeout(() => {
|
|
try {
|
|
this.renderAuditTrail(recommendation.auditTrail);
|
|
} catch (error) {
|
|
console.error('[AI Interface] Failed to render audit trail:', error);
|
|
}
|
|
}, 100);
|
|
}
|
|
|
|
async 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,
|
|
confidence: recTool.confidence,
|
|
justification: recTool.justification,
|
|
priority: recTool.priority
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
const html = `
|
|
<div class="workflow-container">
|
|
${this.renderHeader('Untersuchungsansatz', originalQuery)}
|
|
${this.renderContextualAnalysis(recommendation, 'workflow')}
|
|
${this.renderBackgroundKnowledge(recommendation.background_knowledge)}
|
|
${this.renderWorkflowPhases(toolsByPhase, phaseOrder, phaseNames)}
|
|
${this.renderWorkflowSuggestion(recommendation.workflow_suggestion)}
|
|
</div>
|
|
<div id="audit-trail-container"></div>
|
|
`;
|
|
|
|
this.elements.results.innerHTML = html;
|
|
}
|
|
|
|
async displayToolResults(recommendation, originalQuery) {
|
|
const html = `
|
|
<div class="workflow-container">
|
|
${this.renderHeader('Handlungsempfehlung', originalQuery)}
|
|
${this.renderContextualAnalysis(recommendation, 'tool')}
|
|
${this.renderBackgroundKnowledge(recommendation.background_knowledge)}
|
|
${this.renderToolRecommendations(recommendation.recommended_tools)}
|
|
${this.renderAdditionalConsiderations(recommendation.additional_considerations)}
|
|
</div>
|
|
<div id="audit-trail-container"></div>
|
|
`;
|
|
|
|
this.elements.results.innerHTML = html;
|
|
}
|
|
|
|
renderAuditTrail(rawAuditTrail) {
|
|
const container = document.getElementById('audit-trail-container');
|
|
if (!container || !rawAuditTrail || !Array.isArray(rawAuditTrail)) {
|
|
console.warn('[AI Interface] Cannot render audit trail: invalid container or data');
|
|
return;
|
|
}
|
|
|
|
console.log('[AI Interface] Rendering comprehensive audit trail with', rawAuditTrail.length, 'entries');
|
|
|
|
try {
|
|
// Calculate audit statistics
|
|
const stats = this.calculateAuditStats(rawAuditTrail);
|
|
|
|
container.innerHTML = `
|
|
<div class="audit-trail-container">
|
|
<div class="audit-trail-header clickable" onclick="this.parentElement.querySelector('.audit-trail-details').classList.toggle('collapsed'); this.querySelector('.toggle-icon').style.transform = this.parentElement.querySelector('.audit-trail-details').classList.contains('collapsed') ? 'rotate(0deg)' : 'rotate(90deg)';">
|
|
<div class="audit-trail-title">
|
|
<div class="audit-icon">
|
|
<div class="audit-icon-gradient">🔍</div>
|
|
<h4>KI-Entscheidungspfad</h4>
|
|
</div>
|
|
<div class="audit-stats">
|
|
<div class="stat-item">
|
|
<div class="stat-dot stat-time"></div>
|
|
<span>${formatDuration(stats.totalTime)}</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-dot" style="background-color: ${getConfidenceColor(stats.avgConfidence)};"></div>
|
|
<span>${stats.avgConfidence}% Vertrauen</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span>${rawAuditTrail.length} Schritte</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<svg class="toggle-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="transform: rotate(90deg);">
|
|
<polyline points="9 18 15 12 9 6"/>
|
|
</svg>
|
|
</div>
|
|
|
|
<div class="audit-trail-details">
|
|
<!-- Audit Summary -->
|
|
<div class="audit-summary">
|
|
<div class="summary-header">📊 Analyse-Zusammenfassung</div>
|
|
<div class="summary-grid">
|
|
<div class="summary-stat">
|
|
<div class="summary-value success">${stats.aiDecisionCount}</div>
|
|
<div class="summary-label">KI-Entscheidungen</div>
|
|
</div>
|
|
<div class="summary-stat">
|
|
<div class="summary-value ${stats.embeddingsUsageCount > 0 ? 'success' : 'warning'}">${stats.embeddingsUsageCount}</div>
|
|
<div class="summary-label">Semantische Suchen</div>
|
|
</div>
|
|
<div class="summary-stat">
|
|
<div class="summary-value success">${stats.toolSelectionCount}</div>
|
|
<div class="summary-label">Tool-Auswahlen</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="insights-section">
|
|
${stats.qualityMetrics.aiTransparency >= 80 ? `
|
|
<div class="insights-header success">✅ Hohe Transparenz</div>
|
|
<ul class="insights-list">
|
|
<li>${stats.qualityMetrics.aiTransparency}% der Entscheidungen mit nachvollziehbarer Begründung</li>
|
|
<li>Durchschnittliche Verarbeitungszeit: ${formatDuration(stats.qualityMetrics.avgProcessingTime)}</li>
|
|
</ul>
|
|
` : `
|
|
<div class="insights-header warning">⚠️ Transparenz-Hinweise</div>
|
|
<ul class="insights-list">
|
|
<li>Nur ${stats.qualityMetrics.aiTransparency}% der Entscheidungen vollständig dokumentiert</li>
|
|
<li>Einige KI-Entscheidungen ohne detaillierte Begründung</li>
|
|
</ul>
|
|
`}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Process Flow -->
|
|
<div class="audit-process-flow">
|
|
${this.renderPhaseGroups(rawAuditTrail, stats)}
|
|
</div>
|
|
|
|
<!-- Download/Upload Controls -->
|
|
<div class="ai-results-controls" style="margin-top: 1.5rem; text-align: center; border-top: 1px solid var(--color-border); padding-top: 1rem;">
|
|
<button id="download-results-btn" class="btn btn-secondary" 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="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
|
<polyline points="7 10 12 15 17 10"/>
|
|
<line x1="12" y1="15" x2="12" y2="3"/>
|
|
</svg>
|
|
Analyse herunterladen
|
|
</button>
|
|
|
|
<label for="upload-results" 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="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
|
<polyline points="17 8 12 3 7 8"/>
|
|
<line x1="12" y1="3" x2="12" y2="15"/>
|
|
</svg>
|
|
Analyse hochladen
|
|
<input type="file" id="upload-results" accept=".json" style="display: none;" />
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Technical Details Toggle -->
|
|
<div class="technical-toggle">
|
|
<button class="technical-toggle-btn" onclick="this.parentElement.parentElement.querySelector('.technical-details').classList.toggle('collapsed'); this.textContent = this.parentElement.parentElement.querySelector('.technical-details').classList.contains('collapsed') ? 'Technische Details anzeigen' : 'Technische Details verbergen';">
|
|
Technische Details anzeigen
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Technical Details -->
|
|
<div class="technical-details collapsed">
|
|
${this.renderTechnicalDetails(rawAuditTrail)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Setup download functionality
|
|
this.setupDownloadUpload();
|
|
|
|
} catch (error) {
|
|
console.error('[AI Interface] Error rendering audit trail:', error);
|
|
container.innerHTML = `
|
|
<div class="audit-trail-container">
|
|
<div class="audit-trail-header">
|
|
<div class="audit-trail-title">
|
|
<div class="audit-icon">
|
|
<div class="audit-icon-gradient">⚠️</div>
|
|
<h4>Audit Trail Fehler</h4>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="audit-trail-details">
|
|
<p style="color: var(--color-error); padding: 1rem;">
|
|
Fehler beim Rendern des Audit Trails. Details in der Konsole.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
calculateAuditStats(auditTrail) {
|
|
if (!auditTrail || auditTrail.length === 0) {
|
|
return {
|
|
totalTime: 0,
|
|
avgConfidence: 0,
|
|
stepCount: 0,
|
|
aiDecisionCount: 0,
|
|
embeddingsUsageCount: 0,
|
|
toolSelectionCount: 0,
|
|
qualityMetrics: {
|
|
avgProcessingTime: 0,
|
|
aiTransparency: 0
|
|
}
|
|
};
|
|
}
|
|
|
|
const totalTime = auditTrail.reduce((sum, entry) => sum + (entry.processingTimeMs || 0), 0);
|
|
const validConfidenceEntries = auditTrail.filter(entry => typeof entry.confidence === 'number');
|
|
const avgConfidence = validConfidenceEntries.length > 0
|
|
? Math.round(validConfidenceEntries.reduce((sum, entry) => sum + entry.confidence, 0) / validConfidenceEntries.length)
|
|
: 0;
|
|
|
|
const aiDecisionCount = auditTrail.filter(entry => entry.action === 'ai-decision').length;
|
|
const embeddingsUsageCount = auditTrail.filter(entry => entry.metadata?.embeddingsUsed).length;
|
|
const toolSelectionCount = auditTrail.filter(entry => entry.action === 'selection-decision').length;
|
|
|
|
const avgProcessingTime = auditTrail.length > 0 ? totalTime / auditTrail.length : 0;
|
|
const aiTransparency = auditTrail.length > 0 ?
|
|
(auditTrail.filter(entry => entry.metadata?.aiPrompt || entry.metadata?.reasoning).length / auditTrail.length) * 100 : 0;
|
|
|
|
return {
|
|
totalTime,
|
|
avgConfidence,
|
|
stepCount: auditTrail.length,
|
|
aiDecisionCount,
|
|
embeddingsUsageCount,
|
|
toolSelectionCount,
|
|
qualityMetrics: {
|
|
avgProcessingTime,
|
|
aiTransparency: Math.round(aiTransparency)
|
|
}
|
|
};
|
|
}
|
|
|
|
renderPhaseGroups(auditTrail, stats) {
|
|
// Group audit entries by phase
|
|
const phaseGroups = new Map();
|
|
|
|
auditTrail.forEach(entry => {
|
|
const phase = entry.phase || 'unknown';
|
|
if (!phaseGroups.has(phase)) {
|
|
phaseGroups.set(phase, []);
|
|
}
|
|
// FIXED: Remove non-null assertion, use proper null checking
|
|
const phaseEntries = phaseGroups.get(phase);
|
|
if (phaseEntries) {
|
|
phaseEntries.push(entry);
|
|
}
|
|
});
|
|
|
|
let html = '';
|
|
let groupIndex = 0;
|
|
|
|
for (const [phaseName, entries] of phaseGroups) {
|
|
const isLastPhase = groupIndex === phaseGroups.size - 1;
|
|
const phaseAvgConfidence = entries.reduce((sum, e) => sum + (e.confidence || 0), 0) / entries.length;
|
|
|
|
html += `
|
|
<div class="phase-group ${isLastPhase ? 'last-phase' : ''}">
|
|
<div class="phase-header" style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;">
|
|
<div style="display: flex; align-items: center; gap: 0.75rem;">
|
|
<div class="phase-icon">${this.getPhaseIcon(phaseName)}</div>
|
|
<div class="phase-name">${this.getPhaseDisplayName(phaseName)}</div>
|
|
</div>
|
|
<div class="phase-divider"></div>
|
|
<div class="phase-stats">
|
|
<div class="confidence-bar">
|
|
<div class="confidence-fill" style="width: ${phaseAvgConfidence}%; background-color: ${getConfidenceColor(phaseAvgConfidence)};"></div>
|
|
</div>
|
|
<span class="confidence-text">${Math.round(phaseAvgConfidence)}%</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="phase-entries">
|
|
${entries.map(entry => this.renderAuditEntry(entry)).join('')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
groupIndex++;
|
|
}
|
|
|
|
return html;
|
|
}
|
|
|
|
renderAuditEntry(entry) {
|
|
const confidenceColor = getConfidenceColor(entry.confidence || 0);
|
|
const processingTime = formatDuration(entry.processingTimeMs || 0);
|
|
|
|
return `
|
|
<div class="audit-entry">
|
|
<div class="entry-main">
|
|
<div class="entry-action">${this.getActionDisplayName(entry.action)}</div>
|
|
<div class="entry-meta">
|
|
<div class="confidence-indicator" style="background-color: ${confidenceColor};"></div>
|
|
<span class="confidence-value">${entry.confidence || 0}%</span>
|
|
<span class="processing-time">${processingTime}</span>
|
|
</div>
|
|
</div>
|
|
|
|
${this.renderEntryDetails(entry)}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
renderEntryDetails(entry) {
|
|
const details = [];
|
|
|
|
// AI-specific details
|
|
if (entry.action === 'ai-decision' && entry.metadata?.reasoning) {
|
|
details.push(`<div class="detail-item"><strong>Begründung:</strong> ${truncateText(entry.metadata.reasoning, 200)}</div>`);
|
|
}
|
|
|
|
// Tool selection details
|
|
if (entry.action === 'selection-decision') {
|
|
if (entry.metadata?.selectedToolsCount && entry.metadata?.availableToolsCount) {
|
|
details.push(`<div class="detail-item"><strong>Auswahl:</strong> ${entry.metadata.selectedToolsCount} von ${entry.metadata.availableToolsCount} Tools</div>`);
|
|
}
|
|
if (entry.metadata?.selectionMethod) {
|
|
details.push(`<div class="detail-item"><strong>Methode:</strong> ${entry.metadata.selectionMethod}</div>`);
|
|
}
|
|
}
|
|
|
|
// Embeddings details
|
|
if (entry.metadata?.embeddingsUsed && entry.metadata?.totalMatches) {
|
|
details.push(`<div class="detail-item"><strong>Semantische Treffer:</strong> ${entry.metadata.totalMatches}</div>`);
|
|
}
|
|
|
|
// Confidence factors
|
|
if (entry.metadata?.confidenceFactors?.length > 0) {
|
|
const factors = entry.metadata.confidenceFactors.slice(0, 2).join(', ');
|
|
details.push(`<div class="detail-item"><strong>Faktoren:</strong> ${factors}</div>`);
|
|
}
|
|
|
|
if (details.length === 0) return '';
|
|
|
|
return `
|
|
<div class="entry-details">
|
|
${details.join('')}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
renderTechnicalDetails(auditTrail) {
|
|
return auditTrail.map(entry => `
|
|
<div class="technical-entry">
|
|
<div class="technical-header">
|
|
<div class="technical-phase">${entry.phase}/${entry.action}</div>
|
|
<div class="technical-time">${new Date(entry.timestamp).toLocaleTimeString('de-DE')}</div>
|
|
</div>
|
|
<div class="technical-content">
|
|
${entry.metadata?.aiModel ? `<div class="technical-row">Model: ${entry.metadata.aiModel}</div>` : ''}
|
|
${entry.metadata?.promptTokens ? `<div class="technical-row">Tokens: ${entry.metadata.promptTokens}/${entry.metadata.completionTokens}</div>` : ''}
|
|
${entry.metadata?.microTaskType ? `<div class="technical-row">Task: ${entry.metadata.microTaskType}</div>` : ''}
|
|
${entry.metadata?.selectionMethod ? `<div class="technical-row">Method: ${entry.metadata.selectionMethod}</div>` : ''}
|
|
<div class="technical-row">Confidence: ${entry.confidence}% (${entry.processingTimeMs}ms)</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
getPhaseIcon(phase) {
|
|
const icons = {
|
|
'initialization': '🚀',
|
|
'tool-selection': '🔧',
|
|
'contextual-analysis': '🧠',
|
|
'workflow-phase': '⚡',
|
|
'tool-reasoning': '💭',
|
|
'knowledge-synthesis': '📚',
|
|
'confidence-scoring': '📊',
|
|
'phase-completion': '✅',
|
|
'completion': '🎯',
|
|
'embeddings': '🔍',
|
|
'unknown': '❓'
|
|
};
|
|
return icons[phase] || icons['unknown'];
|
|
}
|
|
|
|
getPhaseDisplayName(phase) {
|
|
const names = {
|
|
'initialization': 'Initialisierung',
|
|
'tool-selection': 'Tool-Auswahl',
|
|
'contextual-analysis': 'Kontext-Analyse',
|
|
'workflow-phase': 'Workflow-Phase',
|
|
'tool-reasoning': 'Tool-Bewertung',
|
|
'knowledge-synthesis': 'Wissens-Synthese',
|
|
'confidence-scoring': 'Vertrauenswertung',
|
|
'phase-completion': 'Phasen-Vervollständigung',
|
|
'completion': 'Abschluss',
|
|
'embeddings': 'Semantische Suche',
|
|
'unknown': 'Unbekannt'
|
|
};
|
|
return names[phase] || phase;
|
|
}
|
|
|
|
getActionDisplayName(action) {
|
|
const actions = {
|
|
'pipeline-start': 'Analyse gestartet',
|
|
'selection-decision': 'Tools ausgewählt',
|
|
'ai-decision': 'KI-Entscheidung',
|
|
'phase-tool-selection': 'Phasen-Tools evaluiert',
|
|
'tool-added-to-phase': 'Tool zur Phase hinzugefügt',
|
|
'concept-selection': 'Konzepte ausgewählt',
|
|
'tool-confidence': 'Vertrauen berechnet',
|
|
'phase-enhancement': 'Phase vervollständigt',
|
|
'similarity-search': 'Ähnlichkeitssuche',
|
|
'pipeline-end': 'Analyse abgeschlossen'
|
|
};
|
|
return actions[action] || action;
|
|
}
|
|
|
|
setupDownloadUpload() {
|
|
const downloadBtn = document.getElementById('download-results-btn');
|
|
const uploadInput = document.getElementById('upload-results');
|
|
|
|
if (downloadBtn) {
|
|
downloadBtn.addEventListener('click', () => this.downloadResults());
|
|
}
|
|
|
|
if (uploadInput) {
|
|
uploadInput.addEventListener('change', (event) => {
|
|
// FIXED: Remove TypeScript casting
|
|
const files = event.target.files;
|
|
const file = files ? files[0] : null;
|
|
if (file) {
|
|
this.handleFileUpload(file);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
downloadResults() {
|
|
if (!this.currentRecommendation) {
|
|
alert('Keine Analyse zum Herunterladen verfügbar.');
|
|
return;
|
|
}
|
|
|
|
const inputValue = this.elements.input ? this.elements.input.value : '';
|
|
|
|
const exportData = {
|
|
metadata: {
|
|
timestamp: new Date().toISOString(),
|
|
version: "1.0",
|
|
toolsDataHash: "current", // This would come from dataService
|
|
aiModel: "claude-sonnet-4", // This would come from aiService
|
|
aiParameters: {},
|
|
userQuery: inputValue,
|
|
mode: this.currentMode,
|
|
processingStats: {},
|
|
exportedBy: 'ForensicPathways'
|
|
},
|
|
recommendation: this.currentRecommendation,
|
|
auditTrail: this.currentRecommendation.auditTrail || [],
|
|
rawContext: {
|
|
selectedTools: [],
|
|
backgroundKnowledge: [],
|
|
contextHistory: [],
|
|
embeddingsSimilarities: {}
|
|
}
|
|
};
|
|
|
|
const blob = new Blob([JSON.stringify(exportData, null, 2)], {
|
|
type: 'application/json'
|
|
});
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `forensic-ai-analysis-${Date.now()}.json`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
|
|
console.log('[AI Interface] Analysis downloaded');
|
|
}
|
|
|
|
async handleFileUpload(file) {
|
|
try {
|
|
// Security validation
|
|
if (file.size > 10 * 1024 * 1024) {
|
|
throw new Error('Datei zu groß (max 10MB)');
|
|
}
|
|
|
|
if (!file.name.endsWith('.json')) {
|
|
throw new Error('Ungültiger Dateityp - nur JSON-Dateien werden unterstützt');
|
|
}
|
|
|
|
const text = await file.text();
|
|
let data;
|
|
|
|
try {
|
|
data = JSON.parse(text);
|
|
} catch (error) {
|
|
throw new Error('Ungültiges JSON-Format');
|
|
}
|
|
|
|
if (!this.validateUploadStructure(data)) {
|
|
throw new Error('Ungültiges Analyse-Dateiformat');
|
|
}
|
|
|
|
console.log('[AI Interface] Valid analysis file uploaded');
|
|
this.displayUploadedResults(data);
|
|
|
|
} catch (error) {
|
|
console.error('[AI Interface] Upload failed:', error);
|
|
alert(`Upload fehlgeschlagen: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
validateUploadStructure(data) {
|
|
try {
|
|
return !!(
|
|
data &&
|
|
typeof data === 'object' &&
|
|
data.metadata &&
|
|
typeof data.metadata === 'object' &&
|
|
typeof data.metadata.timestamp === 'string' &&
|
|
data.recommendation &&
|
|
typeof data.recommendation === 'object' &&
|
|
Array.isArray(data.auditTrail)
|
|
);
|
|
} catch (error) {
|
|
console.error('[AI Interface] Structure validation error:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
displayUploadedResults(data) {
|
|
try {
|
|
// Show upload banner
|
|
this.showUploadedBanner(data.metadata);
|
|
|
|
// Restore interface state
|
|
this.currentRecommendation = data.recommendation;
|
|
|
|
// Display the uploaded analysis
|
|
this.showResults();
|
|
|
|
// Update the recommendation display
|
|
if (data.metadata.mode === 'workflow') {
|
|
this.displayWorkflowResults(data.recommendation, data.metadata.userQuery);
|
|
} else {
|
|
this.displayToolResults(data.recommendation, data.metadata.userQuery);
|
|
}
|
|
|
|
// Render the uploaded audit trail
|
|
setTimeout(() => {
|
|
this.renderAuditTrail(data.auditTrail);
|
|
}, 100);
|
|
|
|
console.log('[AI Interface] Uploaded analysis displayed successfully');
|
|
} catch (error) {
|
|
console.error('[AI Interface] Error displaying uploaded results:', error);
|
|
alert('Fehler beim Anzeigen der hochgeladenen Analyse');
|
|
}
|
|
}
|
|
|
|
showUploadedBanner(metadata) {
|
|
const banner = `
|
|
<div class="uploaded-results-banner" style="background: var(--color-bg-secondary); border: 1px solid var(--color-warning); border-radius: 0.5rem; padding: 1rem; margin-bottom: 1rem; display: flex; align-items: center; gap: 1rem;">
|
|
<div style="font-size: 1.5rem;">📁</div>
|
|
<div style="flex: 1;">
|
|
<strong style="color: var(--color-warning);">Hochgeladene Analyse</strong>
|
|
<div style="font-size: 0.875rem; color: var(--color-text-secondary); margin-top: 0.25rem;">
|
|
Erstellt: ${new Date(metadata.timestamp).toLocaleString('de-DE')} |
|
|
Modus: ${metadata.mode} |
|
|
${metadata.toolsDataHash ? `Hash: ${metadata.toolsDataHash.slice(0, 8)}...` : ''}
|
|
</div>
|
|
</div>
|
|
<button onclick="this.parentElement.remove()" style="background: none; border: none; color: var(--color-text-secondary); cursor: pointer; font-size: 1.25rem;">×</button>
|
|
</div>
|
|
`;
|
|
|
|
// FIXED: Add null checking
|
|
if (this.elements && this.elements.results) {
|
|
this.elements.results.insertAdjacentHTML('afterbegin', banner);
|
|
}
|
|
}
|
|
|
|
renderHeader(title, query) {
|
|
return `
|
|
<div style="text-align: center; margin-bottom: 2rem; padding: 1.5rem; background: linear-gradient(135deg, var(--color-primary) 0%, #525252 100%); color: white; border-radius: 0.75rem;">
|
|
<h3 style="margin: 0 0 0.75rem 0; font-size: 1.5rem;">${title}</h3>
|
|
<p style="margin: 0; opacity: 0.9; line-height: 1.5;">
|
|
Basierend auf Ihrer Anfrage: "<em>${truncateText(query, 100)}</em>"
|
|
</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
renderContextualAnalysis(recommendation, mode) {
|
|
let html = '';
|
|
|
|
const analysisField = mode === 'workflow' ? recommendation.scenario_analysis : recommendation.problem_analysis;
|
|
if (analysisField) {
|
|
html += `
|
|
<div class="card contextual-analysis-card scenario">
|
|
<h4 style="margin: 0 0 1rem 0; color: var(--color-primary);">
|
|
${mode === 'workflow' ? 'Szenario-Analyse' : 'Problem-Analyse'}
|
|
</h4>
|
|
<div style="margin: 0; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word;">${sanitizeText(analysisField)}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
if (recommendation.investigation_approach) {
|
|
html += `
|
|
<div class="card contextual-analysis-card approach">
|
|
<h4 style="margin: 0 0 1rem 0; color: var(--color-accent);">
|
|
${mode === 'workflow' ? 'Untersuchungsansatz' : 'Lösungsansatz'}
|
|
</h4>
|
|
<div style="margin: 0; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word;">${sanitizeText(recommendation.investigation_approach)}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
if (recommendation.critical_considerations) {
|
|
html += `
|
|
<div class="card contextual-analysis-card critical">
|
|
<h4 style="margin: 0 0 1rem 0; color: var(--color-warning);">
|
|
${mode === 'workflow' ? 'Kritische Überlegungen' : 'Wichtige Voraussetzungen'}
|
|
</h4>
|
|
<div style="margin: 0; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word;">${sanitizeText(recommendation.critical_considerations)}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
return html;
|
|
}
|
|
|
|
renderBackgroundKnowledge(backgroundKnowledge) {
|
|
if (!backgroundKnowledge?.length) return '';
|
|
|
|
const conceptLinks = backgroundKnowledge.map(concept => `
|
|
<div style="background-color: var(--color-concept-bg); border: 1px solid var(--color-concept); border-radius: 0.5rem; padding: 1rem; margin-bottom: 0.75rem;">
|
|
<div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem;">
|
|
<button onclick="window.showToolDetails('${concept.concept_name}', 'secondary')"
|
|
style="background: none; border: none; color: var(--color-concept); font-weight: 600; cursor: pointer; text-decoration: underline;">
|
|
📚 ${concept.concept_name}
|
|
</button>
|
|
<span class="badge" style="background-color: var(--color-concept); color: white; font-size: 0.625rem;">Hintergrundwissen</span>
|
|
</div>
|
|
<div style="margin: 0; font-size: 0.8125rem; line-height: 1.5; color: var(--color-text-secondary); white-space: pre-wrap; word-wrap: break-word;">
|
|
${sanitizeText(concept.relevance)}
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
return `
|
|
<div class="card" style="margin-bottom: 2rem; border-left: 4px solid var(--color-concept);">
|
|
<h4 style="margin: 0 0 1rem 0; color: var(--color-concept);">Empfohlenes Hintergrundwissen</h4>
|
|
${conceptLinks}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
renderWorkflowPhases(toolsByPhase, phaseOrder, phaseNames) {
|
|
const phaseColors = [
|
|
'var(--color-primary)',
|
|
'var(--color-accent)',
|
|
'var(--color-warning)',
|
|
'var(--color-method)',
|
|
'var(--color-hosted)',
|
|
'var(--color-concept)'
|
|
];
|
|
|
|
return phaseOrder.map((phase, index) => {
|
|
const phaseTools = toolsByPhase[phase];
|
|
if (phaseTools.length === 0) return '';
|
|
|
|
const phaseColor = phaseColors[index % phaseColors.length];
|
|
|
|
return `
|
|
<div class="workflow-phase" style="margin-bottom: 2rem;">
|
|
<div class="phase-header" style="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: var(--transition-medium);"
|
|
onmouseover="this.style.borderColor='${phaseColor}'; this.style.boxShadow='var(--shadow-md)';"
|
|
onmouseout="this.style.borderColor='var(--color-border)'; this.style.boxShadow='';">
|
|
|
|
<div class="phase-number" style="width: 2rem; height: 2rem; background: ${phaseColor}; color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 600; flex-shrink: 0; box-shadow: var(--shadow-sm);">
|
|
${index + 1}
|
|
</div>
|
|
|
|
<div class="phase-info" style="display: flex; flex-direction: column; gap: 0.75rem; flex: 1; min-width: 0;">
|
|
<h3 class="phase-title" style="font-weight: 600; margin: 0; color: var(--color-text); font-size: 1.25rem;">
|
|
${phaseNames[phase]}
|
|
</h3>
|
|
|
|
<div class="phase-tools" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1rem;">
|
|
${phaseTools.map(tool => this.renderWorkflowTool(tool)).join('')}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
${index < phaseOrder.length - 1 ? `
|
|
<div class="workflow-arrow" style="display: flex; justify-content: center; margin: 1rem 0; color: ${phaseColor};">
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<line x1="12" y1="5" x2="12" y2="19"/>
|
|
<polyline points="19 12 12 19 5 12"/>
|
|
</svg>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
renderWorkflowTool(tool) {
|
|
const priorityColors = {
|
|
high: 'var(--color-error)',
|
|
medium: 'var(--color-warning)',
|
|
low: 'var(--color-accent)'
|
|
};
|
|
|
|
const priority = tool.recommendation ? tool.recommendation.priority : tool.priority;
|
|
const confidenceTooltip = tool.confidence ? this.renderConfidenceTooltip(tool.confidence) : '';
|
|
|
|
const cardClass = this.getToolClass(tool, 'recommendation');
|
|
|
|
return `
|
|
<div class="tool-recommendation ${cardClass}" onclick="window.showToolDetails('${tool.name}')"
|
|
style="position: relative; cursor: pointer; transition: var(--transition-fast);"
|
|
onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='var(--shadow-md)';"
|
|
onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='';">
|
|
|
|
<div class="tool-rec-header" style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.75rem;">
|
|
<h4 class="tool-rec-name" style="font-weight: 600; font-size: 1rem; margin: 0; color: var(--color-text); flex: 1; margin-right: 0.75rem;">
|
|
${tool.icon ? `<span style="margin-right: 0.5rem; font-size: 1.125rem;">${tool.icon}</span>` : ''}
|
|
${tool.name}
|
|
</h4>
|
|
<div style="display: flex; align-items: center; gap: 0.375rem; flex-shrink: 0;">
|
|
<span class="tool-rec-priority ${priority}"
|
|
style="background-color: ${priorityColors[priority]}; color: white; padding: 0.25rem 0.5rem; border-radius: 1rem; font-size: 0.75rem; font-weight: 500; text-transform: uppercase; position: relative; display: flex; align-items: center; gap: 0.25rem;">
|
|
${priority}
|
|
${confidenceTooltip}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tool-rec-justification" style="background-color: var(--color-bg-tertiary); padding: 0.75rem; border-radius: 0.375rem; border-left: 3px solid var(--color-primary); margin: 0.75rem 0; font-style: italic; white-space: pre-wrap; word-wrap: break-word; font-size: 0.875rem; line-height: 1.5; color: var(--color-text-secondary);">
|
|
"${sanitizeText(tool.justification || (tool.recommendation && tool.recommendation.justification) || `Empfohlen für ${tool.phase}`)}"
|
|
</div>
|
|
|
|
<div style="font-size: 0.75rem; color: var(--color-text-secondary); margin-top: auto;">
|
|
<div style="display: flex; flex-wrap: wrap; gap: 0.375rem; margin-bottom: 0.5rem;">
|
|
${this.renderToolBadges(tool)}
|
|
</div>
|
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
<span>${tool.type === 'method' ? 'Methode' : tool.platforms.slice(0, 2).join(', ')}</span>
|
|
<span>${tool.skillLevel}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
renderToolRecommendations(recommendedTools) {
|
|
if (!recommendedTools?.length) return '';
|
|
|
|
return `
|
|
<div class="tool-recommendations-grid" style="display: grid; gap: 1.5rem;">
|
|
${recommendedTools.map((toolRec, index) => {
|
|
const fullTool = tools.find(t => t.name === toolRec.name);
|
|
if (!fullTool) return '';
|
|
|
|
return this.renderDetailedTool(fullTool, toolRec, index + 1);
|
|
}).join('')}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
renderDetailedTool(tool, recommendation, rank) {
|
|
const rankColors = { 1: 'var(--color-accent)', 2: 'var(--color-primary)', 3: 'var(--color-warning)' };
|
|
const suitabilityColors = { high: 'var(--color-accent)', medium: 'var(--color-warning)', low: 'var(--color-text-secondary)' };
|
|
|
|
const confidenceTooltip = recommendation.confidence ? this.renderConfidenceTooltip(recommendation.confidence) : '';
|
|
|
|
return `
|
|
<div class="card ${this.getToolClass(tool, 'card')}" style="cursor: pointer; position: relative;" onclick="window.showToolDetails('${tool.name}')">
|
|
<div style="position: absolute; top: -8px; right: -8px; width: 32px; height: 32px; background-color: ${rankColors[rank]}; color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 1.125rem;">
|
|
${rank}
|
|
</div>
|
|
|
|
<div style="margin-bottom: 1rem;">
|
|
<h3 style="margin: 0 0 0.5rem 0;">${tool.name}</h3>
|
|
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; margin-bottom: 0.75rem;">
|
|
<span class="badge" style="background-color: ${suitabilityColors[recommendation.suitability_score]}; color: white; position: relative; display: flex; align-items: center; gap: 0.25rem;">
|
|
${this.getSuitabilityText(recommendation.suitability_score)}
|
|
${confidenceTooltip}
|
|
</span>
|
|
${this.renderToolBadges(tool)}
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin-bottom: 1.5rem;">
|
|
<h4 style="margin: 0.8rem 0 0.75rem 0; color: var(--color-accent);">Warum diese Methode?</h4>
|
|
<div style="margin: 0; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word;">${sanitizeText(recommendation.detailed_explanation)}</div>
|
|
|
|
${recommendation.implementation_approach ? `
|
|
<h4 style="margin: 0.8rem 0 0.75rem 0; color: var(--color-primary);">Anwendungsansatz</h4>
|
|
<div style="margin: 0; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word;">${sanitizeText(recommendation.implementation_approach)}</div>
|
|
` : ''}
|
|
</div>
|
|
|
|
${this.renderProsAndCons(recommendation.pros, recommendation.cons)}
|
|
${this.renderToolMetadata(tool)}
|
|
${recommendation.alternatives ? this.renderAlternatives(recommendation.alternatives) : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
renderConfidenceTooltip(confidence) {
|
|
if (!confidence || typeof confidence.overall !== 'number') return '';
|
|
|
|
const confidenceColor = getConfidenceColor(confidence.overall);
|
|
|
|
return `
|
|
<span class="confidence-tooltip-trigger"
|
|
style="display: inline-flex; align-items: center; gap: 0.125rem; cursor: help; margin-left: 0.25rem; position: relative;"
|
|
onmouseenter="this.querySelector('.confidence-tooltip').style.display = 'block'"
|
|
onmouseleave="this.querySelector('.confidence-tooltip').style.display = 'none'"
|
|
onclick="event.stopPropagation();">
|
|
<div style="width: 6px; height: 6px; border-radius: 50%; background-color: ${confidenceColor}; flex-shrink: 0;"></div>
|
|
<span style="font-size: 0.625rem; color: white; font-weight: 600; text-shadow: 0 1px 2px rgba(0,0,0,0.5);">${confidence.overall}%</span>
|
|
|
|
<div class="confidence-tooltip"
|
|
style="display: none; position: absolute; top: 100%; right: 0; z-index: 1001; background: var(--color-bg); border: 1px solid var(--color-border); border-radius: 0.5rem; padding: 1rem; min-width: 320px; max-width: 400px; box-shadow: var(--shadow-lg); font-size: 0.75rem; color: var(--color-text); margin-top: 0.5rem;">
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem;">
|
|
<strong style="font-size: 0.875rem; color: var(--color-primary);">KI-Vertrauenswertung</strong>
|
|
<span style="background-color: ${confidenceColor}; color: white; font-weight: 600; padding: 0.125rem 0.375rem; border-radius: 0.25rem; font-size: 0.625rem;">${confidence.overall}%</span>
|
|
</div>
|
|
|
|
<div style="display: grid; grid-template-columns: 1fr; gap: 0.625rem; margin-bottom: 0.75rem;">
|
|
<div style="background: var(--color-bg-secondary); padding: 0.5rem; border-radius: 0.375rem; border-left: 3px solid var(--color-accent);">
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.25rem;">
|
|
<span style="font-weight: 600; font-size: 0.6875rem; color: var(--color-text);">🔍 Semantische Relevanz</span>
|
|
<strong style="color: var(--color-accent);">${confidence.semanticRelevance}%</strong>
|
|
</div>
|
|
<div style="font-size: 0.625rem; color: var(--color-text-secondary); line-height: 1.3;">
|
|
Wie gut die Tool-Beschreibung semantisch zu Ihrer Anfrage passt
|
|
</div>
|
|
</div>
|
|
|
|
<div style="background: var(--color-bg-secondary); padding: 0.5rem; border-radius: 0.375rem; border-left: 3px solid var(--color-primary);">
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.25rem;">
|
|
<span style="font-weight: 600; font-size: 0.6875rem; color: var(--color-text);">🎯 Aufgaben-Eignung</span>
|
|
<strong style="color: var(--color-primary);">${confidence.taskSuitability}%</strong>
|
|
</div>
|
|
<div style="font-size: 0.625rem; color: var(--color-text-secondary); line-height: 1.3;">
|
|
KI-bewertete Eignung für Ihre spezifische Aufgabenstellung
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
${confidence.strengthIndicators && confidence.strengthIndicators.length > 0 ? `
|
|
<div style="margin-bottom: 0.75rem; padding: 0.5rem; background: var(--color-oss-bg); border-radius: 0.375rem; border-left: 3px solid var(--color-accent);">
|
|
<strong style="color: var(--color-accent); font-size: 0.6875rem; display: flex; align-items: center; gap: 0.25rem; margin-bottom: 0.375rem;">
|
|
<span>✓</span> Stärken dieser Empfehlung:
|
|
</strong>
|
|
<ul style="margin: 0; padding-left: 1rem; font-size: 0.625rem; line-height: 1.4; color: var(--color-text);">
|
|
${confidence.strengthIndicators.slice(0, 3).map(s => `<li style="margin-bottom: 0.25rem;">${sanitizeText(s)}</li>`).join('')}
|
|
</ul>
|
|
</div>
|
|
` : ''}
|
|
|
|
${confidence.uncertaintyFactors && confidence.uncertaintyFactors.length > 0 ? `
|
|
<div style="padding: 0.5rem; background: var(--color-hosted-bg); border-radius: 0.375rem; border-left: 3px solid var(--color-warning);">
|
|
<strong style="color: var(--color-warning); font-size: 0.6875rem; display: flex; align-items: center; gap: 0.25rem; margin-bottom: 0.375rem;">
|
|
<span>⚠</span> Mögliche Einschränkungen:
|
|
</strong>
|
|
<ul style="margin: 0; padding-left: 1rem; font-size: 0.625rem; line-height: 1.4; color: var(--color-text);">
|
|
${confidence.uncertaintyFactors.slice(0, 3).map(f => `<li style="margin-bottom: 0.25rem;">${sanitizeText(f)}</li>`).join('')}
|
|
</ul>
|
|
</div>
|
|
` : ''}
|
|
|
|
<div style="margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px solid var(--color-border); font-size: 0.625rem; color: var(--color-text-secondary); text-align: center;">
|
|
Forensisch fundierte KI-Analyse
|
|
</div>
|
|
</div>
|
|
</span>
|
|
`;
|
|
}
|
|
|
|
renderWorkflowSuggestion(suggestion) {
|
|
if (!suggestion) return '';
|
|
|
|
return `
|
|
<div class="card" style="margin-top: 2rem; border-left: 4px solid var(--color-accent);">
|
|
<h4 style="margin: 0 0 1rem 0; color: var(--color-accent);">Workflow-Empfehlung</h4>
|
|
<div style="margin: 0; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word;">${sanitizeText(suggestion)}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
renderAdditionalConsiderations(considerations) {
|
|
if (!considerations) return '';
|
|
|
|
return `
|
|
<div class="card" style="margin-top: 2rem; background-color: var(--color-bg-secondary); border-left: 4px solid var(--color-text-secondary);">
|
|
<h4 style="margin: 0 0 1rem 0; color: var(--color-text-secondary);">Zusätzliche Überlegungen</h4>
|
|
<div style="margin: 0; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word;">${sanitizeText(considerations)}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
renderProsAndCons(pros, cons) {
|
|
if (!pros?.length && !cons?.length) return '';
|
|
|
|
return `
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1.5rem;">
|
|
${pros?.length ? `
|
|
<div style="background-color: var(--color-oss-bg); padding: 1rem; border-radius: 0.5rem; border-left: 3px solid var(--color-accent);">
|
|
<h5 style="margin: 0 0 0.5rem 0; color: var(--color-accent); font-size: 0.875rem;">✓ Vorteile</h5>
|
|
<ul style="margin: 0; padding-left: 1rem;">
|
|
${pros.map(pro => `<li style="margin-bottom: 0.25rem;">${sanitizeText(pro)}</li>`).join('')}
|
|
</ul>
|
|
</div>
|
|
` : ''}
|
|
${cons?.length ? `
|
|
<div style="background-color: var(--color-hosted-bg); padding: 1rem; border-radius: 0.5rem; border-left: 3px solid var(--color-warning);">
|
|
<h5 style="margin: 0 0 0.5rem 0; color: var(--color-warning); font-size: 0.875rem;">✗ Nachteile</h5>
|
|
<ul style="margin: 0; padding-left: 1rem;">
|
|
${cons.map(con => `<li style="margin-bottom: 0.25rem;">${sanitizeText(con)}</li>`).join('')}
|
|
</ul>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
renderToolMetadata(tool) {
|
|
const isMethod = tool.type === 'method';
|
|
|
|
return `
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 0.75rem; font-size: 0.8125rem; color: var(--color-text-secondary); margin-bottom: 1rem; padding: 0.75rem; background-color: var(--color-bg-secondary); border-radius: 0.375rem;">
|
|
${!isMethod ? `<div><strong>Plattformen:</strong> ${tool.platforms.join(', ')}</div>` : ''}
|
|
<div><strong>Skill Level:</strong> ${tool.skillLevel}</div>
|
|
${!isMethod ? `<div><strong>Lizenz:</strong> ${tool.license}</div>` : ''}
|
|
<div><strong>Typ:</strong> ${isMethod ? 'Methode' : tool.accessType}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
renderAlternatives(alternatives) {
|
|
return `
|
|
<div style="background-color: var(--color-bg-secondary); padding: 1rem; border-radius: 0.5rem; margin-bottom: 1rem;">
|
|
<h5 style="margin: 0 0 0.5rem 0; color: var(--color-text-secondary); font-size: 0.875rem;">Alternative Ansätze</h5>
|
|
<div style="margin: 0; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word;">${sanitizeText(alternatives)}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
renderToolBadges(tool) {
|
|
const isMethod = tool.type === 'method';
|
|
const hasValidProjectUrl = isToolHosted(tool);
|
|
|
|
let badges = '';
|
|
if (isMethod) {
|
|
badges += '<span class="badge" style="background-color: var(--color-method); color: white;">Methode</span>';
|
|
} else if (hasValidProjectUrl) {
|
|
badges += '<span class="badge badge-primary">CC24-Server</span>';
|
|
} else if (tool.license !== 'Proprietary') {
|
|
badges += '<span class="badge badge-success">Open Source</span>';
|
|
}
|
|
|
|
if (tool.knowledgebase === true) {
|
|
badges += '<span class="badge badge-error">📖</span>';
|
|
}
|
|
|
|
return badges;
|
|
}
|
|
|
|
getToolClass(tool, context = 'card') {
|
|
const isMethod = tool.type === 'method';
|
|
const hasValidProjectUrl = isToolHosted(tool);
|
|
|
|
if (context === 'recommendation') {
|
|
if (isMethod) return 'method';
|
|
if (hasValidProjectUrl) return 'hosted';
|
|
if (tool.license !== 'Proprietary') return 'oss';
|
|
return '';
|
|
} else {
|
|
if (isMethod) return 'card-method';
|
|
if (hasValidProjectUrl) return 'card-hosted';
|
|
if (tool.license !== 'Proprietary') return 'card-oss';
|
|
return '';
|
|
}
|
|
}
|
|
|
|
getSuitabilityText(score) {
|
|
const texts = {
|
|
high: 'GUT GEEIGNET',
|
|
medium: 'GEEIGNET',
|
|
low: 'VIELLEICHT GEEIGNET'
|
|
};
|
|
return texts[score] || 'GEEIGNET';
|
|
}
|
|
|
|
showLoading() {
|
|
showElement(this.elements.loading);
|
|
}
|
|
|
|
hideLoading() {
|
|
hideElement(this.elements.loading);
|
|
}
|
|
|
|
showResults() {
|
|
showElement(this.elements.results);
|
|
}
|
|
|
|
hideResults() {
|
|
hideElement(this.elements.results);
|
|
hideElement(this.elements.error);
|
|
}
|
|
|
|
showError(message) {
|
|
if (this.elements.errorMessage) this.elements.errorMessage.textContent = message;
|
|
showElement(this.elements.error);
|
|
}
|
|
|
|
hideError() {
|
|
hideElement(this.elements.error);
|
|
}
|
|
|
|
restoreAIResults() {
|
|
if (this.currentRecommendation && this.elements.results) {
|
|
this.showResults();
|
|
|
|
if (this.currentRecommendation.auditTrail) {
|
|
setTimeout(() => {
|
|
try {
|
|
this.renderAuditTrail(this.currentRecommendation.auditTrail);
|
|
console.log('[AI Interface] Audit trail restored successfully');
|
|
} catch (error) {
|
|
console.error('[AI Interface] Audit trail restore failed:', error);
|
|
}
|
|
}, 100);
|
|
}
|
|
|
|
this.hideLoading();
|
|
this.hideError();
|
|
}
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const aiInterface = new AIQueryInterface();
|
|
|
|
window.restoreAIResults = () => aiInterface.restoreAIResults();
|
|
window.isToolHosted = window.isToolHosted || isToolHosted;
|
|
|
|
console.log('[AI Interface] Setup complete');
|
|
});
|
|
|
|
</script> |