consolidation of AI services

This commit is contained in:
overcuriousity 2025-08-11 10:32:08 +02:00
parent d043bba17f
commit 7b636ae051
20 changed files with 3425 additions and 3212 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,131 @@
---
// src/components/ui/AIQueryForm.astro
---
<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>
</div>

View File

@ -0,0 +1,78 @@
---
// src/components/ui/AIResults.astro
---
<!-- 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">
<!-- Results content will be populated by JavaScript -->
</div>

View File

@ -0,0 +1,370 @@
---
// src/components/ui/AuditTrailView.astro
/// <reference types="../../env" />
---
<div id="audit-trail-container"></div>
<script>
// Types for the audit trail system
interface AuditTrailOptions {
title?: string;
collapsible?: boolean;
defaultExpanded?: boolean;
}
interface ProcessedAuditTrail {
totalTime: number;
avgConfidence: number;
stepCount: number;
highConfidenceSteps: number;
lowConfidenceSteps: number;
phases: Array<{
name: string;
icon: string;
displayName: string;
avgConfidence: number;
totalTime: number;
entries: CompressedAuditEntry[];
}>;
summary: {
analysisQuality: 'excellent' | 'good' | 'fair' | 'poor';
keyInsights: string[];
potentialIssues: string[];
};
}
interface CompressedAuditEntry {
timestamp: number;
phase: string;
action: string;
inputSummary: string;
outputSummary: string;
confidence: number;
processingTimeMs: number;
metadata: Record<string, any>;
}
// Audit Trail Renderer Class with proper typing
class AuditTrailRenderer {
public containerId: string;
public options: AuditTrailOptions & {
title: string;
collapsible: boolean;
defaultExpanded: boolean;
};
componentId: string;
constructor(containerId: string, options: AuditTrailOptions = {}) {
this.containerId = containerId;
this.options = {
title: options.title || 'KI-Entscheidungspfad',
collapsible: options.collapsible !== false,
defaultExpanded: options.defaultExpanded || false,
...options
};
this.componentId = `audit-trail-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`;
}
public render(processedAudit: ProcessedAuditTrail | null): void {
const container = document.getElementById(this.containerId);
if (!container) {
console.error(`[AUDIT RENDERER] Container ${this.containerId} not found`);
return;
}
if (!processedAudit || !processedAudit.phases || processedAudit.phases.length === 0) {
this.renderEmpty(container);
return;
}
try {
this.renderProcessed(container, processedAudit);
setTimeout(() => this.attachEventHandlers(), 0);
} catch (error) {
console.error('[AUDIT RENDERER] Failed to render audit trail:', error);
this.renderError(container, error as Error);
}
}
renderProcessed(container: HTMLElement, processedAudit: ProcessedAuditTrail): void {
const detailsId = `${this.componentId}-details`;
container.innerHTML = `
<div class="audit-trail-container">
<div class="audit-trail-header ${this.options.collapsible ? 'clickable' : ''}"
${this.options.collapsible ? `data-target="${detailsId}"` : ''}>
<div class="audit-trail-title">
<div class="audit-icon">
<div class="audit-icon-gradient">✓</div>
<h4>${this.options.title}</h4>
</div>
<div class="audit-stats">
<div class="stat-item">
<div class="stat-dot stat-time"></div>
<span>${this.formatDuration(processedAudit.totalTime)}</span>
</div>
<div class="stat-item">
<div class="stat-dot" style="background-color: ${this.getConfidenceColor(processedAudit.avgConfidence)}"></div>
<span>${processedAudit.avgConfidence}% Vertrauen</span>
</div>
<div class="stat-item">
<span>${processedAudit.stepCount} Schritte</span>
</div>
</div>
</div>
${this.options.collapsible ? `
<div class="toggle-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"/>
</svg>
</div>
` : ''}
</div>
<div id="${detailsId}" class="audit-trail-details ${this.options.collapsible && !this.options.defaultExpanded ? 'collapsed' : ''}">
${this.renderSummary(processedAudit)}
${this.renderProcessFlow(processedAudit)}
</div>
</div>
`;
}
renderSummary(audit: ProcessedAuditTrail): string {
return `
<div class="audit-summary">
<div class="summary-header">📊 Analyse-Qualität</div>
<div class="summary-grid">
<div class="summary-stat">
<div class="summary-value success">${audit.highConfidenceSteps}</div>
<div class="summary-label">Hohe Sicherheit</div>
</div>
<div class="summary-stat">
<div class="summary-value ${audit.lowConfidenceSteps > 0 ? 'warning' : 'success'}">
${audit.lowConfidenceSteps}
</div>
<div class="summary-label">Unsichere Schritte</div>
</div>
<div class="summary-stat">
<div class="summary-value">${this.formatDuration(audit.totalTime)}</div>
<div class="summary-label">Verarbeitungszeit</div>
</div>
</div>
${audit.summary.keyInsights && audit.summary.keyInsights.length > 0 ? `
<div class="insights-section">
<div class="insights-header success">✓ Erkenntnisse:</div>
<ul class="insights-list">
${audit.summary.keyInsights.map(insight => `<li>${this.escapeHtml(insight)}</li>`).join('')}
</ul>
</div>
` : ''}
${audit.summary.potentialIssues && audit.summary.potentialIssues.length > 0 ? `
<div class="insights-section">
<div class="insights-header warning">⚠ Hinweise:</div>
<ul class="insights-list">
${audit.summary.potentialIssues.map(issue => `<li>${this.escapeHtml(issue)}</li>`).join('')}
</ul>
</div>
` : ''}
</div>
`;
}
renderProcessFlow(audit: ProcessedAuditTrail): string {
if (!audit.phases || audit.phases.length === 0) {
return '<div class="audit-process-flow"><p>Keine Phasen verfügbar</p></div>';
}
return `
<div class="audit-process-flow">
${audit.phases.map((phase, index) => `
<div class="phase-group ${index === audit.phases.length - 1 ? 'last-phase' : ''}">
<div class="phase-header" style="display: flex; align-items: center; gap: 1rem; margin-bottom: 0.75rem;">
<div class="phase-info" style="display: flex; align-items: center; gap: 0.75rem;">
<span class="phase-icon" style="font-size: 1rem;">${phase.icon || '📋'}</span>
<span class="phase-name" style="font-size: 0.875rem; font-weight: 500; color: var(--color-text);">${phase.displayName || phase.name}</span>
</div>
<div class="phase-divider" style="flex: 1; height: 1px; background-color: var(--color-border);"></div>
<div class="phase-stats" style="display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0;">
<div class="confidence-bar" style="width: 48px; height: 8px; background-color: var(--color-bg-tertiary); border-radius: 4px; overflow: hidden;">
<div class="confidence-fill"
style="height: 100%; width: ${phase.avgConfidence || 0}%; background-color: ${this.getConfidenceColor(phase.avgConfidence || 0)}; border-radius: 4px; transition: var(--transition-fast);">
</div>
</div>
<span class="confidence-text" style="font-size: 0.75rem; color: var(--color-text-secondary); min-width: 28px;">${phase.avgConfidence || 0}%</span>
</div>
</div>
<div class="phase-entries" style="margin-left: 1.5rem; display: grid; gap: 0.5rem;">
${(phase.entries || []).map(entry => this.renderAuditEntry(entry)).join('')}
</div>
</div>
`).join('')}
</div>
`;
}
renderAuditEntry(entry: CompressedAuditEntry): string {
return `
<div class="audit-entry" style="background-color: var(--color-bg); border: 1px solid var(--color-border); border-radius: 0.375rem; padding: 0.75rem; transition: var(--transition-fast);"
onmouseover="this.style.backgroundColor='var(--color-bg-secondary)'"
onmouseout="this.style.backgroundColor='var(--color-bg)'">
<div class="entry-main" style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.5rem;">
<span class="entry-action" style="font-size: 0.875rem; font-weight: 500; color: var(--color-text);">
${this.getActionDisplayName(entry.action)}
</span>
<div class="entry-meta" style="display: flex; align-items: center; gap: 0.5rem; font-size: 0.75rem; color: var(--color-text-secondary);">
<div class="confidence-indicator"
style="width: 8px; height: 8px; border-radius: 50%; background-color: ${this.getConfidenceColor(entry.confidence || 0)};">
</div>
<span class="confidence-value" style="min-width: 28px; text-align: right;">${entry.confidence || 0}%</span>
<span class="processing-time" style="min-width: 40px; text-align: right;">${entry.processingTimeMs || 0}ms</span>
</div>
</div>
${(entry.inputSummary && entry.inputSummary !== 'null') || (entry.outputSummary && entry.outputSummary !== 'null') ? `
<div class="entry-details" style="font-size: 0.75rem; color: var(--color-text-secondary); padding-top: 0.5rem; border-top: 1px solid var(--color-border);">
${entry.inputSummary && entry.inputSummary !== 'null' ? `
<div class="detail-item" style="margin-bottom: 0.25rem; word-break: break-word;">
<strong>Input:</strong> ${this.escapeHtml(entry.inputSummary)}
</div>
` : ''}
${entry.outputSummary && entry.outputSummary !== 'null' ? `
<div class="detail-item" style="margin-bottom: 0.25rem; word-break: break-word;">
<strong>Output:</strong> ${this.escapeHtml(entry.outputSummary)}
</div>
` : ''}
</div>
` : ''}
</div>
`;
}
getActionDisplayName(action: string): string {
const translations: Record<string, string> = {
'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'
};
return translations[action] || action;
}
renderEmpty(container: HTMLElement): void {
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>Kein Audit-Trail verfügbar</h4>
</div>
</div>
</div>
</div>
`;
}
renderError(container: HTMLElement, error: Error): void {
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" style="background: var(--color-error);">✗</div>
<h4>Audit-Trail Fehler</h4>
</div>
</div>
</div>
<div class="audit-summary">
<p style="color: var(--color-error);">
Fehler beim Laden der Audit-Informationen: ${this.escapeHtml(error.message)}
</p>
</div>
</div>
`;
}
attachEventHandlers(): void {
if (this.options.collapsible) {
const header = document.querySelector(`[data-target="${this.componentId}-details"]`) as HTMLElement;
const details = document.getElementById(`${this.componentId}-details`);
const toggleIcon = header?.querySelector('.toggle-icon svg') as SVGElement;
if (header && details && toggleIcon) {
header.addEventListener('click', () => {
const isCollapsed = details.classList.contains('collapsed');
details.classList.toggle('collapsed');
toggleIcon.style.transform = isCollapsed ? 'rotate(180deg)' : 'rotate(0deg)';
});
}
}
}
// Use global utilities if available, fallback to local implementations
formatDuration(ms: number): string {
if (typeof window !== 'undefined' && window.AuditUtils && window.AuditUtils.formatDuration) {
return window.AuditUtils.formatDuration(ms);
}
// Fallback implementation
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`;
}
getConfidenceColor(confidence: number): string {
if (typeof window !== 'undefined' && window.AuditUtils && window.AuditUtils.getConfidenceColor) {
return window.AuditUtils.getConfidenceColor(confidence);
}
// Fallback implementation
if (confidence >= 80) return 'var(--color-accent)';
if (confidence >= 60) return 'var(--color-warning)';
return 'var(--color-error)';
}
escapeHtml(text: string): string {
if (typeof window !== 'undefined' && window.AuditUtils && window.AuditUtils.escapeHtml) {
return window.AuditUtils.escapeHtml(text);
}
// Fallback implementation
if (typeof text !== 'string') return String(text);
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
declare global {
interface Window {
AuditTrailRenderer?: new (
containerId: string,
options?: {
title?: string;
collapsible?: boolean;
defaultExpanded?: boolean;
}
) => {
containerId: string;
options: {
title: string;
collapsible: boolean;
defaultExpanded: boolean;
};
render: (processedAudit: ProcessedAuditTrail | null) => void;
};
}
}
window.AuditTrailRenderer = AuditTrailRenderer;
</script>

74
src/config/appConfig.ts Normal file
View File

@ -0,0 +1,74 @@
// src/config/appConfig.ts
import dotenv from 'dotenv';
dotenv.config();
function getRequiredEnv(key: string): string {
const value = process.env[key];
if (!value) {
throw new Error(`Missing required environment variable: ${key}`);
}
return value;
}
function getEnvInt(key: string, defaultValue: number): number {
const value = process.env[key];
return value ? parseInt(value, 10) : defaultValue;
}
function getEnvFloat(key: string, defaultValue: number): number {
const value = process.env[key];
return value ? parseFloat(value) : defaultValue;
}
function getEnvBool(key: string, defaultValue: boolean): boolean {
const value = process.env[key];
return value ? value === 'true' : defaultValue;
}
export const config = {
ai: {
endpoint: getRequiredEnv('AI_ANALYZER_ENDPOINT'),
apiKey: getRequiredEnv('AI_ANALYZER_API_KEY'),
model: getRequiredEnv('AI_ANALYZER_MODEL'),
maxSelectedItems: getEnvInt('AI_MAX_SELECTED_ITEMS', 25),
embeddingCandidates: getEnvInt('AI_EMBEDDING_CANDIDATES', 50),
similarityThreshold: getEnvFloat('AI_SIMILARITY_THRESHOLD', 0.3),
microTaskDelay: getEnvInt('AI_MICRO_TASK_DELAY_MS', 500),
embeddingSelectionLimit: getEnvInt('AI_EMBEDDING_SELECTION_LIMIT', 30),
embeddingConceptsLimit: getEnvInt('AI_EMBEDDING_CONCEPTS_LIMIT', 15),
noEmbeddingsToolLimit: getEnvInt('AI_NO_EMBEDDINGS_TOOL_LIMIT', 25),
noEmbeddingsConceptLimit: getEnvInt('AI_NO_EMBEDDINGS_CONCEPT_LIMIT', 10),
embeddingsMinTools: getEnvInt('AI_EMBEDDINGS_MIN_TOOLS', 8),
embeddingsMaxReductionRatio: getEnvFloat('AI_EMBEDDINGS_MAX_REDUCTION_RATIO', 0.75),
methodSelectionRatio: getEnvFloat('AI_METHOD_SELECTION_RATIO', 0.4),
softwareSelectionRatio: getEnvFloat('AI_SOFTWARE_SELECTION_RATIO', 0.5),
maxContextTokens: getEnvInt('AI_MAX_CONTEXT_TOKENS', 4000),
maxPromptTokens: getEnvInt('AI_MAX_PROMPT_TOKENS', 1500),
taskTimeout: getEnvInt('AI_TASK_TIMEOUT_MS', 300000)
},
audit: {
enabled: getEnvBool('FORENSIC_AUDIT_ENABLED', false),
detailLevel: (process.env.FORENSIC_AUDIT_DETAIL_LEVEL || 'standard') as 'minimal' | 'standard' | 'verbose',
retentionHours: getEnvInt('FORENSIC_AUDIT_RETENTION_HOURS', 72),
maxEntries: getEnvInt('FORENSIC_AUDIT_MAX_ENTRIES', 50)
},
confidence: {
semanticWeight: getEnvFloat('CONFIDENCE_SEMANTIC_WEIGHT', 0.3),
suitabilityWeight: getEnvFloat('CONFIDENCE_SUITABILITY_WEIGHT', 0.7),
minimumThreshold: getEnvInt('CONFIDENCE_MINIMUM_THRESHOLD', 40),
mediumThreshold: getEnvInt('CONFIDENCE_MEDIUM_THRESHOLD', 60),
highThreshold: getEnvInt('CONFIDENCE_HIGH_THRESHOLD', 80)
},
rateLimit: {
delayMs: getEnvInt('AI_RATE_LIMIT_DELAY_MS', 2000),
window: 60 * 1000, // 1 minute
maxRequests: getEnvInt('AI_RATE_LIMIT_MAX_REQUESTS', 4),
microTaskTotalLimit: getEnvInt('AI_MICRO_TASK_TOTAL_LIMIT', 50)
}
} as const;
export type AppConfig = typeof config;

119
src/env.d.ts vendored
View File

@ -1,47 +1,134 @@
/// <reference path="../.astro/types.d.ts" /> /// <reference path="../.astro/types.d.ts" />
interface ProcessedAuditTrail {
totalTime: number;
avgConfidence: number;
stepCount: number;
highConfidenceSteps: number;
lowConfidenceSteps: number;
phases: Array<{
name: string;
icon: string;
displayName: string;
avgConfidence: number;
totalTime: number;
entries: CompressedAuditEntry[];
}>;
summary: {
analysisQuality: 'excellent' | 'good' | 'fair' | 'poor';
keyInsights: string[];
potentialIssues: string[];
};
}
interface CompressedAuditEntry {
timestamp: number;
phase: string;
action: string;
inputSummary: string;
outputSummary: string;
confidence: number;
processingTimeMs: number;
metadata: Record<string, any>;
}
declare global { declare global {
interface Window { interface Window {
// Theme utilities
themeUtils: { themeUtils: {
initTheme: () => void; initTheme: () => void;
toggleTheme: () => void; toggleTheme: () => void;
getStoredTheme: () => string; getStoredTheme: () => string;
getSystemTheme: () => string;
applyTheme: (theme: string) => void;
updateThemeToggle: (theme: string) => void;
}; };
// Tool utilities - THESE WERE MISSING
toolsData: any[]; toolsData: any[];
createToolSlug: (toolName: string) => string;
findToolByIdentifier: (tools: any[], identifier: string) => any | undefined;
isToolHosted: (tool: any) => boolean;
// Modal and UI utilities
showToolDetails: (toolName: string, modalType?: string) => void; showToolDetails: (toolName: string, modalType?: string) => void;
hideToolDetails: (modalType?: string) => void; hideToolDetails: (modalType?: string) => void;
hideAllToolDetails: () => void; hideAllToolDetails: () => void;
toggleKbEntry: (entryId: string) => void; toggleKbEntry: (entryId: string) => void;
toggleDomainAgnosticSection: (sectionId: string) => void; toggleDomainAgnosticSection: (sectionId: string) => void;
modalHideInProgress?: boolean;
// AI interface utilities
restoreAIResults?: () => void; restoreAIResults?: () => void;
switchToAIView?: () => void; switchToAIView?: () => void;
// Filter and search utilities - THESE WERE MISSING
clearTagFilters?: () => void; clearTagFilters?: () => void;
clearAllFilters?: () => void; clearAllFilters?: () => void;
applyScenarioSearch?: (scenarioId: string) => void;
selectPhase?: (phase: string) => void;
selectApproach?: (approach: string) => void;
prioritizeSearchResults: (tools: any[], searchTerm: string) => any[];
applyFilters?: () => void; // THIS WAS MISSING
createToolSlug: (toolName: string) => string; // Navigation utilities
findToolByIdentifier: (tools: any[], identifier: string) => any | undefined; navigateToGrid?: (toolName: string) => void;
isToolHosted: (tool: any) => boolean; navigateToMatrix?: (toolName: string) => void;
toggleAllScenarios?: () => void;
showShareDialog?: (shareButton: Element) => void;
// Scroll utilities
scrollToElement: (element: Element | null, options?: ScrollIntoViewOptions) => void;
scrollToElementById: (elementId: string, options?: ScrollIntoViewOptions) => void;
scrollToElementBySelector: (selector: string, options?: ScrollIntoViewOptions) => void;
// Authentication utilities
checkClientAuth: (context?: string) => Promise<{authenticated: boolean; authRequired: boolean; expires?: string}>; checkClientAuth: (context?: string) => Promise<{authenticated: boolean; authRequired: boolean; expires?: string}>;
requireClientAuth: (callback?: () => void, returnUrl?: string, context?: string) => Promise<boolean>; requireClientAuth: (callback?: () => void, returnUrl?: string, context?: string) => Promise<boolean>;
showIfAuthenticated: (selector: string, context?: string) => Promise<void>; showIfAuthenticated: (selector: string, context?: string) => Promise<void>;
setupAuthButtons: (selector?: string) => void; setupAuthButtons: (selector?: string) => void;
scrollToElement: (element: Element | null, options?: ScrollIntoViewOptions) => void; // Sharing utilities
scrollToElementById: (elementId: string, options?: ScrollIntoViewOptions) => void;
scrollToElementBySelector: (selector: string, options?: ScrollIntoViewOptions) => void;
applyScenarioSearch?: (scenarioId: string) => void;
selectPhase?: (phase: string) => void;
selectApproach?: (approach: string) => void;
navigateToGrid?: (toolName: string) => void;
navigateToMatrix?: (toolName: string) => void;
toggleAllScenarios?: () => void;
showShareDialog?: (shareButton: Element) => void;
modalHideInProgress?: boolean;
shareArticle: (button: HTMLElement, url: string, title: string) => Promise<void>; shareArticle: (button: HTMLElement, url: string, title: string) => Promise<void>;
shareCurrentArticle: (button: HTMLElement) => Promise<void>; shareCurrentArticle: (button: HTMLElement) => Promise<void>;
// Audit trail utilities
auditService?: {
processAuditTrail: (rawAuditTrail: any[]) => ProcessedAuditTrail | null;
formatDuration: (ms: number) => string;
getConfidenceColor: (confidence: number) => string;
getActionDisplayName: (action: string) => string;
isEnabled: () => boolean;
};
AuditUtils?: {
formatDuration: (ms: number) => string;
getConfidenceColor: (confidence: number) => string;
escapeHtml: (text: string) => string;
sanitizeText: (text: string) => string;
};
AuditTrailRenderer?: new (
containerId: string,
options?: {
title?: string;
collapsible?: boolean;
defaultExpanded?: boolean;
}
) => {
containerId: string;
options: {
title: string;
collapsible: boolean;
defaultExpanded: boolean;
};
render: (processedAudit: ProcessedAuditTrail | null) => void;
};
}
// EventTarget extension for better typing
interface EventTarget {
closest?: (selector: string) => Element | null;
} }
} }

View File

@ -1,4 +1,5 @@
--- ---
/// <reference types="../env" />
import Navigation from '../components/Navigation.astro'; import Navigation from '../components/Navigation.astro';
import Footer from '../components/Footer.astro'; import Footer from '../components/Footer.astro';
import '../styles/global.css'; import '../styles/global.css';
@ -25,133 +26,123 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
<link rel="icon" type="image/x-icon" href="/favicon.ico"> <link rel="icon" type="image/x-icon" href="/favicon.ico">
<script> <script>
// Import utility functions from consolidated modules
async function loadUtilityFunctions() { async function loadUtilityFunctions() {
try { try {
const { createToolSlug, findToolByIdentifier, isToolHosted } = await import('../utils/clientUtils.js'); const { createToolSlug, findToolByIdentifier, isToolHosted } = await import('../utils/toolHelpers.js');
(window as any).createToolSlug = createToolSlug; window.createToolSlug = createToolSlug;
(window as any).findToolByIdentifier = findToolByIdentifier; window.findToolByIdentifier = findToolByIdentifier;
(window as any).isToolHosted = isToolHosted; window.isToolHosted = isToolHosted;
} catch (error) { } catch (error) {
console.error('Failed to load utility functions:', error); console.error('Failed to load utility functions:', error);
(window as any).createToolSlug = (toolName: string) => { // Fallback implementation
window.createToolSlug = (toolName) => {
if (!toolName || typeof toolName !== 'string') return ''; if (!toolName || typeof toolName !== 'string') return '';
return toolName.toLowerCase().replace(/[^a-z0-9\s-]/g, '').replace(/\s+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, ''); return toolName.toLowerCase().replace(/[^a-z0-9\s-]/g, '').replace(/\s+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
}; };
} }
} }
function scrollToElement(element: Element | null, options = {}) { // Consolidated UI utility functions
if (!element) return; const UIUtils = {
scrollToElement(element, options = {}) {
if (!element) return;
setTimeout(() => { setTimeout(() => {
const headerHeight = document.querySelector('nav')?.offsetHeight || 80; const headerHeight = document.querySelector('nav')?.offsetHeight || 80;
const elementRect = element.getBoundingClientRect(); const elementRect = element.getBoundingClientRect();
const absoluteElementTop = elementRect.top + window.pageYOffset; const absoluteElementTop = elementRect.top + window.pageYOffset;
const targetPosition = absoluteElementTop - headerHeight - 20; const targetPosition = absoluteElementTop - headerHeight - 20;
window.scrollTo({ window.scrollTo({
top: targetPosition, top: targetPosition,
behavior: 'smooth' behavior: 'smooth'
});
}, 100);
},
scrollToElementById(elementId, options = {}) {
const element = document.getElementById(elementId);
if (element) {
this.scrollToElement(element, options);
}
},
scrollToElementBySelector(selector, options = {}) {
const element = document.querySelector(selector);
if (element) {
this.scrollToElement(element, options);
}
},
prioritizeSearchResults(tools, searchTerm) {
if (!searchTerm || !searchTerm.trim()) {
return tools;
}
const lowerSearchTerm = searchTerm.toLowerCase().trim();
return tools.sort((a, b) => {
const aTagsLower = (a.tags || []).map(tag => tag.toLowerCase());
const bTagsLower = (b.tags || []).map(tag => tag.toLowerCase());
const aExactTag = aTagsLower.includes(lowerSearchTerm);
const bExactTag = bTagsLower.includes(lowerSearchTerm);
if (aExactTag && !bExactTag) return -1;
if (!aExactTag && bExactTag) return 1;
return a.name.localeCompare(b.name);
}); });
}, 100);
}
function scrollToElementById(elementId: string, options = {}) {
const element = document.getElementById(elementId);
if (element) {
scrollToElement(element, options);
} }
} };
function scrollToElementBySelector(selector: string, options = {}) { // Theme management
const element = document.querySelector(selector); const ThemeManager = {
if (element) { THEME_KEY: 'dfir-theme',
scrollToElement(element, options);
}
}
function prioritizeSearchResults(tools: any[], searchTerm: string) { getSystemTheme() {
if (!searchTerm || !searchTerm.trim()) {
return tools;
}
const lowerSearchTerm = searchTerm.toLowerCase().trim();
return tools.sort((a, b) => {
const aTagsLower = (a.tags || []).map((tag: string) => tag.toLowerCase());
const bTagsLower = (b.tags || []).map((tag: string) => tag.toLowerCase());
const aExactTag = aTagsLower.includes(lowerSearchTerm);
const bExactTag = bTagsLower.includes(lowerSearchTerm);
if (aExactTag && !bExactTag) return -1;
if (!aExactTag && bExactTag) return 1;
return a.name.localeCompare(b.name);
});
}
(window as any).scrollToElement = scrollToElement;
(window as any).scrollToElementById = scrollToElementById;
(window as any).scrollToElementBySelector = scrollToElementBySelector;
(window as any).prioritizeSearchResults = prioritizeSearchResults;
document.addEventListener('DOMContentLoaded', () => {
loadUtilityFunctions();
const THEME_KEY = 'dfir-theme';
function getSystemTheme() {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
} },
function getStoredTheme() { getStoredTheme() {
return localStorage.getItem(THEME_KEY) || 'auto'; return localStorage.getItem(this.THEME_KEY) || 'auto';
} },
function applyTheme(theme: string) { applyTheme(theme) {
const effectiveTheme = theme === 'auto' ? getSystemTheme() : theme; const effectiveTheme = theme === 'auto' ? this.getSystemTheme() : theme;
document.documentElement.setAttribute('data-theme', effectiveTheme); document.documentElement.setAttribute('data-theme', effectiveTheme);
} },
function updateThemeToggle(theme: string) { updateThemeToggle(theme) {
document.querySelectorAll('[data-theme-toggle]').forEach(button => { document.querySelectorAll('[data-theme-toggle]').forEach(button => {
button.setAttribute('data-current-theme', theme); button.setAttribute('data-current-theme', theme);
}); });
} },
function initTheme() { initTheme() {
const storedTheme = getStoredTheme(); const storedTheme = this.getStoredTheme();
applyTheme(storedTheme); this.applyTheme(storedTheme);
updateThemeToggle(storedTheme); this.updateThemeToggle(storedTheme);
} },
function toggleTheme() { toggleTheme() {
const current = getStoredTheme(); const current = this.getStoredTheme();
const themes = ['light', 'dark', 'auto']; const themes = ['light', 'dark', 'auto'];
const currentIndex = themes.indexOf(current); const currentIndex = themes.indexOf(current);
const nextIndex = (currentIndex + 1) % themes.length; const nextIndex = (currentIndex + 1) % themes.length;
const nextTheme = themes[nextIndex]; const nextTheme = themes[nextIndex];
localStorage.setItem(THEME_KEY, nextTheme); localStorage.setItem(this.THEME_KEY, nextTheme);
applyTheme(nextTheme); this.applyTheme(nextTheme);
updateThemeToggle(nextTheme); this.updateThemeToggle(nextTheme);
} }
};
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { // Authentication utilities
if (getStoredTheme() === 'auto') { const AuthManager = {
applyTheme('auto'); async checkClientAuth(context = 'general') {
}
});
(window as any).themeUtils = {
initTheme,
toggleTheme,
getStoredTheme
};
async function checkClientAuth(context = 'general') {
try { try {
const response = await fetch('/api/auth/status'); const response = await fetch('/api/auth/status');
const data = await response.json(); const data = await response.json();
@ -183,10 +174,10 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
authRequired: true authRequired: true
}; };
} }
} },
async function requireClientAuth(callback: () => void, returnUrl: string, context = 'general') { async requireClientAuth(callback, returnUrl, context = 'general') {
const authStatus = await checkClientAuth(context); const authStatus = await this.checkClientAuth(context);
if (authStatus.authRequired && !authStatus.authenticated) { if (authStatus.authRequired && !authStatus.authenticated) {
const targetUrl = returnUrl || window.location.href; const targetUrl = returnUrl || window.location.href;
@ -198,43 +189,38 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
} }
return true; return true;
} }
} },
async function showIfAuthenticated(selector: string, context = 'general') { async showIfAuthenticated(selector, context = 'general') {
const authStatus = await checkClientAuth(context); const authStatus = await this.checkClientAuth(context);
const element = document.querySelector(selector); const element = document.querySelector(selector);
if (element) { if (element) {
(element as HTMLElement).style.display = (!authStatus.authRequired || authStatus.authenticated) element.style.display = (!authStatus.authRequired || authStatus.authenticated)
? 'inline-flex' ? 'inline-flex'
: 'none'; : 'none';
} }
} },
function setupAuthButtons(selector = '[data-contribute-button]') { setupAuthButtons(selector = '[data-contribute-button]') {
document.addEventListener('click', async (e) => { document.addEventListener('click', async (e) => {
if (!e.target) return; if (!e.target) return;
const button = (e.target as Element).closest(selector); const button = (e.target as HTMLElement).closest(selector) as HTMLAnchorElement;
if (!button) return; if (!button) return;
e.preventDefault(); e.preventDefault();
console.log('[AUTH] Contribute button clicked:', button.getAttribute('data-contribute-button')); await this.requireClientAuth(() => {
window.location.href = button.href;
await requireClientAuth(() => { }, button.href, 'contributions');
console.log('[AUTH] Navigation approved, redirecting to:', (button as HTMLAnchorElement).href);
window.location.href = (button as HTMLAnchorElement).href;
}, (button as HTMLAnchorElement).href, 'contributions');
}); });
} }
};
(window as any).checkClientAuth = checkClientAuth; // Sharing utilities
(window as any).requireClientAuth = requireClientAuth; const SharingUtils = {
(window as any).showIfAuthenticated = showIfAuthenticated; async copyUrlToClipboard(url, button) {
(window as any).setupAuthButtons = setupAuthButtons;
async function copyUrlToClipboard(url: string, button: HTMLElement) {
try { try {
await navigator.clipboard.writeText(url); await navigator.clipboard.writeText(url);
@ -265,27 +251,257 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
button.innerHTML = originalHTML; button.innerHTML = originalHTML;
}, 2000); }, 2000);
} }
} },
async function shareArticle(button: HTMLElement, url: string, title: string) { async shareArticle(button, url, title) {
const fullUrl = window.location.origin + url; const fullUrl = window.location.origin + url;
await copyUrlToClipboard(fullUrl, button); await this.copyUrlToClipboard(fullUrl, button);
},
async shareCurrentArticle(button) {
await this.copyUrlToClipboard(window.location.href, button);
} }
};
async function shareCurrentArticle(button: HTMLElement) { // Audit utilities
await copyUrlToClipboard(window.location.href, button); const AuditUtils = {
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`;
},
getConfidenceColor(confidence) {
if (confidence >= 80) return 'var(--color-accent)';
if (confidence >= 60) return 'var(--color-warning)';
return 'var(--color-error)';
},
escapeHtml(text) {
if (typeof text !== 'string') return String(text);
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
},
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();
} }
};
(window as any).shareArticle = shareArticle; // Filter and search functionality
(window as any).shareCurrentArticle = shareCurrentArticle; const FilterUtils = {
clearTagFilters() {
const tagFilters = document.querySelectorAll('.tag-filter.active');
tagFilters.forEach(filter => filter.classList.remove('active'));
this.updateResults();
},
initTheme(); clearAllFilters() {
setupAuthButtons('[data-contribute-button]'); // Clear search input
const searchInput = document.getElementById('search-input');
if (searchInput) (searchInput as HTMLInputElement).value = '';
// Clear domain filters
const domainFilters = document.querySelectorAll('.domain-filter.active');
domainFilters.forEach(filter => filter.classList.remove('active'));
// Clear phase filters
const phaseFilters = document.querySelectorAll('.phase-filter.active');
phaseFilters.forEach(filter => filter.classList.remove('active'));
// Clear tag filters
this.clearTagFilters();
// Clear other filters
const otherFilters = document.querySelectorAll('.filter-item.active');
otherFilters.forEach(filter => filter.classList.remove('active'));
this.updateResults();
},
updateResults() {
// Trigger filter update if function exists
if (typeof window.applyFilters === 'function') {
window.applyFilters();
}
}
};
// Navigation utilities
const NavigationUtils = {
navigateToGrid(toolName) {
if (toolName) {
const slug = window.createToolSlug(toolName);
window.location.href = `/?tool=${slug}&view=grid`;
} else {
window.location.href = '/?view=grid';
}
},
navigateToMatrix(toolName) {
if (toolName) {
const slug = window.createToolSlug(toolName);
window.location.href = `/?tool=${slug}&view=matrix`;
} else {
window.location.href = '/?view=matrix';
}
},
applyScenarioSearch(scenarioId) {
const searchInput = document.getElementById('search-input');
if (searchInput && scenarioId) {
(searchInput as HTMLInputElement).value = scenarioId;
if (typeof window.applyFilters === 'function') {
window.applyFilters();
}
}
},
selectPhase(phase) {
const phaseFilter = document.querySelector(`[data-phase="${phase}"]`);
if (phaseFilter) {
phaseFilter.classList.add('active');
FilterUtils.updateResults();
}
},
selectApproach(approach) {
const approachFilter = document.querySelector(`[data-approach="${approach}"]`);
if (approachFilter) {
approachFilter.classList.add('active');
FilterUtils.updateResults();
}
}
};
// Modal and UI state management
const ModalUtils = {
showShareDialog(shareButton) {
// Implementation for share dialog
const url = window.location.href;
SharingUtils.shareCurrentArticle(shareButton);
},
toggleAllScenarios() {
const scenarioCards = document.querySelectorAll('.scenario-card');
const expandedCards = document.querySelectorAll('.scenario-card.expanded');
if (expandedCards.length === scenarioCards.length) {
// Collapse all
scenarioCards.forEach(card => card.classList.remove('expanded'));
} else {
// Expand all
scenarioCards.forEach(card => card.classList.add('expanded'));
}
},
toggleKbEntry(entryId) {
const entry = document.getElementById(entryId);
if (entry) {
entry.classList.toggle('expanded');
}
},
toggleDomainAgnosticSection(sectionId) {
const section = document.getElementById(sectionId);
if (section) {
section.classList.toggle('collapsed');
}
}
};
// Initialize audit service if available
async function initializeAuditService() {
try {
const { auditService } = await import('../utils/auditService.js');
window.auditService = auditService;
} catch (error) {
console.warn('Audit service not available:', error);
}
}
// Expose utilities to global scope
Object.assign(window, {
// UI utilities
scrollToElement: UIUtils.scrollToElement.bind(UIUtils),
scrollToElementById: UIUtils.scrollToElementById.bind(UIUtils),
scrollToElementBySelector: UIUtils.scrollToElementBySelector.bind(UIUtils),
prioritizeSearchResults: UIUtils.prioritizeSearchResults.bind(UIUtils),
// Theme utilities
themeUtils: ThemeManager,
// Auth utilities
checkClientAuth: AuthManager.checkClientAuth.bind(AuthManager),
requireClientAuth: AuthManager.requireClientAuth.bind(AuthManager),
showIfAuthenticated: AuthManager.showIfAuthenticated.bind(AuthManager),
setupAuthButtons: AuthManager.setupAuthButtons.bind(AuthManager),
// Sharing utilities
shareArticle: SharingUtils.shareArticle.bind(SharingUtils),
shareCurrentArticle: SharingUtils.shareCurrentArticle.bind(SharingUtils),
// Filter utilities
clearTagFilters: FilterUtils.clearTagFilters.bind(FilterUtils),
clearAllFilters: FilterUtils.clearAllFilters.bind(FilterUtils),
// Navigation utilities
navigateToGrid: NavigationUtils.navigateToGrid.bind(NavigationUtils),
navigateToMatrix: NavigationUtils.navigateToMatrix.bind(NavigationUtils),
applyScenarioSearch: NavigationUtils.applyScenarioSearch.bind(NavigationUtils),
selectPhase: NavigationUtils.selectPhase.bind(NavigationUtils),
selectApproach: NavigationUtils.selectApproach.bind(NavigationUtils),
// Modal utilities
showShareDialog: ModalUtils.showShareDialog.bind(ModalUtils),
toggleAllScenarios: ModalUtils.toggleAllScenarios.bind(ModalUtils),
toggleKbEntry: ModalUtils.toggleKbEntry.bind(ModalUtils),
toggleDomainAgnosticSection: ModalUtils.toggleDomainAgnosticSection.bind(ModalUtils),
// Audit utilities
AuditUtils: AuditUtils
});
document.addEventListener('DOMContentLoaded', async () => {
await loadUtilityFunctions();
await initializeAuditService();
// Initialize theme
ThemeManager.initTheme();
// Setup auth buttons
AuthManager.setupAuthButtons('[data-contribute-button]');
// Initialize AI button visibility
const initAIButton = async () => { const initAIButton = async () => {
await showIfAuthenticated('#ai-view-toggle', 'ai'); await AuthManager.showIfAuthenticated('#ai-view-toggle', 'ai');
}; };
initAIButton(); initAIButton();
// Listen for system theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
if (ThemeManager.getStoredTheme() === 'auto') {
ThemeManager.applyTheme('auto');
}
});
console.log('[BaseLayout] All utilities initialized successfully');
}); });
</script> </script>
</head> </head>

View File

@ -1,24 +1,15 @@
// src/pages/api/ai/enhance-input.ts - Enhanced AI service compatibility // src/pages/api/ai/enhance-input.ts
import type { APIRoute } from 'astro'; import type { APIRoute } from 'astro';
import { withAPIAuth } from '../../../utils/auth.js'; import { withAPIAuth } from '../../../utils/auth.js';
import { apiError, apiServerError, createAuthErrorResponse } from '../../../utils/api.js'; import { apiError, apiServerError, createAuthErrorResponse } from '../../../utils/api.js';
import { enqueueApiCall } from '../../../utils/rateLimitedQueue.js'; import { enqueueApiCall } from '../../../utils/rateLimitedQueue.js';
import { aiService } from '../../../services/ai/aiService.js';
import { config } from '../../../config/appConfig.js';
import { logger } from '../../../services/logger.js';
export const prerender = false; export const prerender = false;
function getEnv(key: string): string {
const value = process.env[key];
if (!value) {
throw new Error(`Missing environment variable: ${key}`);
}
return value;
}
const AI_ENDPOINT = getEnv('AI_ANALYZER_ENDPOINT');
const AI_ANALYZER_API_KEY = getEnv('AI_ANALYZER_API_KEY');
const AI_ANALYZER_MODEL = getEnv('AI_ANALYZER_MODEL');
const rateLimitStore = new Map<string, { count: number; resetTime: number }>(); const rateLimitStore = new Map<string, { count: number; resetTime: number }>();
const RATE_LIMIT_WINDOW = 60 * 1000; const RATE_LIMIT_WINDOW = 60 * 1000;
const RATE_LIMIT_MAX = 5; const RATE_LIMIT_MAX = 5;
@ -95,39 +86,6 @@ ${input}
`.trim(); `.trim();
} }
async function callAIService(prompt: string): Promise<Response> {
const endpoint = AI_ENDPOINT;
const apiKey = AI_ANALYZER_API_KEY;
const model = AI_ANALYZER_MODEL;
let headers: Record<string, string> = {
'Content-Type': 'application/json'
};
if (apiKey) {
headers['Authorization'] = `Bearer ${apiKey}`;
console.log('[ENHANCE API] Using API key authentication');
} else {
console.log('[ENHANCE API] No API key - making request without authentication');
}
const requestBody = {
model,
messages: [{ role: 'user', content: prompt }],
max_tokens: 300,
temperature: 0.7,
top_p: 0.9,
frequency_penalty: 0.2,
presence_penalty: 0.1
};
return fetch(`${endpoint}/v1/chat/completions`, {
method: 'POST',
headers,
body: JSON.stringify(requestBody)
});
}
export const POST: APIRoute = async ({ request }) => { export const POST: APIRoute = async ({ request }) => {
try { try {
const authResult = await withAPIAuth(request, 'ai'); const authResult = await withAPIAuth(request, 'ai');
@ -145,39 +103,52 @@ export const POST: APIRoute = async ({ request }) => {
const { input } = body; const { input } = body;
if (!input || typeof input !== 'string' || input.length < 40) { if (!input || typeof input !== 'string' || input.length < 40) {
logger.api('POST', '/api/ai/enhance-input', 400, {
error: 'Input too short',
userId,
inputLength: input?.length || 0
});
return apiError.badRequest('Input too short for enhancement (minimum 40 characters)'); return apiError.badRequest('Input too short for enhancement (minimum 40 characters)');
} }
const sanitizedInput = sanitizeInput(input); const sanitizedInput = sanitizeInput(input);
if (sanitizedInput.length < 40) { if (sanitizedInput.length < 40) {
logger.api('POST', '/api/ai/enhance-input', 400, {
error: 'Input too short after sanitization',
userId
});
return apiError.badRequest('Input too short after sanitization'); return apiError.badRequest('Input too short after sanitization');
} }
const systemPrompt = createEnhancementPrompt(sanitizedInput); const systemPrompt = createEnhancementPrompt(sanitizedInput);
const taskId = `enhance_${userId}_${Date.now()}_${Math.random().toString(36).substr(2, 4)}`; const taskId = `enhance_${userId}_${Date.now()}_${Math.random().toString(36).substr(2, 4)}`;
const aiResponse = await enqueueApiCall(() => callAIService(systemPrompt), taskId); logger.info('enhance-input', 'Processing enhancement request', {
userId,
inputLength: sanitizedInput.length,
taskId
});
if (!aiResponse.ok) { const aiResponse = await enqueueApiCall(() =>
const errorText = await aiResponse.text(); aiService.call({
console.error('[ENHANCE API] AI enhancement error:', errorText, 'Status:', aiResponse.status); prompt: systemPrompt,
maxTokens: 300,
temperature: 0.7,
context: 'input-enhancement'
})
, taskId);
if (!aiResponse.content) {
logger.error('enhance-input', 'No enhancement response received', undefined, {
userId,
taskId
});
return apiServerError.unavailable('Enhancement service unavailable'); return apiServerError.unavailable('Enhancement service unavailable');
} }
const aiData = await aiResponse.json();
const aiContent = aiData.choices?.[0]?.message?.content;
if (!aiContent) {
return apiServerError.unavailable('No enhancement response');
}
let questions; let questions;
try { try {
const cleanedContent = aiContent questions = aiService.parseJSONResponse(aiResponse.content, []);
.replace(/^```json\s*/i, '')
.replace(/\s*```\s*$/, '')
.trim();
questions = JSON.parse(cleanedContent);
if (!Array.isArray(questions)) { if (!Array.isArray(questions)) {
throw new Error('Response is not an array'); throw new Error('Response is not an array');
@ -199,11 +170,22 @@ export const POST: APIRoute = async ({ request }) => {
} }
} catch (error) { } catch (error) {
console.error('Failed to parse enhancement response:', aiContent); logger.warn('enhance-input', 'Failed to parse enhancement response', {
userId,
taskId,
error: error.message,
responsePreview: aiResponse.content.slice(0, 100)
});
questions = []; questions = [];
} }
console.log(`[ENHANCE API] User: ${userId}, Forensics Questions: ${questions.length}, Input length: ${sanitizedInput.length}`); logger.api('POST', '/api/ai/enhance-input', 200, {
userId,
taskId,
inputLength: sanitizedInput.length,
questionsGenerated: questions.length,
processingTime: `${aiResponse.requestTimeMs}ms`
});
return new Response(JSON.stringify({ return new Response(JSON.stringify({
success: true, success: true,
@ -216,7 +198,9 @@ export const POST: APIRoute = async ({ request }) => {
}); });
} catch (error) { } catch (error) {
console.error('Enhancement error:', error); logger.error('enhance-input', 'Enhancement processing failed', error as Error, {
endpoint: '/api/ai/enhance-input'
});
return apiServerError.internal('Enhancement processing failed'); return apiServerError.internal('Enhancement processing failed');
} }
}; };

View File

@ -4,7 +4,9 @@ import type { APIRoute } from 'astro';
import { withAPIAuth } from '../../../utils/auth.js'; import { withAPIAuth } from '../../../utils/auth.js';
import { apiError, apiServerError, createAuthErrorResponse } from '../../../utils/api.js'; import { apiError, apiServerError, createAuthErrorResponse } from '../../../utils/api.js';
import { enqueueApiCall } from '../../../utils/rateLimitedQueue.js'; import { enqueueApiCall } from '../../../utils/rateLimitedQueue.js';
import { aiPipeline } from '../../../utils/aiPipeline.js'; import { pipelineOrchestrator } from '../../../services/ai/pipelineOrchestrator.js';
import { config } from '../../../config/appConfig.js';
import { logger } from '../../../services/logger.js';
export const prerender = false; export const prerender = false;
@ -16,10 +18,6 @@ interface RateLimitData {
const rateLimitStore = new Map<string, RateLimitData>(); const rateLimitStore = new Map<string, RateLimitData>();
const RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute
const MAIN_RATE_LIMIT_MAX = parseInt(process.env.AI_RATE_LIMIT_MAX_REQUESTS || '4', 10);
const MICRO_TASK_TOTAL_LIMIT = parseInt(process.env.AI_MICRO_TASK_TOTAL_LIMIT || '50', 10);
function sanitizeInput(input: string): string { function sanitizeInput(input: string): string {
let sanitized = input let sanitized = input
.replace(/```[\s\S]*?```/g, '[CODE_BLOCK_REMOVED]') .replace(/```[\s\S]*?```/g, '[CODE_BLOCK_REMOVED]')
@ -39,26 +37,26 @@ function checkRateLimit(userId: string): { allowed: boolean; reason?: string; mi
if (!userLimit || now > userLimit.resetTime) { if (!userLimit || now > userLimit.resetTime) {
rateLimitStore.set(userId, { rateLimitStore.set(userId, {
count: 1, count: 1,
resetTime: now + RATE_LIMIT_WINDOW, resetTime: now + config.rateLimit.window,
microTaskCount: 0 microTaskCount: 0
}); });
return { return {
allowed: true, allowed: true,
microTasksRemaining: MICRO_TASK_TOTAL_LIMIT microTasksRemaining: config.rateLimit.microTaskTotalLimit
}; };
} }
if (userLimit.count >= MAIN_RATE_LIMIT_MAX) { if (userLimit.count >= config.rateLimit.maxRequests) {
return { return {
allowed: false, allowed: false,
reason: `Main rate limit exceeded. Max ${MAIN_RATE_LIMIT_MAX} requests per minute.` reason: `Main rate limit exceeded. Max ${config.rateLimit.maxRequests} requests per minute.`
}; };
} }
if (userLimit.microTaskCount >= MICRO_TASK_TOTAL_LIMIT) { if (userLimit.microTaskCount >= config.rateLimit.microTaskTotalLimit) {
return { return {
allowed: false, allowed: false,
reason: `Micro-task limit exceeded. Max ${MICRO_TASK_TOTAL_LIMIT} AI calls per minute.` reason: `Micro-task limit exceeded. Max ${config.rateLimit.microTaskTotalLimit} AI calls per minute.`
}; };
} }
@ -66,7 +64,7 @@ function checkRateLimit(userId: string): { allowed: boolean; reason?: string; mi
return { return {
allowed: true, allowed: true,
microTasksRemaining: MICRO_TASK_TOTAL_LIMIT - userLimit.microTaskCount microTasksRemaining: config.rateLimit.microTaskTotalLimit - userLimit.microTaskCount
}; };
} }
@ -74,7 +72,11 @@ function incrementMicroTaskCount(userId: string, aiCallsMade: number): void {
const userLimit = rateLimitStore.get(userId); const userLimit = rateLimitStore.get(userId);
if (userLimit) { if (userLimit) {
userLimit.microTaskCount += aiCallsMade; userLimit.microTaskCount += aiCallsMade;
console.log(`[RATE LIMIT] User ${userId} now at ${userLimit.microTaskCount}/${MICRO_TASK_TOTAL_LIMIT} micro-task calls`); logger.debug('rate-limit', `User micro-task usage updated`, {
userId,
currentCount: userLimit.microTaskCount,
limit: config.rateLimit.microTaskTotalLimit
});
} }
} }
@ -95,7 +97,10 @@ function cleanupExpiredRateLimits() {
const toRemove = entries.slice(0, entries.length - maxStoreSize); const toRemove = entries.slice(0, entries.length - maxStoreSize);
toRemove.forEach(([userId]) => rateLimitStore.delete(userId)); toRemove.forEach(([userId]) => rateLimitStore.delete(userId));
console.log(`[RATE LIMIT] Cleanup: removed ${toRemove.length} old entries`); logger.info('rate-limit', `Cleanup completed`, {
removed: toRemove.length,
remaining: rateLimitStore.size
});
} }
} }
@ -118,55 +123,60 @@ export const POST: APIRoute = async ({ request }) => {
const body = await request.json(); const body = await request.json();
const { query, mode = 'workflow', taskId: clientTaskId } = body; const { query, mode = 'workflow', taskId: clientTaskId } = body;
console.log(`[MICRO-TASK API] Received request - TaskId: ${clientTaskId}, Mode: ${mode}, Query length: ${query?.length || 0}`);
console.log(`[MICRO-TASK API] Micro-task calls remaining: ${rateLimitResult.microTasksRemaining}`);
if (!query || typeof query !== 'string') { if (!query || typeof query !== 'string') {
console.log(`[MICRO-TASK API] Invalid query for task ${clientTaskId}`); logger.api('POST', '/api/ai/query', 400, { error: 'Invalid query', userId });
return apiError.badRequest('Query required'); return apiError.badRequest('Query required');
} }
if (!['workflow', 'tool'].includes(mode)) { if (!['workflow', 'tool'].includes(mode)) {
console.log(`[MICRO-TASK API] Invalid mode for task ${clientTaskId}: ${mode}`); logger.api('POST', '/api/ai/query', 400, { error: 'Invalid mode', userId, mode });
return apiError.badRequest('Invalid mode. Must be "workflow" or "tool"'); return apiError.badRequest('Invalid mode. Must be "workflow" or "tool"');
} }
const sanitizedQuery = sanitizeInput(query); const sanitizedQuery = sanitizeInput(query);
if (sanitizedQuery.includes('[FILTERED]')) { if (sanitizedQuery.includes('[FILTERED]')) {
console.log(`[MICRO-TASK API] Filtered input detected for task ${clientTaskId}`); logger.api('POST', '/api/ai/query', 400, { error: 'Filtered input', userId });
return apiError.badRequest('Invalid input detected'); return apiError.badRequest('Invalid input detected');
} }
const taskId = clientTaskId || `ai_${userId}_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`; const taskId = clientTaskId || `ai_${userId}_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
console.log(`[MICRO-TASK API] About to enqueue micro-task pipeline ${taskId}`); logger.api('POST', '/api/ai/query', 200, {
taskId,
mode,
queryLength: sanitizedQuery.length,
userId,
microTasksRemaining: rateLimitResult.microTasksRemaining
});
const result = await enqueueApiCall(() => const result = await enqueueApiCall(() =>
aiPipeline.processQuery(sanitizedQuery, mode) pipelineOrchestrator.processQuery(sanitizedQuery, mode)
, taskId); , taskId);
if (!result || !result.recommendation) { if (!result || !result.recommendation) {
return apiServerError.unavailable('No response from micro-task AI pipeline'); return apiServerError.unavailable('No response from AI pipeline');
} }
const stats = result.processingStats; const stats = result.processingStats;
const estimatedAICallsMade = stats.microTasksCompleted + stats.microTasksFailed; const estimatedAICallsMade = stats.microTasksCompleted + stats.microTasksFailed;
incrementMicroTaskCount(userId, estimatedAICallsMade); incrementMicroTaskCount(userId, estimatedAICallsMade);
console.log(`[MICRO-TASK API] Pipeline completed for ${taskId}:`); logger.api('POST', '/api/ai/query', 200, {
console.log(` - Mode: ${mode}`); taskId,
console.log(` - User: ${userId}`); mode,
console.log(` - Query length: ${sanitizedQuery.length}`); userId,
console.log(` - Processing time: ${stats.processingTimeMs}ms`); queryLength: sanitizedQuery.length,
console.log(` - Micro-tasks completed: ${stats.microTasksCompleted}`); processingTime: `${stats.processingTimeMs}ms`,
console.log(` - Micro-tasks failed: ${stats.microTasksFailed}`); microTasksCompleted: stats.microTasksCompleted,
console.log(` - Estimated AI calls: ${estimatedAICallsMade}`); microTasksFailed: stats.microTasksFailed,
console.log(` - Embeddings used: ${stats.embeddingsUsed}`); estimatedAICallsMade,
console.log(` - Final items: ${stats.finalSelectedItems}`); embeddingsUsed: stats.embeddingsUsed,
finalItems: stats.finalSelectedItems
});
const currentLimit = rateLimitStore.get(userId); const currentLimit = rateLimitStore.get(userId);
const remainingMicroTasks = currentLimit ? const remainingMicroTasks = currentLimit ?
MICRO_TASK_TOTAL_LIMIT - currentLimit.microTaskCount : MICRO_TASK_TOTAL_LIMIT; config.rateLimit.microTaskTotalLimit - currentLimit.microTaskCount : config.rateLimit.microTaskTotalLimit;
return new Response(JSON.stringify({ return new Response(JSON.stringify({
success: true, success: true,
@ -182,9 +192,9 @@ export const POST: APIRoute = async ({ request }) => {
estimatedAICallsMade estimatedAICallsMade
}, },
rateLimitInfo: { rateLimitInfo: {
mainRequestsRemaining: MAIN_RATE_LIMIT_MAX - (currentLimit?.count || 0), mainRequestsRemaining: config.rateLimit.maxRequests - (currentLimit?.count || 0),
microTaskCallsRemaining: remainingMicroTasks, microTaskCallsRemaining: remainingMicroTasks,
resetTime: Date.now() + RATE_LIMIT_WINDOW resetTime: Date.now() + config.rateLimit.window
} }
}), { }), {
status: 200, status: 200,
@ -192,7 +202,9 @@ export const POST: APIRoute = async ({ request }) => {
}); });
} catch (error) { } catch (error) {
console.error('[MICRO-TASK API] Pipeline error:', error); logger.error('api', 'Pipeline processing failed', error as Error, {
endpoint: '/api/ai/query'
});
if (error.message.includes('embeddings')) { if (error.message.includes('embeddings')) {
return apiServerError.unavailable('Embeddings service error - using AI fallback'); return apiServerError.unavailable('Embeddings service error - using AI fallback');

View File

@ -0,0 +1,228 @@
// src/services/ai/aiService.ts
import { config } from '../../config/appConfig.js';
import { logger } from '../logger.js';
export interface AIRequest {
prompt: string;
maxTokens?: number;
temperature?: number;
context?: string;
}
export interface AIResponse {
content: string;
requestTimeMs: number;
tokenCount?: number;
}
export class AIService {
private readonly endpoint: string;
private readonly apiKey: string;
private readonly model: string;
constructor() {
this.endpoint = config.ai.endpoint;
this.apiKey = config.ai.apiKey;
this.model = config.ai.model;
logger.info('ai-service', 'AI Service initialized', {
model: this.model,
hasApiKey: !!this.apiKey
});
}
async call(request: AIRequest): Promise<AIResponse> {
const startTime = Date.now();
const timerId = logger.time('ai-service', `call-${request.context || 'unknown'}`);
try {
const headers: Record<string, string> = {
'Content-Type': 'application/json'
};
if (this.apiKey) {
headers['Authorization'] = `Bearer ${this.apiKey}`;
}
const requestBody = {
model: this.model,
messages: [{ role: 'user', content: request.prompt }],
max_tokens: request.maxTokens || 1500,
temperature: request.temperature || 0.3
};
logger.debug('ai-service', 'Making AI request', {
context: request.context,
promptLength: request.prompt.length,
maxTokens: requestBody.max_tokens,
temperature: requestBody.temperature
});
const response = await fetch(`${this.endpoint}/v1/chat/completions`, {
method: 'POST',
headers,
body: JSON.stringify(requestBody)
});
const requestTimeMs = Date.now() - startTime;
logger.timeEnd(timerId);
if (!response.ok) {
const errorText = await response.text();
logger.error('ai-service', 'AI API request failed', new Error(`HTTP ${response.status}`), {
status: response.status,
context: request.context,
errorText: errorText.slice(0, 200)
});
throw new Error(`AI API error: ${response.status} - ${errorText}`);
}
const data = await response.json();
const content = data.choices?.[0]?.message?.content;
if (!content) {
logger.error('ai-service', 'No response content from AI model', undefined, {
context: request.context,
choices: data.choices?.length || 0
});
throw new Error('No response from AI model');
}
logger.info('ai-service', 'AI request completed', {
context: request.context,
responseLength: content.length,
duration: `${requestTimeMs}ms`,
success: true
});
return {
content: content.trim(),
requestTimeMs,
tokenCount: data.usage?.total_tokens
};
} catch (error) {
const requestTimeMs = Date.now() - startTime;
logger.error('ai-service', 'AI service call failed', error as Error, {
context: request.context,
duration: `${requestTimeMs}ms`
});
throw error;
}
}
async callMicroTask(prompt: string, context: string, maxTokens: number = 500): Promise<AIResponse> {
return this.call({
prompt,
maxTokens,
temperature: 0.3,
context: `micro-task:${context}`
});
}
estimateTokens(text: string): number {
// Simple estimation: ~4 characters per token
return Math.ceil(text.length / 4);
}
validatePromptSize(prompt: string, maxTokens: number = config.ai.maxPromptTokens): boolean {
const estimatedTokens = this.estimateTokens(prompt);
if (estimatedTokens > maxTokens) {
logger.warn('ai-service', 'Prompt exceeds token limit', {
estimated: estimatedTokens,
limit: maxTokens,
promptLength: prompt.length
});
return false;
}
return true;
}
// Utility for parsing JSON responses with error handling
parseJSONResponse<T>(response: string, fallback: T): T {
try {
let cleaned = response.trim();
// Remove markdown code blocks
const jsonBlockPatterns = [
/```json\s*([\s\S]*?)\s*```/i,
/```\s*([\s\S]*?)\s*```/i,
/\{[\s\S]*\}/,
];
for (const pattern of jsonBlockPatterns) {
const match = cleaned.match(pattern);
if (match) {
cleaned = match[1] || match[0];
break;
}
}
// Handle truncated JSON
if (!cleaned.endsWith('}') && !cleaned.endsWith(']')) {
logger.warn('ai-service', 'JSON appears truncated, attempting recovery');
let braceCount = 0;
let bracketCount = 0;
let inString = false;
let escaped = false;
let lastCompleteStructure = '';
for (let i = 0; i < cleaned.length; i++) {
const char = cleaned[i];
if (escaped) {
escaped = false;
continue;
}
if (char === '\\') {
escaped = true;
continue;
}
if (char === '"' && !escaped) {
inString = !inString;
continue;
}
if (!inString) {
if (char === '{') braceCount++;
if (char === '}') braceCount--;
if (char === '[') bracketCount++;
if (char === ']') bracketCount--;
if (braceCount === 0 && bracketCount === 0 && (char === '}' || char === ']')) {
lastCompleteStructure = cleaned.substring(0, i + 1);
}
}
}
if (lastCompleteStructure) {
cleaned = lastCompleteStructure;
} else {
if (braceCount > 0) cleaned += '}';
if (bracketCount > 0) cleaned += ']';
}
}
const parsed = JSON.parse(cleaned);
logger.debug('ai-service', 'JSON parsing successful', {
originalLength: response.length,
cleanedLength: cleaned.length
});
return parsed;
} catch (error) {
logger.warn('ai-service', 'JSON parsing failed, using fallback', {
error: (error as Error).message,
responsePreview: response.slice(0, 100)
});
return fallback;
}
}
}
export const aiService = new AIService();

View File

@ -0,0 +1,228 @@
// src/services/ai/confidenceService.ts
import { config } from '../../config/appConfig.js';
import { isToolHosted } from '../../utils/toolHelpers.js';
import { logger } from '../logger.js';
export interface ConfidenceMetrics {
overall: number;
semanticRelevance: number;
taskSuitability: number;
uncertaintyFactors: string[];
strengthIndicators: string[];
}
export interface AnalysisContext {
userQuery: string;
mode: string;
embeddingsSimilarities: Map<string, number>;
selectedTools?: Array<{
tool: any;
phase: string;
priority: string;
}>;
}
export class ConfidenceService {
private readonly semanticWeight: number;
private readonly suitabilityWeight: number;
private readonly minimumThreshold: number;
private readonly mediumThreshold: number;
private readonly highThreshold: number;
constructor() {
this.semanticWeight = config.confidence.semanticWeight;
this.suitabilityWeight = config.confidence.suitabilityWeight;
this.minimumThreshold = config.confidence.minimumThreshold;
this.mediumThreshold = config.confidence.mediumThreshold;
this.highThreshold = config.confidence.highThreshold;
logger.info('confidence', 'Confidence service initialized', {
semanticWeight: this.semanticWeight,
suitabilityWeight: this.suitabilityWeight
});
}
calculateRecommendationConfidence(
tool: any,
context: AnalysisContext,
taskRelevance: number = 70,
limitations: string[] = []
): ConfidenceMetrics {
// Calculate semantic relevance from embeddings
const rawSemanticRelevance = context.embeddingsSimilarities.has(tool.name) ?
context.embeddingsSimilarities.get(tool.name)! * 100 : 50;
// Enhanced task suitability based on context
let enhancedTaskSuitability = taskRelevance;
if (context.mode === 'workflow') {
const toolSelection = context.selectedTools?.find((st: any) =>
st.tool && st.tool.name === tool.name
);
if (toolSelection && tool.phases && Array.isArray(tool.phases) &&
tool.phases.includes(toolSelection.phase)) {
const phaseBonus = Math.min(15, 100 - taskRelevance);
enhancedTaskSuitability = Math.min(100, taskRelevance + phaseBonus);
}
}
// Calculate overall confidence
const overall = (
rawSemanticRelevance * this.semanticWeight +
enhancedTaskSuitability * this.suitabilityWeight
);
const uncertaintyFactors = this.identifyUncertaintyFactors(
tool, context, limitations, overall
);
const strengthIndicators = this.identifyStrengthIndicators(
tool, context, overall
);
const result: ConfidenceMetrics = {
overall: Math.round(overall),
semanticRelevance: Math.round(rawSemanticRelevance),
taskSuitability: Math.round(enhancedTaskSuitability),
uncertaintyFactors,
strengthIndicators
};
logger.confidence(
tool.name,
result.overall,
result.semanticRelevance,
result.taskSuitability
);
return result;
}
private identifyUncertaintyFactors(
tool: any,
context: AnalysisContext,
limitations: string[],
confidence: number
): string[] {
const factors: string[] = [];
// Include provided limitations
if (limitations?.length > 0) {
factors.push(...limitations.slice(0, 2));
}
// Semantic similarity concerns
const similarity = context.embeddingsSimilarities.get(tool.name) || 0.5;
if (similarity < 0.7) {
factors.push('Geringe semantische Ähnlichkeit zur Anfrage');
}
// Skill level vs query urgency mismatch
if (tool.skillLevel === 'expert' &&
/schnell|rapid|triage|urgent|sofort/i.test(context.userQuery)) {
factors.push('Experten-Tool für zeitkritisches Szenario');
}
if (tool.skillLevel === 'novice' &&
/komplex|erweitert|tiefgehend|advanced|forensisch/i.test(context.userQuery)) {
factors.push('Einsteiger-Tool für komplexe Analyse');
}
// Installation/setup requirements
if (tool.type === 'software' && !isToolHosted(tool) &&
tool.accessType === 'download') {
factors.push('Installation und Setup erforderlich');
}
// License considerations
if (tool.license === 'Proprietary') {
factors.push('Kommerzielle Software - Lizenzkosten zu beachten');
}
// Overall confidence concerns
if (confidence < 60) {
factors.push('Moderate Gesamtbewertung - alternative Ansätze empfohlen');
}
return factors.slice(0, 4);
}
private identifyStrengthIndicators(
tool: any,
context: AnalysisContext,
confidence: number
): string[] {
const indicators: string[] = [];
// High semantic similarity
const similarity = context.embeddingsSimilarities.get(tool.name) || 0.5;
if (similarity >= 0.7) {
indicators.push('Sehr gute semantische Übereinstimmung mit Ihrer Anfrage');
}
// Documentation availability
if (tool.knowledgebase === true) {
indicators.push('Umfassende Dokumentation und Wissensbasis verfügbar');
}
// Immediate availability
if (isToolHosted(tool)) {
indicators.push('Sofort verfügbar über gehostete Lösung');
}
// Balanced complexity
if (tool.skillLevel === 'intermediate' || tool.skillLevel === 'advanced') {
indicators.push('Ausgewogenes Verhältnis zwischen Funktionalität und Benutzerfreundlichkeit');
}
// Method alignment with query type
if (tool.type === 'method' &&
/methodik|vorgehen|prozess|ansatz/i.test(context.userQuery)) {
indicators.push('Methodischer Ansatz passt zu Ihrer prozeduralen Anfrage');
}
return indicators.slice(0, 4);
}
getConfidenceLevel(confidence: number): 'low' | 'medium' | 'high' {
if (confidence >= this.highThreshold) return 'high';
if (confidence >= this.mediumThreshold) return 'medium';
return 'low';
}
getConfidenceColor(confidence: number): string {
if (confidence >= this.highThreshold) return 'var(--color-accent)';
if (confidence >= this.mediumThreshold) return 'var(--color-warning)';
return 'var(--color-error)';
}
calculateSelectionConfidence(
result: any,
candidateCount: number
): number {
if (!result?.selectedTools) return 30;
const selectionRatio = result.selectedTools.length / candidateCount;
const hasReasoning = result.reasoning && result.reasoning.length > 50;
let confidence = 60;
// Selection ratio scoring
if (selectionRatio > 0.05 && selectionRatio < 0.3) {
confidence += 20;
} else if (selectionRatio <= 0.05) {
confidence -= 10;
} else {
confidence -= 15;
}
// Quality indicators
if (hasReasoning) confidence += 15;
if (result.selectedConcepts?.length > 0) confidence += 5;
return Math.min(95, Math.max(25, confidence));
}
}
export const confidenceService = new ConfidenceService();

View File

@ -0,0 +1,510 @@
// src/services/ai/pipelineOrchestrator.ts
import { getCompressedToolsDataForAI } from '../../utils/dataService.js';
import { getPrompt } from '../../config/prompts.js';
import { config } from '../../config/appConfig.js';
import { aiService } from './aiService.js';
import { toolSelector } from './toolSelector.js';
import { confidenceService } from './confidenceService.js';
import { auditService } from '../../utils/auditService.js';
import { logger } from '../logger.js';
export interface AnalysisResult {
recommendation: any;
processingStats: {
embeddingsUsed: boolean;
candidatesFromEmbeddings: number;
finalSelectedItems: number;
processingTimeMs: number;
microTasksCompleted: number;
microTasksFailed: number;
contextContinuityUsed: boolean;
};
}
export interface AnalysisContext {
userQuery: string;
mode: string;
filteredData: any;
selectedTools?: Array<{
tool: any;
phase: string;
priority: string;
justification?: string;
taskRelevance?: number;
limitations?: string[];
}>;
backgroundKnowledge?: Array<{
concept: any;
relevance: string;
}>;
seenToolNames: Set<string>;
embeddingsSimilarities: Map<string, number>;
scenarioAnalysis?: string;
problemAnalysis?: string;
investigationApproach?: string;
criticalConsiderations?: string;
}
export class PipelineOrchestrator {
private readonly microTaskDelay: number;
constructor() {
this.microTaskDelay = config.ai.microTaskDelay;
logger.info('pipeline', 'Pipeline orchestrator initialized');
}
async processQuery(userQuery: string, mode: string): Promise<AnalysisResult> {
const startTime = Date.now();
let completeTasks = 0;
let failedTasks = 0;
logger.pipeline('initialization', 'processing started', { userQuery: userQuery.slice(0, 50), mode });
try {
const toolsData = await getCompressedToolsDataForAI();
const context: AnalysisContext = {
userQuery,
mode,
filteredData: {},
seenToolNames: new Set<string>(),
embeddingsSimilarities: new Map<string, number>()
};
// Step 1: Get candidates and select tools
const candidateData = await toolSelector.getCandidates(userQuery, toolsData);
const toolSelection = await toolSelector.selectTools(
userQuery,
candidateData.tools,
candidateData.concepts,
mode,
candidateData.selectionMethod
);
context.filteredData = {
tools: toolSelection.selectedTools,
concepts: toolSelection.selectedConcepts,
domains: toolsData.domains,
phases: toolsData.phases,
'domain-agnostic-software': toolsData['domain-agnostic-software']
};
// Step 2: Scenario/Problem Analysis
const analysisResult = await this.analyzeScenario(context);
if (analysisResult.success) completeTasks++; else failedTasks++;
await this.delay(this.microTaskDelay);
// Step 3: Investigation Approach
const approachResult = await this.generateApproach(context);
if (approachResult.success) completeTasks++; else failedTasks++;
await this.delay(this.microTaskDelay);
// Step 4: Critical Considerations
const considerationsResult = await this.generateCriticalConsiderations(context);
if (considerationsResult.success) completeTasks++; else failedTasks++;
await this.delay(this.microTaskDelay);
// Step 5: Mode-specific processing
if (mode === 'workflow') {
const workflowTasks = await this.processWorkflowMode(context, toolsData);
completeTasks += workflowTasks.completed;
failedTasks += workflowTasks.failed;
} else {
const toolTasks = await this.processToolMode(context);
completeTasks += toolTasks.completed;
failedTasks += toolTasks.failed;
}
// Step 6: Background Knowledge
const knowledgeResult = await this.selectBackgroundKnowledge(context);
if (knowledgeResult.success) completeTasks++; else failedTasks++;
await this.delay(this.microTaskDelay);
// Step 7: Final Recommendations
const finalResult = await this.generateFinalRecommendations(context);
if (finalResult.success) completeTasks++; else failedTasks++;
const recommendation = this.buildRecommendation(context, mode, finalResult.content);
const processingStats = {
embeddingsUsed: candidateData.selectionMethod === 'embeddings_candidates',
candidatesFromEmbeddings: candidateData.tools.length,
finalSelectedItems: (context.selectedTools?.length || 0) + (context.backgroundKnowledge?.length || 0),
processingTimeMs: Date.now() - startTime,
microTasksCompleted: completeTasks,
microTasksFailed: failedTasks,
contextContinuityUsed: true
};
logger.pipeline('completion', 'processing completed', {
duration: `${processingStats.processingTimeMs}ms`,
tasksCompleted: completeTasks,
tasksFailed: failedTasks,
finalItems: processingStats.finalSelectedItems
});
return {
recommendation: {
...recommendation,
auditTrail: auditService.isEnabled() ? [] : undefined // Audit trail handled by auditService
},
processingStats
};
} catch (error) {
const duration = Date.now() - startTime;
logger.error('pipeline', 'Processing failed', error as Error, {
duration: `${duration}ms`,
completedTasks: completeTasks,
failedTasks: failedTasks
});
throw error;
}
}
private async analyzeScenario(context: AnalysisContext): Promise<{ success: boolean; content: string }> {
try {
const isWorkflow = context.mode === 'workflow';
const prompt = getPrompt('scenarioAnalysis', isWorkflow, context.userQuery);
const response = await aiService.callMicroTask(prompt, 'scenario-analysis', 400);
if (isWorkflow) {
context.scenarioAnalysis = response.content;
} else {
context.problemAnalysis = response.content;
}
logger.pipeline('micro-task', `${isWorkflow ? 'scenario' : 'problem'} analysis completed`);
return { success: true, content: response.content };
} catch (error) {
logger.error('pipeline', 'Scenario analysis failed', error as Error);
return { success: false, content: '' };
}
}
private async generateApproach(context: AnalysisContext): Promise<{ success: boolean; content: string }> {
try {
const isWorkflow = context.mode === 'workflow';
const prompt = getPrompt('investigationApproach', isWorkflow, context.userQuery);
const response = await aiService.callMicroTask(prompt, 'investigation-approach', 400);
context.investigationApproach = response.content;
logger.pipeline('micro-task', 'investigation approach completed');
return { success: true, content: response.content };
} catch (error) {
logger.error('pipeline', 'Investigation approach failed', error as Error);
return { success: false, content: '' };
}
}
private async generateCriticalConsiderations(context: AnalysisContext): Promise<{ success: boolean; content: string }> {
try {
const isWorkflow = context.mode === 'workflow';
const prompt = getPrompt('criticalConsiderations', isWorkflow, context.userQuery);
const response = await aiService.callMicroTask(prompt, 'critical-considerations', 350);
context.criticalConsiderations = response.content;
logger.pipeline('micro-task', 'critical considerations completed');
return { success: true, content: response.content };
} catch (error) {
logger.error('pipeline', 'Critical considerations failed', error as Error);
return { success: false, content: '' };
}
}
private async processWorkflowMode(context: AnalysisContext, toolsData: any): Promise<{ completed: number; failed: number }> {
let completed = 0;
let failed = 0;
const phases = toolsData.phases || [];
for (const phase of phases) {
try {
const result = await this.selectToolsForPhase(context, phase);
if (result.success) completed++; else failed++;
await this.delay(this.microTaskDelay);
} catch (error) {
logger.error('pipeline', `Phase ${phase.id} tool selection failed`, error as Error);
failed++;
}
}
logger.pipeline('workflow', 'phase processing completed', {
phases: phases.length,
completed,
failed
});
return { completed, failed };
}
private async processToolMode(context: AnalysisContext): Promise<{ completed: number; failed: number }> {
let completed = 0;
let failed = 0;
const topTools = context.filteredData.tools.slice(0, 3);
for (let i = 0; i < topTools.length; i++) {
try {
const result = await this.evaluateSpecificTool(context, topTools[i], i + 1);
if (result.success) completed++; else failed++;
await this.delay(this.microTaskDelay);
} catch (error) {
logger.error('pipeline', `Tool evaluation failed for ${topTools[i]?.name}`, error as Error);
failed++;
}
}
logger.pipeline('tool-mode', 'tool evaluation completed', {
toolsEvaluated: topTools.length,
completed,
failed
});
return { completed, failed };
}
private async selectToolsForPhase(context: AnalysisContext, phase: any): Promise<{ success: boolean }> {
const phaseTools = context.filteredData.tools.filter((tool: any) =>
tool && tool.phases && Array.isArray(tool.phases) && tool.phases.includes(phase.id)
);
if (phaseTools.length === 0) {
logger.debug('pipeline', `No tools available for phase ${phase.id}`);
return { success: true };
}
try {
const prompt = getPrompt('phaseToolSelection', context.userQuery, phase, phaseTools);
const response = await aiService.callMicroTask(prompt, `phase-${phase.id}`, 1000);
const selections = aiService.parseJSONResponse(response.content, []);
if (Array.isArray(selections)) {
const validSelections = selections.filter((sel: any) => {
return phaseTools.some((tool: any) => tool && tool.name === sel.toolName);
});
validSelections.forEach((sel: any) => {
const tool = phaseTools.find((t: any) => t && t.name === sel.toolName);
if (tool) {
const taskRelevance = typeof sel.taskRelevance === 'number' ?
sel.taskRelevance : parseInt(String(sel.taskRelevance)) || 70;
this.addToolToSelection(context, tool, phase.id, this.derivePriorityFromScore(taskRelevance),
sel.justification, taskRelevance, sel.limitations);
}
});
logger.pipeline('phase-selection', `Phase ${phase.id} completed`, {
available: phaseTools.length,
selected: validSelections.length
});
}
return { success: true };
} catch (error) {
logger.error('pipeline', `Phase ${phase.id} tool selection failed`, error as Error);
return { success: false };
}
}
private async evaluateSpecificTool(context: AnalysisContext, tool: any, rank: number): Promise<{ success: boolean }> {
try {
const existingSelection = context.selectedTools?.find((st: any) => st.tool && st.tool.name === tool.name);
const taskRelevance = existingSelection?.taskRelevance || 70;
const priority = this.derivePriorityFromScore(taskRelevance);
const prompt = getPrompt('toolEvaluation', context.userQuery, tool, rank, taskRelevance);
const response = await aiService.callMicroTask(prompt, `tool-eval-${tool.name}`, 1000);
const evaluation = aiService.parseJSONResponse(response.content, {
detailed_explanation: 'Evaluation failed',
implementation_approach: '',
pros: [],
limitations: [],
alternatives: ''
});
this.addToolToSelection(context, {
...tool,
evaluation: {
...evaluation,
rank,
task_relevance: taskRelevance
}
}, 'evaluation', priority, evaluation.detailed_explanation, taskRelevance, evaluation.limitations);
logger.pipeline('tool-evaluation', `Tool ${tool.name} evaluated`, { rank, taskRelevance });
return { success: true };
} catch (error) {
logger.error('pipeline', `Tool evaluation failed for ${tool.name}`, error as Error);
return { success: false };
}
}
private async selectBackgroundKnowledge(context: AnalysisContext): Promise<{ success: boolean }> {
const availableConcepts = context.filteredData.concepts;
if (availableConcepts.length === 0) {
return { success: true };
}
try {
const selectedToolNames = context.selectedTools?.map((st: any) => st.tool && st.tool.name).filter(Boolean) || [];
const prompt = getPrompt('backgroundKnowledgeSelection', context.userQuery, context.mode, selectedToolNames, availableConcepts);
const response = await aiService.callMicroTask(prompt, 'background-knowledge', 700);
const selections = aiService.parseJSONResponse(response.content, []);
if (Array.isArray(selections)) {
context.backgroundKnowledge = selections.filter((sel: any) =>
sel.conceptName && availableConcepts.some((concept: any) => concept.name === sel.conceptName)
).map((sel: any) => ({
concept: availableConcepts.find((c: any) => c.name === sel.conceptName),
relevance: sel.relevance
}));
logger.pipeline('background-knowledge', 'selection completed', {
available: availableConcepts.length,
selected: context.backgroundKnowledge?.length || 0
});
}
return { success: true };
} catch (error) {
logger.error('pipeline', 'Background knowledge selection failed', error as Error);
return { success: false };
}
}
private async generateFinalRecommendations(context: AnalysisContext): Promise<{ success: boolean; content: string }> {
try {
const selectedToolNames = context.selectedTools?.map((st: any) => st.tool && st.tool.name).filter(Boolean) || [];
const prompt = getPrompt('finalRecommendations', context.mode === 'workflow', context.userQuery, selectedToolNames);
const response = await aiService.callMicroTask(prompt, 'final-recommendations', 350);
logger.pipeline('final', 'recommendations generated');
return { success: true, content: response.content };
} catch (error) {
logger.error('pipeline', 'Final recommendations failed', error as Error);
return { success: false, content: '' };
}
}
private addToolToSelection(
context: AnalysisContext,
tool: any,
phase: string,
priority: string,
justification?: string,
taskRelevance?: number,
limitations?: string[]
): void {
context.seenToolNames.add(tool.name);
if (!context.selectedTools) context.selectedTools = [];
context.selectedTools.push({
tool,
phase,
priority,
justification,
taskRelevance,
limitations
});
}
private derivePriorityFromScore(taskRelevance: number): string {
if (taskRelevance >= 80) return 'high';
if (taskRelevance >= 60) return 'medium';
return 'low';
}
private async delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
private buildRecommendation(context: AnalysisContext, mode: string, finalContent: string): any {
const isWorkflow = mode === 'workflow';
const base = {
[isWorkflow ? 'scenario_analysis' : 'problem_analysis']:
isWorkflow ? context.scenarioAnalysis : context.problemAnalysis,
investigation_approach: context.investigationApproach,
critical_considerations: context.criticalConsiderations,
background_knowledge: context.backgroundKnowledge?.map((bk: any) => ({
concept_name: bk.concept.name,
relevance: bk.relevance
})) || []
};
if (isWorkflow) {
const recommendedToolsWithConfidence = context.selectedTools?.map((st: any) => {
const confidence = confidenceService.calculateRecommendationConfidence(
st.tool,
context,
st.taskRelevance || 70,
st.limitations || []
);
return {
name: st.tool.name,
type: st.tool.type,
phase: st.phase,
priority: st.priority,
justification: st.justification || `Empfohlen für ${st.phase}`,
confidence,
recommendationStrength: confidenceService.getConfidenceLevel(confidence.overall)
};
}) || [];
return {
...base,
recommended_tools: recommendedToolsWithConfidence,
workflow_suggestion: finalContent
};
} else {
const recommendedToolsWithConfidence = context.selectedTools?.map((st: any) => {
const confidence = confidenceService.calculateRecommendationConfidence(
st.tool,
context,
st.taskRelevance || 70,
st.limitations || []
);
return {
name: st.tool.name,
type: st.tool.type,
rank: st.tool.evaluation?.rank || 1,
suitability_score: st.priority,
detailed_explanation: st.tool.evaluation?.detailed_explanation || '',
implementation_approach: st.tool.evaluation?.implementation_approach || '',
pros: st.tool.evaluation?.pros || [],
cons: st.tool.evaluation?.limitations || [],
alternatives: st.tool.evaluation?.alternatives || '',
confidence,
recommendationStrength: confidenceService.getConfidenceLevel(confidence.overall)
};
}) || [];
return {
...base,
recommended_tools: recommendedToolsWithConfidence,
additional_considerations: finalContent
};
}
}
}
export const pipelineOrchestrator = new PipelineOrchestrator();

View File

@ -0,0 +1,354 @@
// src/services/ai/toolSelector.ts
import { config } from '../../config/appConfig.js';
import { aiService } from './aiService.js';
import { embeddingsService } from '../../utils/embeddings.js';
import { getPrompt } from '../../config/prompts.js';
import { logger } from '../logger.js';
export interface ToolSelection {
selectedTools: any[];
selectedConcepts: any[];
selectionMethod: string;
confidence: number;
}
export interface CandidateData {
tools: any[];
concepts: any[];
selectionMethod: string;
}
export class ToolSelector {
private readonly embeddingCandidates: number;
private readonly similarityThreshold: number;
private readonly embeddingSelectionLimit: number;
private readonly embeddingConceptsLimit: number;
private readonly noEmbeddingsToolLimit: number;
private readonly noEmbeddingsConceptLimit: number;
private readonly embeddingsMinTools: number;
private readonly embeddingsMaxReductionRatio: number;
private readonly methodSelectionRatio: number;
private readonly softwareSelectionRatio: number;
private readonly maxSelectedItems: number;
constructor() {
this.embeddingCandidates = config.ai.embeddingCandidates;
this.similarityThreshold = config.ai.similarityThreshold;
this.embeddingSelectionLimit = config.ai.embeddingSelectionLimit;
this.embeddingConceptsLimit = config.ai.embeddingConceptsLimit;
this.noEmbeddingsToolLimit = config.ai.noEmbeddingsToolLimit;
this.noEmbeddingsConceptLimit = config.ai.noEmbeddingsConceptLimit;
this.embeddingsMinTools = config.ai.embeddingsMinTools;
this.embeddingsMaxReductionRatio = config.ai.embeddingsMaxReductionRatio;
this.methodSelectionRatio = config.ai.methodSelectionRatio;
this.softwareSelectionRatio = config.ai.softwareSelectionRatio;
this.maxSelectedItems = config.ai.maxSelectedItems;
logger.info('tool-selector', 'Tool selector initialized', {
embeddingCandidates: this.embeddingCandidates,
methodRatio: `${(this.methodSelectionRatio * 100).toFixed(0)}%`,
softwareRatio: `${(this.softwareSelectionRatio * 100).toFixed(0)}%`
});
}
async getCandidates(
userQuery: string,
toolsData: any
): Promise<CandidateData> {
try {
await embeddingsService.waitForInitialization();
} catch (error) {
logger.warn('tool-selector', 'Embeddings initialization failed, using full dataset');
}
if (embeddingsService.isEnabled()) {
return this.getEmbeddingCandidates(userQuery, toolsData);
} else {
return this.getFullDatasetCandidates(toolsData);
}
}
private async getEmbeddingCandidates(
userQuery: string,
toolsData: any
): Promise<CandidateData> {
const startTime = Date.now();
const similarItems = await embeddingsService.findSimilar(
userQuery,
this.embeddingCandidates,
this.similarityThreshold
);
logger.embedding('similarity search completed', {
query: userQuery.slice(0, 50),
candidates: this.embeddingCandidates,
threshold: this.similarityThreshold,
results: similarItems.length,
duration: `${Date.now() - startTime}ms`
});
const toolsMap = new Map(toolsData.tools.map((tool: any) => [tool.name, tool]));
const conceptsMap = new Map(toolsData.concepts.map((concept: any) => [concept.name, concept]));
const similarTools = similarItems
.filter((item: any) => item.type === 'tool')
.map((item: any) => toolsMap.get(item.name))
.filter((tool: any): tool is NonNullable<any> => tool !== undefined && tool !== null);
const similarConcepts = similarItems
.filter((item: any) => item.type === 'concept')
.map((item: any) => conceptsMap.get(item.name))
.filter((concept: any): concept is NonNullable<any> => concept !== undefined && concept !== null);
const totalAvailableTools = toolsData.tools.length;
const reductionRatio = similarTools.length / totalAvailableTools;
const useEmbeddings = similarTools.length >= this.embeddingsMinTools &&
reductionRatio <= this.embeddingsMaxReductionRatio;
if (useEmbeddings) {
logger.selection('embeddings', similarTools.length, similarConcepts.length, {
reduction: `${totalAvailableTools}${similarTools.length}`,
ratio: reductionRatio.toFixed(2)
});
return {
tools: similarTools,
concepts: similarConcepts,
selectionMethod: 'embeddings_candidates'
};
} else {
logger.info('tool-selector', 'Embeddings filtering insufficient, using full dataset', {
toolsFound: similarTools.length,
minRequired: this.embeddingsMinTools,
reductionRatio: reductionRatio.toFixed(2),
maxRatio: this.embeddingsMaxReductionRatio
});
return this.getFullDatasetCandidates(toolsData);
}
}
private getFullDatasetCandidates(toolsData: any): CandidateData {
logger.selection('full-dataset', toolsData.tools.length, toolsData.concepts.length);
return {
tools: toolsData.tools,
concepts: toolsData.concepts,
selectionMethod: 'full_dataset'
};
}
async selectTools(
userQuery: string,
candidateTools: any[],
candidateConcepts: any[],
mode: string,
selectionMethod: string
): Promise<ToolSelection> {
const startTime = Date.now();
const candidateMethods = candidateTools.filter((tool: any) =>
tool && tool.type === 'method'
);
const candidateSoftware = candidateTools.filter((tool: any) =>
tool && tool.type === 'software'
);
logger.info('tool-selector', 'Starting AI tool selection', {
mode,
selectionMethod,
methods: candidateMethods.length,
software: candidateSoftware.length,
concepts: candidateConcepts.length
});
const { toolsToSend, conceptsToSend } = this.prepareSelectionData(
candidateMethods,
candidateSoftware,
candidateConcepts,
selectionMethod
);
const basePrompt = getPrompt(
'toolSelection',
mode,
userQuery,
selectionMethod,
this.maxSelectedItems
);
const prompt = getPrompt(
'toolSelectionWithData',
basePrompt,
toolsToSend,
conceptsToSend
);
if (!aiService.validatePromptSize(prompt, 35000)) {
throw new Error('Prompt too large for AI model');
}
const response = await aiService.call({
prompt,
maxTokens: 2500,
context: `tool-selection-${mode}`
});
const result = aiService.parseJSONResponse(response.content, {
selectedTools: [],
selectedConcepts: [],
reasoning: ''
});
if (!Array.isArray(result.selectedTools) || !Array.isArray(result.selectedConcepts)) {
throw new Error('AI selection returned invalid structure');
}
const totalSelected = result.selectedTools.length + result.selectedConcepts.length;
if (totalSelected === 0) {
throw new Error('AI selection returned empty selection');
}
const selectedTools = this.mapSelectedItems(result.selectedTools, candidateTools);
const selectedConcepts = this.mapSelectedItems(result.selectedConcepts, candidateConcepts);
const selectedMethods = selectedTools.filter((t: any) => t && t.type === 'method');
const selectedSoftware = selectedTools.filter((t: any) => t && t.type === 'software');
const confidence = this.calculateSelectionConfidence(result, candidateTools.length + candidateConcepts.length);
const duration = Date.now() - startTime;
logger.selection('ai-completed', selectedTools.length, selectedConcepts.length, {
methods: selectedMethods.length,
software: selectedSoftware.length,
confidence: `${confidence}%`,
duration: `${duration}ms`,
selectionMethod
});
return {
selectedTools,
selectedConcepts,
selectionMethod,
confidence
};
}
private prepareSelectionData(
candidateMethods: any[],
candidateSoftware: any[],
candidateConcepts: any[],
selectionMethod: string
): { toolsToSend: any[], conceptsToSend: any[] } {
const isEmbeddingsBased = selectionMethod === 'embeddings_candidates';
const totalLimit = isEmbeddingsBased ?
this.embeddingSelectionLimit :
this.noEmbeddingsToolLimit;
const conceptLimit = isEmbeddingsBased ?
this.embeddingConceptsLimit :
this.noEmbeddingsConceptLimit;
const methodLimit = Math.ceil(totalLimit * this.methodSelectionRatio);
const softwareLimit = Math.floor(totalLimit * this.softwareSelectionRatio);
const toolsToSend = [
...candidateMethods.slice(0, methodLimit).map(this.createToolData),
...candidateSoftware.slice(0, softwareLimit).map(this.createToolData)
];
// Fill remaining capacity
const remainingCapacity = totalLimit - toolsToSend.length;
if (remainingCapacity > 0) {
if (candidateMethods.length > methodLimit) {
toolsToSend.push(
...candidateMethods
.slice(methodLimit, methodLimit + remainingCapacity)
.map(this.createToolData)
);
} else if (candidateSoftware.length > softwareLimit) {
toolsToSend.push(
...candidateSoftware
.slice(softwareLimit, softwareLimit + remainingCapacity)
.map(this.createToolData)
);
}
}
const conceptsToSend = candidateConcepts
.slice(0, conceptLimit)
.map(this.createConceptData);
logger.debug('tool-selector', 'Selection data prepared', {
toolsToSend: toolsToSend.length,
conceptsToSend: conceptsToSend.length,
methodLimit,
softwareLimit,
conceptLimit,
isEmbeddingsBased
});
return { toolsToSend, conceptsToSend };
}
private createToolData = (tool: any) => ({
name: tool.name,
type: tool.type,
description: tool.description,
domains: tool.domains,
phases: tool.phases,
platforms: tool.platforms || [],
tags: tool.tags || [],
skillLevel: tool.skillLevel,
license: tool.license,
accessType: tool.accessType,
projectUrl: tool.projectUrl,
knowledgebase: tool.knowledgebase,
related_concepts: tool.related_concepts || [],
related_software: tool.related_software || []
});
private createConceptData = (concept: any) => ({
name: concept.name,
type: 'concept',
description: concept.description,
domains: concept.domains,
phases: concept.phases,
tags: concept.tags || [],
skillLevel: concept.skillLevel,
related_concepts: concept.related_concepts || [],
related_software: concept.related_software || []
});
private mapSelectedItems(selectedNames: string[], candidates: any[]): any[] {
const candidatesMap = new Map(candidates.map((item: any) => [item.name, item]));
return selectedNames
.map((name: string) => candidatesMap.get(name))
.filter((item: any): item is NonNullable<any> => item !== undefined && item !== null);
}
private calculateSelectionConfidence(result: any, candidateCount: number): number {
if (!result?.selectedTools) return 30;
const selectionRatio = result.selectedTools.length / candidateCount;
const hasReasoning = result.reasoning && result.reasoning.length > 50;
let confidence = 60;
if (selectionRatio > 0.05 && selectionRatio < 0.3) confidence += 20;
else if (selectionRatio <= 0.05) confidence -= 10;
else confidence -= 15;
if (hasReasoning) confidence += 15;
if (result.selectedConcepts?.length > 0) confidence += 5;
return Math.min(95, Math.max(25, confidence));
}
}
export const toolSelector = new ToolSelector();

159
src/services/logger.ts Normal file
View File

@ -0,0 +1,159 @@
// src/services/logger.ts
export enum LogLevel {
ERROR = 0,
WARN = 1,
INFO = 2,
DEBUG = 3
}
interface LogContext {
[key: string]: any;
}
class Logger {
private logLevel: LogLevel;
private enabledContexts: Set<string>;
constructor() {
this.logLevel = this.getLogLevel();
this.enabledContexts = this.getEnabledContexts();
}
private getLogLevel(): LogLevel {
const level = process.env.LOG_LEVEL?.toUpperCase() || 'INFO';
switch (level) {
case 'ERROR': return LogLevel.ERROR;
case 'WARN': return LogLevel.WARN;
case 'DEBUG': return LogLevel.DEBUG;
default: return LogLevel.INFO;
}
}
private getEnabledContexts(): Set<string> {
const contexts = process.env.LOG_CONTEXTS?.split(',') || [];
return new Set(contexts.map(c => c.trim().toLowerCase()));
}
private shouldLog(level: LogLevel, context: string): boolean {
if (level > this.logLevel) return false;
if (this.enabledContexts.size > 0 && !this.enabledContexts.has(context.toLowerCase())) {
return false;
}
return true;
}
private formatMessage(level: string, context: string, message: string, data?: LogContext): string {
const timestamp = new Date().toISOString();
const contextStr = context.toUpperCase().padEnd(12);
let logMessage = `[${timestamp}] ${level.padEnd(5)} [${contextStr}] ${message}`;
if (data && Object.keys(data).length > 0) {
const dataStr = Object.entries(data)
.map(([key, value]) => `${key}=${typeof value === 'object' ? JSON.stringify(value) : value}`)
.join(' ');
logMessage += ` | ${dataStr}`;
}
return logMessage;
}
error(context: string, message: string, error?: Error, data?: LogContext): void {
if (!this.shouldLog(LogLevel.ERROR, context)) return;
const logData = { ...data };
if (error) {
logData.error = error.message;
logData.stack = error.stack;
}
console.error(this.formatMessage('ERROR', context, message, logData));
}
warn(context: string, message: string, data?: LogContext): void {
if (!this.shouldLog(LogLevel.WARN, context)) return;
console.warn(this.formatMessage('WARN', context, message, data));
}
info(context: string, message: string, data?: LogContext): void {
if (!this.shouldLog(LogLevel.INFO, context)) return;
console.log(this.formatMessage('INFO', context, message, data));
}
debug(context: string, message: string, data?: LogContext): void {
if (!this.shouldLog(LogLevel.DEBUG, context)) return;
console.log(this.formatMessage('DEBUG', context, message, data));
}
// Specialized logging methods for common contexts
pipeline(phase: string, action: string, details?: LogContext): void {
this.info('pipeline', `${phase}/${action}`, details);
}
audit(phase: string, action: string, confidence: number, timeMs: number, metadata?: LogContext): void {
this.info('audit', `${phase}/${action} completed`, {
confidence: `${confidence}%`,
duration: `${timeMs}ms`,
...metadata
});
}
api(method: string, path: string, status: number, details?: LogContext): void {
const level = status >= 400 ? LogLevel.ERROR : status >= 300 ? LogLevel.WARN : LogLevel.INFO;
if (level === LogLevel.ERROR) {
this.error('api', `${method} ${path}`, undefined, { status, ...details });
} else if (level === LogLevel.WARN) {
this.warn('api', `${method} ${path}`, { status, ...details });
} else {
this.info('api', `${method} ${path}`, { status, ...details });
}
}
embedding(action: string, details?: LogContext): void {
this.info('embedding', action, details);
}
selection(method: string, toolCount: number, conceptCount: number, details?: LogContext): void {
this.info('selection', `${method} selection completed`, {
tools: toolCount,
concepts: conceptCount,
...details
});
}
confidence(toolName: string, overall: number, semantic: number, suitability: number): void {
this.debug('confidence', `${toolName} scored`, {
overall: `${overall}%`,
semantic: `${semantic}%`,
suitability: `${suitability}%`
});
}
queue(action: string, details?: LogContext): void {
this.info('queue', action, details);
}
// Performance timing
time(context: string, label: string): string {
const timerId = `${context}:${label}:${Date.now()}`;
console.time(timerId);
return timerId;
}
timeEnd(timerId: string, message?: string): void {
console.timeEnd(timerId);
if (message) {
const [context] = timerId.split(':');
this.debug(context, message);
}
}
}
export const logger = new Logger();
// Export convenience functions for common patterns
export const logPipeline = logger.pipeline.bind(logger);
export const logAudit = logger.audit.bind(logger);
export const logAPI = logger.api.bind(logger);
export const logError = logger.error.bind(logger);
export const logInfo = logger.info.bind(logger);

File diff suppressed because it is too large Load Diff

184
src/utils/aiUtils.ts Normal file
View File

@ -0,0 +1,184 @@
// src/utils/aiUtils.ts
import { isToolHosted, createToolSlug } from './toolHelpers.js';
export interface ModeConfig {
placeholder: string;
description: string;
submitText: string;
loadingText: string;
}
export function getModeConfig(): Record<string, ModeConfig> {
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..."
}
};
}
export function generateShareURL(toolName: string, view: string, modal: string | null = null): string {
const toolSlug = createToolSlug(toolName);
const baseUrl = window.location.origin + window.location.pathname;
const params = new URLSearchParams();
params.set('tool', toolSlug);
params.set('view', view);
if (modal) {
params.set('modal', modal);
}
return `${baseUrl}?${params.toString()}`;
}
export function getErrorMessage(error: Error): string {
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}`;
}
export function renderToolBadges(tool: any): string {
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;
}
export function getToolClass(tool: any, context: string = 'card'): string {
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 '';
}
}
export function getSuitabilityText(score: string): string {
const texts = {
high: 'GUT GEEIGNET',
medium: 'GEEIGNET',
low: 'VIELLEICHT GEEIGNET'
};
return texts[score] || 'GEEIGNET';
}
export function truncateText(text: string, maxLength: number): string {
if (!text || text.length <= maxLength) return text;
return text.slice(0, maxLength) + '...';
}
export function sanitizeText(text: string): string {
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();
}
export function escapeHtml(text: string): string {
if (typeof text !== 'string') return String(text);
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
export function renderConfidenceTooltip(confidence: any): string {
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>
<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>
`;
}
function getConfidenceColor(confidence: number): string {
if (confidence >= 80) return 'var(--color-accent)';
if (confidence >= 60) return 'var(--color-warning)';
return 'var(--color-error)';
}

View File

@ -1,17 +1,8 @@
// src/utils/auditService.ts // src/utils/auditService.ts
import 'dotenv/config'; import { config } from '../config/appConfig.js';
import { logger } from '../services/logger.js';
function env(key: string, fallback: string | undefined = undefined): string | undefined { export interface AuditEntry {
if (typeof process !== 'undefined' && process.env?.[key] !== undefined) {
return process.env[key];
}
if (typeof import.meta !== 'undefined' && (import.meta as any).env?.[key] !== undefined) {
return (import.meta as any).env[key];
}
return fallback;
}
interface AuditEntry {
timestamp: number; timestamp: number;
phase: string; phase: string;
action: string; action: string;
@ -22,14 +13,7 @@ interface AuditEntry {
metadata: Record<string, any>; metadata: Record<string, any>;
} }
interface AuditConfig { export interface CompressedAuditEntry {
enabled: boolean;
detailLevel: 'minimal' | 'standard' | 'verbose';
retentionHours: number;
maxEntries: number;
}
interface CompressedAuditEntry {
timestamp: number; timestamp: number;
phase: string; phase: string;
action: string; action: string;
@ -40,7 +24,7 @@ interface CompressedAuditEntry {
metadata: Record<string, any>; metadata: Record<string, any>;
} }
interface ProcessedAuditTrail { export interface ProcessedAuditTrail {
totalTime: number; totalTime: number;
avgConfidence: number; avgConfidence: number;
stepCount: number; stepCount: number;
@ -62,7 +46,8 @@ interface ProcessedAuditTrail {
} }
class AuditService { class AuditService {
private config: AuditConfig; private readonly enabled: boolean;
private readonly detailLevel: 'minimal' | 'standard' | 'verbose';
private tempEntries: AuditEntry[] = []; private tempEntries: AuditEntry[] = [];
private readonly phaseConfig = { private readonly phaseConfig = {
@ -87,50 +72,13 @@ class AuditService {
}; };
constructor() { constructor() {
this.config = this.loadConfig(); this.enabled = config.audit.enabled;
} this.detailLevel = config.audit.detailLevel;
private loadConfig(): AuditConfig { logger.info('audit-service', 'Audit service initialized', {
const enabledFlag = env('FORENSIC_AUDIT_ENABLED', 'false'); enabled: this.enabled,
const detailLevel = env('FORENSIC_AUDIT_DETAIL_LEVEL', 'standard') as 'minimal' | 'standard' | 'verbose'; detailLevel: this.detailLevel
const retentionHours = parseInt(env('FORENSIC_AUDIT_RETENTION_HOURS', '72') || '72', 10);
const maxEntries = parseInt(env('FORENSIC_AUDIT_MAX_ENTRIES', '50') || '50', 10);
console.log('[AUDIT SERVICE] Configuration loaded:', {
enabled: enabledFlag === 'true',
detailLevel,
retentionHours,
maxEntries,
context: typeof process !== 'undefined' ? 'server' : 'client'
}); });
return {
enabled: enabledFlag === 'true',
detailLevel,
retentionHours,
maxEntries
};
}
getDebugInfo(): {
config: AuditConfig;
environment: Record<string, any>;
context: string;
} {
const context = typeof process !== 'undefined' ? 'server' : 'client';
return {
config: this.config,
environment: {
FORENSIC_AUDIT_ENABLED: env('FORENSIC_AUDIT_ENABLED'),
FORENSIC_AUDIT_DETAIL_LEVEL: env('FORENSIC_AUDIT_DETAIL_LEVEL'),
FORENSIC_AUDIT_RETENTION_HOURS: env('FORENSIC_AUDIT_RETENTION_HOURS'),
FORENSIC_AUDIT_MAX_ENTRIES: env('FORENSIC_AUDIT_MAX_ENTRIES'),
processEnvKeys: typeof process !== 'undefined' ? Object.keys(process.env).filter(k => k.includes('AUDIT')) : [],
importMetaEnvAvailable: typeof import.meta !== 'undefined' && !!(import.meta as any).env
},
context
};
} }
addEntry( addEntry(
@ -142,7 +90,7 @@ class AuditService {
startTime: number, startTime: number,
metadata: Record<string, any> = {} metadata: Record<string, any> = {}
): void { ): void {
if (!this.config.enabled) return; if (!this.enabled) return;
const entry: AuditEntry = { const entry: AuditEntry = {
timestamp: Date.now(), timestamp: Date.now(),
@ -156,76 +104,73 @@ class AuditService {
}; };
this.tempEntries.push(entry); this.tempEntries.push(entry);
console.log(`[AUDIT] ${phase}/${action}: ${confidence}% confidence, ${entry.processingTimeMs}ms`); logger.audit(phase, action, confidence, entry.processingTimeMs, metadata);
} }
mergeAndClear(auditTrail: AuditEntry[]): void { mergeAndClear(auditTrail: AuditEntry[]): void {
if (!this.config.enabled || this.tempEntries.length === 0) return; if (!this.enabled || this.tempEntries.length === 0) return;
auditTrail.unshift(...this.tempEntries); auditTrail.unshift(...this.tempEntries);
const entryCount = this.tempEntries.length; const entryCount = this.tempEntries.length;
this.tempEntries = []; this.tempEntries = [];
console.log(`[AUDIT] Merged ${entryCount} entries into audit trail`); logger.debug('audit-service', `Merged audit entries`, { entryCount });
} }
processAuditTrail(rawAuditTrail: AuditEntry[]): ProcessedAuditTrail | null { processAuditTrail(rawAuditTrail: AuditEntry[]): ProcessedAuditTrail | null {
if (!this.config.enabled) { if (!this.enabled) {
console.log('[AUDIT] Service disabled, returning null'); return null;
return null;
} }
if (!rawAuditTrail || !Array.isArray(rawAuditTrail) || rawAuditTrail.length === 0) { if (!rawAuditTrail || !Array.isArray(rawAuditTrail) || rawAuditTrail.length === 0) {
console.log('[AUDIT] No audit trail data provided'); logger.debug('audit-service', 'No audit trail data to process');
return null; return null;
} }
try { try {
console.log('[AUDIT] Processing', rawAuditTrail.length, 'audit entries'); logger.debug('audit-service', 'Processing audit trail', { entries: rawAuditTrail.length });
const totalTime = rawAuditTrail.reduce((sum, entry) => sum + (entry.processingTimeMs || 0), 0); const totalTime = rawAuditTrail.reduce((sum, entry) => sum + (entry.processingTimeMs || 0), 0);
const validConfidenceEntries = rawAuditTrail.filter(entry => typeof entry.confidence === 'number'); const validConfidenceEntries = rawAuditTrail.filter(entry => typeof entry.confidence === 'number');
const avgConfidence = validConfidenceEntries.length > 0 const avgConfidence = validConfidenceEntries.length > 0
? Math.round(validConfidenceEntries.reduce((sum, entry) => sum + entry.confidence, 0) / validConfidenceEntries.length) ? Math.round(validConfidenceEntries.reduce((sum, entry) => sum + entry.confidence, 0) / validConfidenceEntries.length)
: 0; : 0;
const highConfidenceSteps = rawAuditTrail.filter(entry => (entry.confidence || 0) >= 80).length; const highConfidenceSteps = rawAuditTrail.filter(entry => (entry.confidence || 0) >= 80).length;
const lowConfidenceSteps = rawAuditTrail.filter(entry => (entry.confidence || 0) < 60).length; const lowConfidenceSteps = rawAuditTrail.filter(entry => (entry.confidence || 0) < 60).length;
const groupedEntries = rawAuditTrail.reduce((groups, entry) => { const groupedEntries = rawAuditTrail.reduce((groups, entry) => {
const phase = entry.phase || 'unknown'; const phase = entry.phase || 'unknown';
if (!groups[phase]) groups[phase] = []; if (!groups[phase]) groups[phase] = [];
groups[phase].push(entry); groups[phase].push(entry);
return groups; return groups;
}, {} as Record<string, AuditEntry[]>); }, {} as Record<string, AuditEntry[]>);
const phases = Object.entries(groupedEntries).map(([phase, entries]) => { const phases = Object.entries(groupedEntries).map(([phase, entries]) => {
const phaseConfig = this.phaseConfig[phase] || { icon: '📋', displayName: phase }; const phaseConfig = this.phaseConfig[phase] || { icon: '📋', displayName: phase };
const validEntries = entries.filter(entry => entry && typeof entry === 'object'); const validEntries = entries.filter(entry => entry && typeof entry === 'object');
const phaseAvgConfidence = validEntries.length > 0 const phaseAvgConfidence = validEntries.length > 0
? Math.round(validEntries.reduce((sum, entry) => sum + (entry.confidence || 0), 0) / validEntries.length) ? Math.round(validEntries.reduce((sum, entry) => sum + (entry.confidence || 0), 0) / validEntries.length)
: 0; : 0;
const phaseTotalTime = validEntries.reduce((sum, entry) => sum + (entry.processingTimeMs || 0), 0); const phaseTotalTime = validEntries.reduce((sum, entry) => sum + (entry.processingTimeMs || 0), 0);
return { return {
name: phase, name: phase,
icon: phaseConfig.icon, icon: phaseConfig.icon,
displayName: phaseConfig.displayName, displayName: phaseConfig.displayName,
avgConfidence: phaseAvgConfidence, avgConfidence: phaseAvgConfidence,
totalTime: phaseTotalTime, totalTime: phaseTotalTime,
entries: validEntries entries: validEntries
.map(e => this.compressEntry(e)) .map(e => this.compressEntry(e))
.filter((e): e is CompressedAuditEntry => e !== null) .filter((e): e is CompressedAuditEntry => e !== null)
}; };
}).filter(phase => phase.entries.length > 0); }).filter(phase => phase.entries.length > 0);
const summary = this.generateSummary(rawAuditTrail, avgConfidence, lowConfidenceSteps); const summary = this.generateSummary(rawAuditTrail, avgConfidence, lowConfidenceSteps);
const result: ProcessedAuditTrail = { const result: ProcessedAuditTrail = {
totalTime, totalTime,
avgConfidence, avgConfidence,
stepCount: rawAuditTrail.length, stepCount: rawAuditTrail.length,
@ -233,25 +178,30 @@ class AuditService {
lowConfidenceSteps, lowConfidenceSteps,
phases, phases,
summary summary
}; };
console.log('[AUDIT] Successfully processed audit trail:', result); logger.debug('audit-service', 'Audit trail processed successfully', {
return result; totalTime: `${totalTime}ms`,
avgConfidence: `${avgConfidence}%`,
phases: phases.length
});
return result;
} catch (error) { } catch (error) {
console.error('[AUDIT] Error processing audit trail:', error); logger.error('audit-service', 'Error processing audit trail', error as Error);
return null; return null;
} }
} }
private compressEntry(entry: AuditEntry): CompressedAuditEntry | null { private compressEntry(entry: AuditEntry): CompressedAuditEntry | null {
if (!entry || typeof entry !== 'object') { if (!entry || typeof entry !== 'object') {
console.warn('[AUDIT] Invalid audit entry:', entry); logger.warn('audit-service', 'Invalid audit entry', { entry });
return null; return null;
} }
try { try {
return { return {
timestamp: entry.timestamp || Date.now(), timestamp: entry.timestamp || Date.now(),
phase: entry.phase || 'unknown', phase: entry.phase || 'unknown',
action: entry.action || 'unknown', action: entry.action || 'unknown',
@ -260,17 +210,17 @@ class AuditService {
confidence: entry.confidence || 0, confidence: entry.confidence || 0,
processingTimeMs: entry.processingTimeMs || 0, processingTimeMs: entry.processingTimeMs || 0,
metadata: entry.metadata || {} metadata: entry.metadata || {}
}; };
} catch (error) { } catch (error) {
console.error('[AUDIT] Error compressing entry:', error); logger.error('audit-service', 'Error compressing audit entry', error as Error);
return null; return null;
} }
} }
private compressData(data: any): any { private compressData(data: any): any {
if (this.config.detailLevel === 'verbose') { if (this.detailLevel === 'verbose') {
return data; return data;
} else if (this.config.detailLevel === 'standard') { } else if (this.detailLevel === 'standard') {
return this.summarizeForStorage(data); return this.summarizeForStorage(data);
} else { } else {
return this.minimalSummary(data); return this.minimalSummary(data);
@ -387,25 +337,12 @@ class AuditService {
} }
isEnabled(): boolean { isEnabled(): boolean {
return this.config.enabled; return this.enabled;
} }
getConfig(): AuditConfig { getConfig(): typeof config.audit {
return { ...this.config }; return config.audit;
} }
} }
export const auditService = new AuditService(); export const auditService = new AuditService();
export type { ProcessedAuditTrail, CompressedAuditEntry };
export const debugAuditService = {
getDebugInfo() {
return auditService.getDebugInfo();
},
isEnabled() {
return auditService.isEnabled();
},
getConfig() {
return auditService.getConfig();
}
};

View File

@ -1,36 +1,13 @@
// src/utils/clientUtils.ts // src/utils/clientUtils.ts
// Client-side utilities that mirror server-side toolHelpers.ts // Client-side utilities - now only contains AutocompleteManager to avoid duplication
// Other utilities consolidated into globalUtils.ts
export function createToolSlug(toolName: string): string { import { createToolSlug, findToolByIdentifier, isToolHosted } from './toolHelpers.js';
if (!toolName || typeof toolName !== 'string') {
console.warn('[toolHelpers] Invalid toolName provided to createToolSlug:', toolName);
return '';
}
return toolName.toLowerCase() // Re-export tool helper functions to maintain API compatibility
.replace(/[^a-z0-9\s-]/g, '') // Remove special characters export { createToolSlug, findToolByIdentifier, isToolHosted };
.replace(/\s+/g, '-') // Replace spaces with hyphens
.replace(/-+/g, '-') // Remove duplicate hyphens
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
}
export function findToolByIdentifier(tools: any[], identifier: string): any | undefined { // Consolidated Autocomplete Functionality - this is unique and not duplicated elsewhere
if (!identifier || !Array.isArray(tools)) return undefined;
return tools.find((tool: any) =>
tool.name === identifier ||
createToolSlug(tool.name) === identifier.toLowerCase()
);
}
export function isToolHosted(tool: any): boolean {
return tool.projectUrl !== undefined &&
tool.projectUrl !== null &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
}
// Consolidated Autocomplete Functionality
interface AutocompleteOptions { interface AutocompleteOptions {
minLength?: number; minLength?: number;
maxResults?: number; maxResults?: number;

314
src/utils/globalUtils.ts Normal file
View File

@ -0,0 +1,314 @@
// src/utils/globalUtils.ts
// Consolidated global utilities to eliminate duplication and provide consistent interface
import { createToolSlug, findToolByIdentifier, isToolHosted } from './toolHelpers.js';
import { auditService } from './auditService.js';
// Utility functions for UI interactions
export const UIUtils = {
scrollToElement(element: Element | null, options: ScrollIntoViewOptions = {}) {
if (!element) return;
setTimeout(() => {
const headerHeight = document.querySelector('nav')?.offsetHeight || 80;
const elementRect = element.getBoundingClientRect();
const absoluteElementTop = elementRect.top + window.pageYOffset;
const targetPosition = absoluteElementTop - headerHeight - 20;
window.scrollTo({
top: targetPosition,
behavior: 'smooth',
...options
});
}, 100);
},
scrollToElementById(elementId: string, options: ScrollIntoViewOptions = {}) {
const element = document.getElementById(elementId);
if (element) {
this.scrollToElement(element, options);
}
},
scrollToElementBySelector(selector: string, options: ScrollIntoViewOptions = {}) {
const element = document.querySelector(selector);
if (element) {
this.scrollToElement(element, options);
}
},
prioritizeSearchResults(tools: any[], searchTerm: string) {
if (!searchTerm || !searchTerm.trim()) {
return tools;
}
const lowerSearchTerm = searchTerm.toLowerCase().trim();
return tools.sort((a, b) => {
const aTagsLower = (a.tags || []).map((tag: string) => tag.toLowerCase());
const bTagsLower = (b.tags || []).map((tag: string) => tag.toLowerCase());
const aExactTag = aTagsLower.includes(lowerSearchTerm);
const bExactTag = bTagsLower.includes(lowerSearchTerm);
if (aExactTag && !bExactTag) return -1;
if (!aExactTag && bExactTag) return 1;
return a.name.localeCompare(b.name);
});
}
};
// Theme management
export const ThemeManager = {
THEME_KEY: 'dfir-theme',
getSystemTheme(): 'dark' | 'light' {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
},
getStoredTheme(): string {
return localStorage.getItem(this.THEME_KEY) || 'auto';
},
applyTheme(theme: string) {
const effectiveTheme = theme === 'auto' ? this.getSystemTheme() : theme;
document.documentElement.setAttribute('data-theme', effectiveTheme);
},
updateThemeToggle(theme: string) {
document.querySelectorAll('[data-theme-toggle]').forEach(button => {
button.setAttribute('data-current-theme', theme);
});
},
initTheme() {
const storedTheme = this.getStoredTheme();
this.applyTheme(storedTheme);
this.updateThemeToggle(storedTheme);
},
toggleTheme() {
const current = this.getStoredTheme();
const themes = ['light', 'dark', 'auto'];
const currentIndex = themes.indexOf(current);
const nextIndex = (currentIndex + 1) % themes.length;
const nextTheme = themes[nextIndex];
localStorage.setItem(this.THEME_KEY, nextTheme);
this.applyTheme(nextTheme);
this.updateThemeToggle(nextTheme);
}
};
// Authentication utilities
export const AuthManager = {
async checkClientAuth(context: string = 'general') {
try {
const response = await fetch('/api/auth/status');
const data = await response.json();
switch (context) {
case 'contributions':
return {
authenticated: data.contributionAuthenticated,
authRequired: data.contributionAuthRequired,
expires: data.expires
};
case 'ai':
return {
authenticated: data.aiAuthenticated,
authRequired: data.aiAuthRequired,
expires: data.expires
};
default:
return {
authenticated: data.authenticated,
authRequired: data.contributionAuthRequired || data.aiAuthRequired,
expires: data.expires
};
}
} catch (error) {
console.error('Auth check failed:', error);
return {
authenticated: false,
authRequired: true
};
}
},
async requireClientAuth(callback?: () => void, returnUrl?: string, context: string = 'general') {
const authStatus = await this.checkClientAuth(context);
if (authStatus.authRequired && !authStatus.authenticated) {
const targetUrl = returnUrl || window.location.href;
window.location.href = `/api/auth/login?returnTo=${encodeURIComponent(targetUrl)}`;
return false;
} else {
if (typeof callback === 'function') {
callback();
}
return true;
}
},
async showIfAuthenticated(selector: string, context: string = 'general') {
const authStatus = await this.checkClientAuth(context);
const element = document.querySelector(selector);
if (element) {
(element as HTMLElement).style.display = (!authStatus.authRequired || authStatus.authenticated)
? 'inline-flex'
: 'none';
}
},
setupAuthButtons(selector: string = '[data-contribute-button]') {
document.addEventListener('click', async (e) => {
if (!e.target) return;
const target = e.target as HTMLElement;
const button = target.closest(selector) as HTMLAnchorElement;
if (!button) return;
e.preventDefault();
await this.requireClientAuth(() => {
window.location.href = button.href;
}, button.href, 'contributions');
});
}
};
// Sharing utilities
export const SharingUtils = {
async copyUrlToClipboard(url: string, button: HTMLElement) {
try {
await navigator.clipboard.writeText(url);
const originalHTML = button.innerHTML;
button.innerHTML = `
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.375rem;">
<polyline points="20,6 9,17 4,12"/>
</svg>
Kopiert!
`;
button.style.color = 'var(--color-accent)';
setTimeout(() => {
button.innerHTML = originalHTML;
button.style.color = '';
}, 2000);
} catch (err) {
const textArea = document.createElement('textarea');
textArea.value = url;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
const originalHTML = button.innerHTML;
button.innerHTML = 'Kopiert!';
setTimeout(() => {
button.innerHTML = originalHTML;
}, 2000);
}
},
async shareArticle(button: HTMLElement, url: string, title: string) {
const fullUrl = window.location.origin + url;
await this.copyUrlToClipboard(fullUrl, button);
},
async shareCurrentArticle(button: HTMLElement) {
await this.copyUrlToClipboard(window.location.href, button);
}
};
// Audit utilities for window exposure
export const AuditUtils = {
formatDuration(ms: number): string {
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`;
},
getConfidenceColor(confidence: number): string {
if (confidence >= 80) return 'var(--color-accent)';
if (confidence >= 60) return 'var(--color-warning)';
return 'var(--color-error)';
},
escapeHtml(text: string): string {
if (typeof text !== 'string') return String(text);
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
},
sanitizeText(text: string): string {
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();
}
};
// Tool utilities (re-export from toolHelpers)
export const ToolUtils = {
createToolSlug,
findToolByIdentifier,
isToolHosted
};
// Centralized initialization function
export function initializeGlobalUtils() {
// Expose theme utilities
window.themeUtils = ThemeManager;
// Expose UI utilities
window.scrollToElement = UIUtils.scrollToElement.bind(UIUtils);
window.scrollToElementById = UIUtils.scrollToElementById.bind(UIUtils);
window.scrollToElementBySelector = UIUtils.scrollToElementBySelector.bind(UIUtils);
window.prioritizeSearchResults = UIUtils.prioritizeSearchResults.bind(UIUtils);
// Expose auth utilities
window.checkClientAuth = AuthManager.checkClientAuth.bind(AuthManager);
window.requireClientAuth = AuthManager.requireClientAuth.bind(AuthManager);
window.showIfAuthenticated = AuthManager.showIfAuthenticated.bind(AuthManager);
window.setupAuthButtons = AuthManager.setupAuthButtons.bind(AuthManager);
// Expose sharing utilities
window.shareArticle = SharingUtils.shareArticle.bind(SharingUtils);
window.shareCurrentArticle = SharingUtils.shareCurrentArticle.bind(SharingUtils);
// Expose tool utilities
window.createToolSlug = ToolUtils.createToolSlug;
window.findToolByIdentifier = ToolUtils.findToolByIdentifier;
window.isToolHosted = ToolUtils.isToolHosted;
// Expose audit utilities
window.AuditUtils = AuditUtils;
window.auditService = auditService;
}
// Listen for system theme changes
export function setupThemeChangeListener() {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
if (ThemeManager.getStoredTheme() === 'auto') {
ThemeManager.applyTheme('auto');
}
});
}

View File

@ -1,14 +1,7 @@
// src/utils/rateLimitedQueue.ts // src/utils/rateLimitedQueue.ts
import dotenv from "dotenv"; import { config } from '../config/appConfig.js';
import { logger } from '../services/logger.js';
dotenv.config();
const RATE_LIMIT_DELAY_MS =
Number.parseInt(process.env.AI_RATE_LIMIT_DELAY_MS ?? "2000", 10) || 2000;
const TASK_TIMEOUT_MS =
Number.parseInt(process.env.AI_TASK_TIMEOUT_MS ?? "300000", 10) || 300000;
export type Task<T = unknown> = () => Promise<T>; export type Task<T = unknown> = () => Promise<T>;
@ -32,8 +25,8 @@ export interface QueueStatus {
class RateLimitedQueue { class RateLimitedQueue {
private tasks: QueuedTask[] = []; private tasks: QueuedTask[] = [];
private isProcessing = false; private isProcessing = false;
private delayMs = RATE_LIMIT_DELAY_MS; private readonly delayMs: number;
private taskTimeoutMs = TASK_TIMEOUT_MS; private readonly taskTimeoutMs: number;
private lastProcessedAt = 0; private lastProcessedAt = 0;
private currentlyProcessingTaskId: string | null = null; private currentlyProcessingTaskId: string | null = null;
@ -41,9 +34,17 @@ class RateLimitedQueue {
private readonly TASK_RETENTION_MS = 300000; // 5 minutes private readonly TASK_RETENTION_MS = 300000; // 5 minutes
constructor() { constructor() {
this.delayMs = config.rateLimit.delayMs;
this.taskTimeoutMs = config.ai.taskTimeout;
this.cleanupInterval = setInterval(() => { this.cleanupInterval = setInterval(() => {
this.cleanupOldTasks(); this.cleanupOldTasks();
}, 30000); }, 30000);
logger.info('queue', 'Rate limited queue initialized', {
delayMs: this.delayMs,
taskTimeoutMs: this.taskTimeoutMs
});
} }
private cleanupOldTasks(): void { private cleanupOldTasks(): void {
@ -64,7 +65,10 @@ class RateLimitedQueue {
const cleaned = initialLength - this.tasks.length; const cleaned = initialLength - this.tasks.length;
if (cleaned > 0) { if (cleaned > 0) {
console.log(`[QUEUE] Cleaned up ${cleaned} old tasks, ${this.tasks.length} remaining`); logger.queue('cleanup completed', {
cleaned,
remaining: this.tasks.length
});
} }
} }
@ -72,6 +76,7 @@ class RateLimitedQueue {
if (this.cleanupInterval) { if (this.cleanupInterval) {
clearInterval(this.cleanupInterval); clearInterval(this.cleanupInterval);
} }
logger.info('queue', 'Rate limited queue shut down');
} }
add<T>(task: Task<T>, taskId?: string): Promise<T> { add<T>(task: Task<T>, taskId?: string): Promise<T> {
@ -96,6 +101,11 @@ class RateLimitedQueue {
this.tasks.push(queuedTask); this.tasks.push(queuedTask);
logger.queue('task queued', {
taskId: id,
queueLength: this.tasks.length
});
setTimeout(() => { setTimeout(() => {
this.processQueue(); this.processQueue();
}, 100); }, 100);
@ -162,6 +172,7 @@ class RateLimitedQueue {
if (this.isProcessing) return; if (this.isProcessing) return;
this.isProcessing = true; this.isProcessing = true;
logger.queue('processing started');
try { try {
while (true) { while (true) {
@ -176,6 +187,11 @@ class RateLimitedQueue {
this.currentlyProcessingTaskId = nextTask.id; this.currentlyProcessingTaskId = nextTask.id;
this.lastProcessedAt = Date.now(); this.lastProcessedAt = Date.now();
logger.queue('task processing started', {
taskId: nextTask.id,
queuePosition: 1
});
try { try {
await Promise.race([ await Promise.race([
nextTask.task(), nextTask.task(),
@ -189,25 +205,34 @@ class RateLimitedQueue {
nextTask.status = "completed"; nextTask.status = "completed";
nextTask.completedAt = Date.now(); nextTask.completedAt = Date.now();
console.log(`[QUEUE] Task ${nextTask.id} completed`);
logger.queue('task completed', {
taskId: nextTask.id,
duration: `${Date.now() - nextTask.startedAt!}ms`
});
} catch (error) { } catch (error) {
const err = error as Error; const err = error as Error;
nextTask.status = err.message.includes("timed out") ? "timedout" : "failed"; nextTask.status = err.message.includes("timed out") ? "timedout" : "failed";
nextTask.completedAt = Date.now(); nextTask.completedAt = Date.now();
console.error(`[QUEUE] Task ${nextTask.id} failed:`, error);
logger.error('queue', `Task ${nextTask.id} failed`, err, {
duration: `${Date.now() - nextTask.startedAt!}ms`,
status: nextTask.status
});
} }
this.currentlyProcessingTaskId = null; this.currentlyProcessingTaskId = null;
const hasMoreQueued = this.tasks.some((t) => t.status === "queued"); const hasMoreQueued = this.tasks.some((t) => t.status === "queued");
if (hasMoreQueued) { if (hasMoreQueued) {
console.log(`[QUEUE] Waiting ${this.delayMs}ms before next task`); logger.debug('queue', `Waiting ${this.delayMs}ms before next task`);
await new Promise((r) => setTimeout(r, this.delayMs)); await new Promise((r) => setTimeout(r, this.delayMs));
} }
} }
} finally { } finally {
this.isProcessing = false; this.isProcessing = false;
console.log(`[QUEUE] Queue processing finished`); logger.queue('processing finished');
} }
} }