Compare commits
21 Commits
57c507915f
...
5164aa640a
Author | SHA1 | Date | |
---|---|---|---|
5164aa640a | |||
![]() |
b515a45e1e | ||
![]() |
1c0025796a | ||
![]() |
769c223d39 | ||
![]() |
fe1be323bb | ||
![]() |
27e64f05ca | ||
![]() |
183e36b86d | ||
![]() |
8c5dc36788 | ||
![]() |
cba42962f6 | ||
![]() |
99117e8e7a | ||
![]() |
c267681e7d | ||
![]() |
1eb49315fa | ||
![]() |
f00e2d3cfd | ||
![]() |
a0955c2e58 | ||
![]() |
4b0d208ef5 | ||
![]() |
7c3cc7ec9a | ||
![]() |
3a5e8e88b2 | ||
![]() |
ec1969b2e2 | ||
![]() |
6c73a20dff | ||
![]() |
fe5eb78353 | ||
![]() |
6308c03709 |
File diff suppressed because one or more lines are too long
308
.env.example
308
.env.example
@ -1,79 +1,257 @@
|
|||||||
# ===========================================
|
# ============================================================================
|
||||||
# ForensicPathways Environment Configuration
|
# ForensicPathways Environment Configuration - COMPLETE
|
||||||
# ===========================================
|
# ============================================================================
|
||||||
|
# Copy this file to .env and adjust the values below.
|
||||||
|
# This file covers ALL environment variables used in the codebase.
|
||||||
|
|
||||||
# === Authentication Configuration ===
|
# ============================================================================
|
||||||
|
# 1. CORE APPLICATION SETTINGS (REQUIRED)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Your application's public URL (used for redirects and links)
|
||||||
|
PUBLIC_BASE_URL=http://localhost:4321
|
||||||
|
|
||||||
|
# Application environment
|
||||||
|
NODE_ENV=development
|
||||||
|
|
||||||
|
# Secret key for session encryption (CHANGE IN PRODUCTION!)
|
||||||
|
AUTH_SECRET=your-secret-key-change-in-production-please
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 2. AI SERVICES CONFIGURATION (REQUIRED FOR AI FEATURES)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Main AI Analysis Service (for query processing and recommendations)
|
||||||
|
# Examples: http://localhost:11434 (Ollama), https://api.mistral.ai, https://api.openai.com
|
||||||
|
AI_ANALYZER_ENDPOINT=https://api.mistral.ai/v1/chat/completions
|
||||||
|
AI_ANALYZER_API_KEY=
|
||||||
|
AI_ANALYZER_MODEL=mistral/mistral-small-latest
|
||||||
|
|
||||||
|
# Vector Embeddings Service (for semantic search)
|
||||||
|
# Leave API_KEY empty for Ollama, use actual key for cloud services
|
||||||
|
AI_EMBEDDINGS_ENABLED=true
|
||||||
|
AI_EMBEDDINGS_ENDPOINT=https://api.mistral.ai/v1/embeddings
|
||||||
|
AI_EMBEDDINGS_API_KEY=
|
||||||
|
AI_EMBEDDINGS_MODEL=mistral-embed
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 3. AI PIPELINE CONFIGURATION (CONTEXT & PERFORMANCE TUNING)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# === SIMILARITY SEARCH STAGE ===
|
||||||
|
# How many similar tools/concepts embeddings search returns as candidates
|
||||||
|
# 🔍 This is the FIRST filter - vector similarity matching
|
||||||
|
# Lower = faster, less comprehensive | Higher = slower, more comprehensive
|
||||||
|
AI_EMBEDDING_CANDIDATES=50
|
||||||
|
|
||||||
|
# Minimum similarity score threshold (0.0-1.0)
|
||||||
|
# Lower = more results but less relevant | Higher = fewer but more relevant
|
||||||
|
AI_SIMILARITY_THRESHOLD=0.3
|
||||||
|
|
||||||
|
# === AI SELECTION FROM EMBEDDINGS ===
|
||||||
|
# When embeddings are enabled, how many top tools to send with full context
|
||||||
|
# 🎯 This is the SECOND filter - take best N from embeddings results
|
||||||
|
AI_EMBEDDING_SELECTION_LIMIT=30
|
||||||
|
AI_EMBEDDING_CONCEPTS_LIMIT=15
|
||||||
|
|
||||||
|
# Maximum tools/concepts sent to AI when embeddings are DISABLED
|
||||||
|
# Set to 0 for no limit (WARNING: may cause token overflow with large datasets)
|
||||||
|
AI_NO_EMBEDDINGS_TOOL_LIMIT=0
|
||||||
|
AI_NO_EMBEDDINGS_CONCEPT_LIMIT=0
|
||||||
|
|
||||||
|
# === AI SELECTION STAGE ===
|
||||||
|
# Maximum tools the AI can select from embedding candidates
|
||||||
|
# 🤖 This is the SECOND filter - AI intelligent selection
|
||||||
|
# Should be ≤ AI_EMBEDDING_CANDIDATES
|
||||||
|
AI_MAX_SELECTED_ITEMS=25
|
||||||
|
|
||||||
|
# === EMBEDDINGS EFFICIENCY THRESHOLDS ===
|
||||||
|
# Minimum tools required for embeddings to be considered useful
|
||||||
|
AI_EMBEDDINGS_MIN_TOOLS=8
|
||||||
|
|
||||||
|
# Maximum percentage of total tools that embeddings can return to be considered "filtering"
|
||||||
|
AI_EMBEDDINGS_MAX_REDUCTION_RATIO=0.75
|
||||||
|
|
||||||
|
# === CONTEXT FLOW SUMMARY ===
|
||||||
|
# 1. Vector Search: 111 total tools → AI_EMBEDDING_CANDIDATES (40) most similar
|
||||||
|
# 2. AI Selection: 40 candidates → AI_MAX_SELECTED_ITEMS (25) best matches
|
||||||
|
# 3. Final Output: Recommendations based on analyzed subset
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 4. AI PERFORMANCE & RATE LIMITING
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# === USER RATE LIMITS (per minute) ===
|
||||||
|
# Main queries per user per minute
|
||||||
|
AI_RATE_LIMIT_MAX_REQUESTS=4
|
||||||
|
|
||||||
|
# Total AI micro-task calls per user per minute (across all micro-tasks)
|
||||||
|
AI_MICRO_TASK_TOTAL_LIMIT=30
|
||||||
|
|
||||||
|
# === PIPELINE TIMING ===
|
||||||
|
# Delay between micro-tasks within a single query (milliseconds)
|
||||||
|
# Higher = gentler on AI service | Lower = faster responses
|
||||||
|
AI_MICRO_TASK_DELAY_MS=500
|
||||||
|
|
||||||
|
# Delay between queued requests (milliseconds)
|
||||||
|
AI_RATE_LIMIT_DELAY_MS=2000
|
||||||
|
|
||||||
|
# === EMBEDDINGS BATCH PROCESSING ===
|
||||||
|
# How many embeddings to generate per API call
|
||||||
|
AI_EMBEDDINGS_BATCH_SIZE=10
|
||||||
|
|
||||||
|
# Delay between embedding batches (milliseconds)
|
||||||
|
AI_EMBEDDINGS_BATCH_DELAY_MS=1000
|
||||||
|
|
||||||
|
# Maximum tools sent to AI for detailed analysis (micro-tasks)
|
||||||
|
AI_MAX_TOOLS_TO_ANALYZE=20
|
||||||
|
AI_MAX_CONCEPTS_TO_ANALYZE=10
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 5. AI CONTEXT & TOKEN MANAGEMENT
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Maximum context tokens to maintain across micro-tasks
|
||||||
|
# Controls how much conversation history is preserved between AI calls
|
||||||
|
AI_MAX_CONTEXT_TOKENS=4000
|
||||||
|
|
||||||
|
# Maximum tokens per individual AI prompt
|
||||||
|
# Larger = more context per call | Smaller = faster responses
|
||||||
|
AI_MAX_PROMPT_TOKENS=2500
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 6. AUTHENTICATION & AUTHORIZATION (OPTIONAL)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Enable authentication for different features
|
||||||
AUTHENTICATION_NECESSARY=false
|
AUTHENTICATION_NECESSARY=false
|
||||||
AUTHENTICATION_NECESSARY_CONTRIBUTIONS=false
|
AUTHENTICATION_NECESSARY_CONTRIBUTIONS=false
|
||||||
AUTHENTICATION_NECESSARY_AI=false
|
AUTHENTICATION_NECESSARY_AI=false
|
||||||
AUTH_SECRET=your-secret-key-change-in-production
|
|
||||||
|
|
||||||
# OIDC Configuration (if authentication enabled)
|
# OIDC Provider Settings (only needed if authentication enabled)
|
||||||
OIDC_ENDPOINT=https://your-oidc-provider.com
|
OIDC_ENDPOINT=https://your-oidc-provider.com
|
||||||
OIDC_CLIENT_ID=your-client-id
|
OIDC_CLIENT_ID=your-client-id
|
||||||
OIDC_CLIENT_SECRET=your-client-secret
|
OIDC_CLIENT_SECRET=your-client-secret
|
||||||
|
|
||||||
# ===================================================================
|
# ============================================================================
|
||||||
# AI CONFIGURATION - Complete Reference for Improved Pipeline
|
# 7. FILE UPLOADS - NEXTCLOUD INTEGRATION (OPTIONAL)
|
||||||
# ===================================================================
|
# ============================================================================
|
||||||
|
|
||||||
# === CORE AI ENDPOINTS & MODELS ===
|
# Nextcloud server for file uploads (knowledgebase contributions)
|
||||||
AI_API_ENDPOINT=https://llm.mikoshi.de
|
# Leave empty to disable file upload functionality
|
||||||
AI_API_KEY=sREDACTED3w
|
|
||||||
AI_MODEL='mistral/mistral-small-latest'
|
|
||||||
|
|
||||||
# === IMPROVED PIPELINE: Use separate analyzer model (mistral-small is fine) ===
|
|
||||||
AI_ANALYZER_ENDPOINT=https://llm.mikoshi.de
|
|
||||||
AI_ANALYZER_API_KEY=skREDACTEDw3w
|
|
||||||
AI_ANALYZER_MODEL='mistral/mistral-small-latest'
|
|
||||||
|
|
||||||
# === EMBEDDINGS CONFIGURATION ===
|
|
||||||
AI_EMBEDDINGS_ENABLED=true
|
|
||||||
AI_EMBEDDINGS_ENDPOINT=https://api.mistral.ai/v1/embeddings
|
|
||||||
AI_EMBEDDINGS_API_KEY=ZREDACTED3wL
|
|
||||||
AI_EMBEDDINGS_MODEL=mistral-embed
|
|
||||||
AI_EMBEDDINGS_BATCH_SIZE=20
|
|
||||||
AI_EMBEDDINGS_BATCH_DELAY_MS=1000
|
|
||||||
|
|
||||||
# === PIPELINE: VectorIndex (HNSW) Configuration ===
|
|
||||||
AI_MAX_SELECTED_ITEMS=60 # Tools visible to each micro-task
|
|
||||||
AI_EMBEDDING_CANDIDATES=60 # VectorIndex candidates (HNSW is more efficient)
|
|
||||||
AI_SIMILARITY_THRESHOLD=0.3 # Not used by VectorIndex (uses cosine distance internally)
|
|
||||||
|
|
||||||
# === MICRO-TASK CONFIGURATION ===
|
|
||||||
AI_MICRO_TASK_DELAY_MS=500 # Delay between micro-tasks
|
|
||||||
AI_MICRO_TASK_TIMEOUT_MS=25000 # Timeout per micro-task (increased for full context)
|
|
||||||
|
|
||||||
# === RATE LIMITING ===
|
|
||||||
AI_RATE_LIMIT_DELAY_MS=3000 # Main rate limit delay
|
|
||||||
AI_RATE_LIMIT_MAX_REQUESTS=6 # Main requests per minute (reduced - fewer but richer calls)
|
|
||||||
AI_MICRO_TASK_RATE_LIMIT=15 # Micro-task requests per minute (was 30)
|
|
||||||
|
|
||||||
# === QUEUE MANAGEMENT ===
|
|
||||||
AI_QUEUE_MAX_SIZE=50
|
|
||||||
AI_QUEUE_CLEANUP_INTERVAL_MS=300000
|
|
||||||
|
|
||||||
# === PERFORMANCE & MONITORING ===
|
|
||||||
AI_MICRO_TASK_DEBUG=true
|
|
||||||
AI_PERFORMANCE_METRICS=true
|
|
||||||
AI_RESPONSE_CACHE_TTL_MS=3600000
|
|
||||||
|
|
||||||
# ===================================================================
|
|
||||||
# LEGACY VARIABLES (still used but less important)
|
|
||||||
# ===================================================================
|
|
||||||
|
|
||||||
# These are still used by other parts of the system:
|
|
||||||
AI_RESPONSE_CACHE_TTL_MS=3600000 # For caching responses
|
|
||||||
AI_QUEUE_MAX_SIZE=50 # Queue management
|
|
||||||
AI_QUEUE_CLEANUP_INTERVAL_MS=300000 # Queue cleanup
|
|
||||||
|
|
||||||
# === Application Configuration ===
|
|
||||||
PUBLIC_BASE_URL=http://localhost:4321
|
|
||||||
NODE_ENV=development
|
|
||||||
|
|
||||||
# Nextcloud Integration (Optional)
|
|
||||||
NEXTCLOUD_ENDPOINT=https://your-nextcloud.com
|
NEXTCLOUD_ENDPOINT=https://your-nextcloud.com
|
||||||
|
|
||||||
|
# Nextcloud credentials (app password recommended)
|
||||||
NEXTCLOUD_USERNAME=your-username
|
NEXTCLOUD_USERNAME=your-username
|
||||||
NEXTCLOUD_PASSWORD=your-password
|
NEXTCLOUD_PASSWORD=your-app-password
|
||||||
|
|
||||||
|
# Upload directory on Nextcloud (will be created if doesn't exist)
|
||||||
NEXTCLOUD_UPLOAD_PATH=/kb-media
|
NEXTCLOUD_UPLOAD_PATH=/kb-media
|
||||||
|
|
||||||
|
# Public URL base for sharing uploaded files
|
||||||
|
# Usually your Nextcloud base URL + share path
|
||||||
NEXTCLOUD_PUBLIC_URL=https://your-nextcloud.com/s/
|
NEXTCLOUD_PUBLIC_URL=https://your-nextcloud.com/s/
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 8. GIT CONTRIBUTIONS - ISSUE CREATION (OPTIONAL)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Git provider: gitea, github, or gitlab
|
||||||
|
GIT_PROVIDER=gitea
|
||||||
|
|
||||||
|
# Repository URL (used to extract owner/name)
|
||||||
|
# Example: https://git.example.com/owner/forensic-pathways.git
|
||||||
|
GIT_REPO_URL=https://git.example.com/owner/forensic-pathways.git
|
||||||
|
|
||||||
|
# API endpoint for your git provider
|
||||||
|
# Gitea: https://git.example.com/api/v1
|
||||||
|
# GitHub: https://api.github.com
|
||||||
|
# GitLab: https://gitlab.example.com/api/v4
|
||||||
|
GIT_API_ENDPOINT=https://git.example.com/api/v1
|
||||||
|
|
||||||
|
# Personal access token or API token for creating issues
|
||||||
|
# Generate this in your git provider's settings
|
||||||
|
GIT_API_TOKEN=your-git-api-token
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 9. AUDIT & DEBUGGING (OPTIONAL)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Enable detailed audit trail of AI decision-making
|
||||||
|
FORENSIC_AUDIT_ENABLED=true
|
||||||
|
|
||||||
|
# Audit detail level: minimal, standard, verbose
|
||||||
|
FORENSIC_AUDIT_DETAIL_LEVEL=standard
|
||||||
|
|
||||||
|
# Audit retention time (hours)
|
||||||
|
FORENSIC_AUDIT_RETENTION_HOURS=24
|
||||||
|
|
||||||
|
# Maximum audit entries per request
|
||||||
|
FORENSIC_AUDIT_MAX_ENTRIES=50
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 10. SIMPLIFIED CONFIDENCE SCORING SYSTEM
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Confidence component weights (must sum to 1.0)
|
||||||
|
CONFIDENCE_SEMANTIC_WEIGHT=0.5 # Weight for vector similarity quality
|
||||||
|
CONFIDENCE_SUITABILITY_WEIGHT=0.5 # Weight for AI-determined task fitness
|
||||||
|
|
||||||
|
# Confidence thresholds (0-100)
|
||||||
|
CONFIDENCE_MINIMUM_THRESHOLD=50 # Below this = weak recommendation
|
||||||
|
CONFIDENCE_MEDIUM_THRESHOLD=70 # 40-59 = weak, 60-79 = moderate
|
||||||
|
CONFIDENCE_HIGH_THRESHOLD=80 # 80+ = strong recommendation
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# PERFORMANCE TUNING PRESETS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 🚀 FOR FASTER RESPONSES (prevent token overflow):
|
||||||
|
# AI_NO_EMBEDDINGS_TOOL_LIMIT=25
|
||||||
|
# AI_NO_EMBEDDINGS_CONCEPT_LIMIT=10
|
||||||
|
|
||||||
|
# 🎯 FOR FULL DATABASE ACCESS (risk of truncation):
|
||||||
|
# AI_NO_EMBEDDINGS_TOOL_LIMIT=0
|
||||||
|
# AI_NO_EMBEDDINGS_CONCEPT_LIMIT=0
|
||||||
|
|
||||||
|
# 🔋 FOR LOW-POWER SYSTEMS:
|
||||||
|
# AI_NO_EMBEDDINGS_TOOL_LIMIT=15
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# FEATURE COMBINATIONS GUIDE
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 📝 BASIC SETUP (AI only):
|
||||||
|
# - Configure AI_ANALYZER_* and AI_EMBEDDINGS_*
|
||||||
|
# - Leave authentication, file uploads, and git disabled
|
||||||
|
|
||||||
|
# 🔐 WITH AUTHENTICATION:
|
||||||
|
# - Set AUTHENTICATION_NECESSARY_* to true
|
||||||
|
# - Configure OIDC_* settings
|
||||||
|
|
||||||
|
# 📁 WITH FILE UPLOADS:
|
||||||
|
# - Configure all NEXTCLOUD_* settings
|
||||||
|
# - Test connection before enabling in UI
|
||||||
|
|
||||||
|
# 🔄 WITH CONTRIBUTIONS:
|
||||||
|
# - Configure all GIT_* settings
|
||||||
|
# - Test API token permissions for issue creation
|
||||||
|
|
||||||
|
# 🔍 WITH FULL MONITORING:
|
||||||
|
# - Enable FORENSIC_AUDIT_ENABLED=true
|
||||||
|
# - Configure audit retention and detail level
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# SETUP CHECKLIST
|
||||||
|
# ============================================================================
|
||||||
|
# ✅ 1. Set PUBLIC_BASE_URL to your domain
|
||||||
|
# ✅ 2. Change AUTH_SECRET to a secure random string
|
||||||
|
# ✅ 3. Configure AI endpoints (Ollama: leave API_KEY empty)
|
||||||
|
# ✅ 4. Start with default AI values, tune based on performance
|
||||||
|
# ✅ 5. Enable authentication if needed (configure OIDC)
|
||||||
|
# ✅ 6. Configure Nextcloud if file uploads needed
|
||||||
|
# ✅ 7. Configure Git provider if contributions needed
|
||||||
|
# ✅ 8. Test with a simple query to verify pipeline works
|
||||||
|
# ✅ 9. Enable audit trail for transparency if desired
|
||||||
|
# ✅ 10. Tune performance settings based on usage patterns
|
||||||
|
# ============================================================================
|
@ -62,7 +62,7 @@ Ein kuratiertes Verzeichnis für Digital Forensics und Incident Response (DFIR)
|
|||||||
|
|
||||||
### AI Service (Mistral/OpenAI-kompatibel)
|
### AI Service (Mistral/OpenAI-kompatibel)
|
||||||
- **Zweck:** KI-gestützte Tool-Empfehlungen
|
- **Zweck:** KI-gestützte Tool-Empfehlungen
|
||||||
- **Konfiguration:** `AI_API_ENDPOINT`, `AI_API_KEY`, `AI_MODEL`
|
- **Konfiguration:** `AI_ANALYZER_ENDPOINT`, `AI_ANALYZER_API_KEY`, `AI_ANALYZER_MODEL`
|
||||||
|
|
||||||
### Uptime Kuma
|
### Uptime Kuma
|
||||||
- **Zweck:** Status-Monitoring für gehostete Services
|
- **Zweck:** Status-Monitoring für gehostete Services
|
||||||
@ -157,9 +157,9 @@ PUBLIC_BASE_URL=https://your-domain.com
|
|||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
|
|
||||||
# AI Service Configuration (Required for AI features)
|
# AI Service Configuration (Required for AI features)
|
||||||
AI_MODEL=mistral-large-latest
|
AI_ANALYZER_MODEL=mistral-large-latest
|
||||||
AI_API_ENDPOINT=https://api.mistral.ai
|
AI_ANALYZER_ENDPOINT=https://api.mistral.ai
|
||||||
AI_API_KEY=your-mistral-api-key
|
AI_ANALYZER_API_KEY=your-mistral-api-key
|
||||||
AI_RATE_LIMIT_DELAY_MS=1000
|
AI_RATE_LIMIT_DELAY_MS=1000
|
||||||
|
|
||||||
# Git Integration (Required for contributions)
|
# Git Integration (Required for contributions)
|
||||||
|
337
context.md
337
context.md
@ -1,337 +0,0 @@
|
|||||||
# ForensicPathways Architecture System Prompt
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
ForensicPathways is a curated directory of Digital Forensics and Incident Response (DFIR) tools, methods, and concepts built with Astro and TypeScript. It serves as an educational and professional resource for forensic investigators, following the NIST SP 800-86 framework (Kent, Chevalier, Grance & Dang).
|
|
||||||
|
|
||||||
## Core Technology Stack
|
|
||||||
- **Framework**: Astro (static site generator with islands architecture)
|
|
||||||
- **Language**: TypeScript with strict typing
|
|
||||||
- **Styling**: Vanilla CSS with custom properties (CSS variables)
|
|
||||||
- **Data Storage**: YAML files for tool catalog
|
|
||||||
- **Content**: Astro Content Collections for knowledge base articles
|
|
||||||
- **Authentication**: OIDC (OpenID Connect) with configurable requirements
|
|
||||||
- **AI Integration**: Mistral API for workflow recommendations
|
|
||||||
- **File Storage**: Nextcloud with local fallback
|
|
||||||
- **Version Control**: Git integration for community contributions
|
|
||||||
|
|
||||||
## Data Model Architecture
|
|
||||||
|
|
||||||
### Core Entity: Tool
|
|
||||||
```yaml
|
|
||||||
name: string # Tool identifier
|
|
||||||
icon: string? # Emoji icon
|
|
||||||
type: 'software'|'method'|'concept' # Tool classification
|
|
||||||
description: string # Detailed description
|
|
||||||
domains: string[] # Forensic domains (incident-response, malware-analysis, etc.)
|
|
||||||
phases: string[] # NIST framework phases (data-collection, examination, analysis, reporting)
|
|
||||||
platforms: string[] # Operating systems (for software only)
|
|
||||||
skillLevel: string # novice|beginner|intermediate|advanced|expert
|
|
||||||
url: string # Primary documentation/homepage
|
|
||||||
projectUrl: string? # Hosted instance URL (CC24 server)
|
|
||||||
license: string? # Software license
|
|
||||||
knowledgebase: boolean? # Has detailed documentation
|
|
||||||
tags: string[] # Searchable keywords
|
|
||||||
related_concepts: string[]? # Links to concept-type tools
|
|
||||||
related_software: string[]? #Links to software-type-tools
|
|
||||||
```
|
|
||||||
|
|
||||||
### Taxonomies
|
|
||||||
- **Domains**: Forensic specializations (7 main domains)
|
|
||||||
- **Phases**: NIST investigation phases (4 phases)
|
|
||||||
- **Domain-Agnostic-Software**: Cross-cutting tools and platforms
|
|
||||||
- **Skill Levels**: Standardized competency requirements
|
|
||||||
|
|
||||||
## Component Architecture
|
|
||||||
|
|
||||||
### Layout System
|
|
||||||
- **BaseLayout.astro**: Global layout with theme system, authentication setup, shared utilities
|
|
||||||
- **Navigation.astro**: Main navigation with active state management
|
|
||||||
- **Footer.astro**: Site footer with links and licensing info
|
|
||||||
|
|
||||||
### Core Components
|
|
||||||
- **ToolCard.astro**: Individual tool display with metadata, actions, and type-specific styling
|
|
||||||
- **ToolMatrix.astro**: Matrix view showing tools by domain/phase intersection
|
|
||||||
- **ToolFilters.astro**: Search, filtering, and view controls
|
|
||||||
- **AIQueryInterface.astro**: AI-powered workflow recommendation system
|
|
||||||
|
|
||||||
### Utility Components
|
|
||||||
- **ShareButton.astro**: URL sharing with multiple view options
|
|
||||||
- **ContributionButton.astro**: Authenticated contribution links
|
|
||||||
- **ThemeToggle.astro**: Light/dark/auto theme switching
|
|
||||||
|
|
||||||
## Feature Systems
|
|
||||||
|
|
||||||
### 1. Authentication System (`src/utils/auth.ts`)
|
|
||||||
- **OIDC Integration**: Uses environment-configured provider
|
|
||||||
- **Contextual Requirements**: Different auth requirements for contributions vs AI features
|
|
||||||
- **Session Management**: JWT-based with configurable expiration
|
|
||||||
- **Client-Side Utilities**: Window-level auth checking functions
|
|
||||||
|
|
||||||
### 2. AI Recommendation System
|
|
||||||
- **Dual Modes**:
|
|
||||||
- Workflow mode: Multi-phase recommendations for scenarios
|
|
||||||
- Tool mode: Specific tool recommendations for problems
|
|
||||||
- **Rate Limiting**: Queue-based system with status updates
|
|
||||||
- **Data Integration**: Uses compressed tool database for AI context
|
|
||||||
- **Response Validation**: Ensures AI only recommends existing tools
|
|
||||||
|
|
||||||
### 3. Contribution System
|
|
||||||
- **Git Integration**: Automated issue creation via Gitea/GitHub/GitLab APIs
|
|
||||||
- **Tool Contributions**: Form-based tool submissions
|
|
||||||
- **Knowledge Base**: Rich text with file upload support
|
|
||||||
- **Validation**: Client and server-side validation with Zod schemas
|
|
||||||
|
|
||||||
### 4. File Upload System
|
|
||||||
- **Primary**: Nextcloud integration with public link generation
|
|
||||||
- **Fallback**: Local file storage with public URL generation
|
|
||||||
- **Validation**: File type and size restrictions
|
|
||||||
- **Rate Limiting**: Per-user upload quotas
|
|
||||||
|
|
||||||
## Styling Architecture
|
|
||||||
|
|
||||||
### CSS Custom Properties System
|
|
||||||
```css
|
|
||||||
/* Core color system */
|
|
||||||
--color-primary: /* Adaptive based on theme */
|
|
||||||
--color-accent: /* Secondary brand color */
|
|
||||||
--color-bg: /* Main background */
|
|
||||||
--color-text: /* Primary text */
|
|
||||||
|
|
||||||
/* Component-specific colors */
|
|
||||||
--color-hosted: /* CC24 server hosted tools */
|
|
||||||
--color-oss: /* Open source tools */
|
|
||||||
--color-method: /* Methodology entries */
|
|
||||||
--color-concept: /* Knowledge concepts */
|
|
||||||
|
|
||||||
/* Theme system */
|
|
||||||
[data-theme="light"] { /* Light theme values */ }
|
|
||||||
[data-theme="dark"] { /* Dark theme values */ }
|
|
||||||
```
|
|
||||||
|
|
||||||
### Component Styling Patterns
|
|
||||||
- **Card System**: Consistent `.card` base with type-specific variants
|
|
||||||
- **Badge System**: Status and metadata indicators
|
|
||||||
- **Button System**: Semantic button classes with size variants
|
|
||||||
- **Grid System**: CSS Grid with responsive breakpoints
|
|
||||||
|
|
||||||
## Data Flow Architecture
|
|
||||||
|
|
||||||
### 1. Data Loading (`src/utils/dataService.ts`)
|
|
||||||
```typescript
|
|
||||||
// Daily randomization with seeded shuffle
|
|
||||||
getToolsData() → shuffled tools array
|
|
||||||
getCompressedToolsDataForAI() → AI-optimized dataset
|
|
||||||
clearCache() → cache invalidation
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Client-Side State Management
|
|
||||||
- **Global Tools Data**: Attached to `window.toolsData`
|
|
||||||
- **Filter State**: Component-local state with event emission
|
|
||||||
- **View State**: Grid/Matrix/AI view switching
|
|
||||||
- **Modal State**: Tool detail overlays with multi-modal support
|
|
||||||
|
|
||||||
### 3. Search and Filtering
|
|
||||||
- **Text Search**: Name, description, tags
|
|
||||||
- **Faceted Filtering**: Domain, phase, skill level, license type
|
|
||||||
- **Tag Cloud**: Frequency-weighted tag selection
|
|
||||||
- **Real-time Updates**: Immediate filtering with event system
|
|
||||||
|
|
||||||
## API Architecture
|
|
||||||
|
|
||||||
### Endpoint Structure
|
|
||||||
```
|
|
||||||
/api/auth/ # Authentication endpoints
|
|
||||||
login.ts # OIDC initiation
|
|
||||||
process.ts # OIDC callback processing
|
|
||||||
status.ts # Auth status checking
|
|
||||||
|
|
||||||
/api/contribute/ # Contribution endpoints
|
|
||||||
tool.ts # Tool submissions
|
|
||||||
knowledgebase.ts # KB article submissions
|
|
||||||
|
|
||||||
/api/ai/ # AI features
|
|
||||||
query.ts # Workflow recommendations
|
|
||||||
queue-status.ts # Queue monitoring
|
|
||||||
|
|
||||||
/api/upload/ # File handling
|
|
||||||
media.ts # File upload processing
|
|
||||||
|
|
||||||
/api/health.ts # System health check
|
|
||||||
```
|
|
||||||
|
|
||||||
### Response Patterns
|
|
||||||
All APIs use consolidated response utilities (`src/utils/api.ts`):
|
|
||||||
- **Success**: `apiResponse.success()`, `apiResponse.created()`
|
|
||||||
- **Errors**: `apiError.badRequest()`, `apiError.unauthorized()`, etc.
|
|
||||||
- **Server Errors**: `apiServerError.internal()`, etc.
|
|
||||||
|
|
||||||
## File Organization Patterns
|
|
||||||
|
|
||||||
### Page Structure
|
|
||||||
- **Static Pages**: About, impressum, status
|
|
||||||
- **Dynamic Pages**: Tool details, knowledge base articles
|
|
||||||
- **Authenticated Pages**: Contribution forms
|
|
||||||
- **API Routes**: RESTful endpoints with consistent naming
|
|
||||||
|
|
||||||
### Utility Organization
|
|
||||||
- **Tool Operations**: `toolHelpers.ts` - Core tool manipulation
|
|
||||||
- **Data Management**: `dataService.ts` - YAML loading and caching
|
|
||||||
- **Authentication**: `auth.ts` - OIDC flow and session management
|
|
||||||
- **External APIs**: `gitContributions.ts`, `nextcloud.ts`
|
|
||||||
- **Rate Limiting**: `rateLimitedQueue.ts` - AI request queuing
|
|
||||||
|
|
||||||
## Key Architectural Decisions
|
|
||||||
|
|
||||||
### 1. Static-First with Dynamic Islands
|
|
||||||
- Astro's islands architecture for interactivity
|
|
||||||
- Static generation for performance
|
|
||||||
- Selective hydration for complex components
|
|
||||||
|
|
||||||
### 2. YAML-Based Data Management
|
|
||||||
- Human-readable tool catalog
|
|
||||||
- Git-friendly versioning
|
|
||||||
- Type-safe loading with Zod validation
|
|
||||||
|
|
||||||
### 3. Contextual Authentication
|
|
||||||
- Optional authentication based on feature
|
|
||||||
- Graceful degradation for unauthenticated users
|
|
||||||
- Environment-configurable requirements
|
|
||||||
|
|
||||||
### 4. Multi-Modal UI Patterns
|
|
||||||
- Grid view for browsing
|
|
||||||
- Matrix view for relationship visualization
|
|
||||||
- AI interface for guided recommendations
|
|
||||||
|
|
||||||
### 5. Progressive Enhancement
|
|
||||||
- Core functionality works without JavaScript
|
|
||||||
- Enhanced features require client-side hydration
|
|
||||||
- Responsive design with mobile-first approach
|
|
||||||
|
|
||||||
## Development Patterns
|
|
||||||
|
|
||||||
### Component Design
|
|
||||||
- Props interfaces with TypeScript
|
|
||||||
- Consistent styling via CSS classes
|
|
||||||
- Event-driven communication between components
|
|
||||||
- Server-side rendering with client-side enhancement
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
- Comprehensive try-catch in API routes
|
|
||||||
- User-friendly error messages
|
|
||||||
- Graceful fallbacks for external services
|
|
||||||
- Logging for debugging and monitoring
|
|
||||||
|
|
||||||
### Performance Optimizations
|
|
||||||
- Daily data randomization with caching
|
|
||||||
- Compressed datasets for AI context
|
|
||||||
- Rate limiting for external API calls
|
|
||||||
- Efficient DOM updates with targeted selectors
|
|
||||||
|
|
||||||
This architecture emphasizes maintainability, user experience, and extensibility while managing the complexity of a multi-feature forensics tool directory.
|
|
||||||
|
|
||||||
|
|
||||||
## Absolutely Essential (Core Architecture - 8 files)
|
|
||||||
|
|
||||||
**File**: `src/data/tools.yaml.example`
|
|
||||||
**Why**: Defines the core data model - what a "tool" is, the schema, domains, phases, etc. Without this, an AI can't understand what the application manages.
|
|
||||||
|
|
||||||
**File**: `src/pages/index.astro`
|
|
||||||
**Why**: Main application entry point showing the core functionality (filters, matrix, AI interface, tool grid). Contains the primary application logic flow.
|
|
||||||
|
|
||||||
**File**: `src/layouts/BaseLayout.astro`
|
|
||||||
**Why**: Global layout with theme system, authentication setup, and shared utility functions. Shows the overall app structure.
|
|
||||||
|
|
||||||
**File**: `src/utils/dataService.ts`
|
|
||||||
**Why**: Core data loading and processing logic. Essential for understanding how data flows through the application.
|
|
||||||
|
|
||||||
**File**: `src/utils/toolHelpers.ts`
|
|
||||||
**Why**: Core utility functions used throughout the app (slug creation, tool identification, hosting checks).
|
|
||||||
|
|
||||||
**File**: `src/styles/global.css`
|
|
||||||
**Why**: Complete styling system - defines visual architecture, component styling, theme system, responsive design.
|
|
||||||
|
|
||||||
**File**: `src/env.d.ts`
|
|
||||||
**Why**: Global TypeScript definitions and window interface extensions. Shows the global API surface.
|
|
||||||
|
|
||||||
**File**: `src/components/ToolCard.astro`
|
|
||||||
**Why**: Shows how the core entity (tools) are rendered and structured. Represents the component architecture pattern.
|
|
||||||
|
|
||||||
## Secondary Priority (Major Features - 4 files)
|
|
||||||
|
|
||||||
**File**: `src/components/ToolMatrix.astro` - Matrix view functionality
|
|
||||||
**File**: `src/components/AIQueryInterface.astro` - AI recommendation system
|
|
||||||
**File**: `src/utils/auth.ts` - Authentication system
|
|
||||||
**File**: `src/content/config.ts` - Content collection schema
|
|
||||||
|
|
||||||
## Strategy for Context Management
|
|
||||||
|
|
||||||
1. **Always provide**: The 8 essential files above
|
|
||||||
2. **Add selectively**: Include 1-3 secondary files based on the specific development task
|
|
||||||
3. **Reference others**: Mention other relevant files by name/purpose without including full content
|
|
||||||
|
|
||||||
user01@altiera /v/h/u/P/forensic-pathways (main)> tree src
|
|
||||||
src
|
|
||||||
├── components
|
|
||||||
│ ├── AIQueryInterface.astro
|
|
||||||
│ ├── ContributionButton.astro
|
|
||||||
│ ├── Footer.astro
|
|
||||||
│ ├── Navigation.astro
|
|
||||||
│ ├── ShareButton.astro
|
|
||||||
│ ├── ThemeToggle.astro
|
|
||||||
│ ├── ToolCard.astro
|
|
||||||
│ ├── ToolFilters.astro
|
|
||||||
│ └── ToolMatrix.astro
|
|
||||||
├── content
|
|
||||||
│ ├── config.ts
|
|
||||||
│ └── knowledgebase
|
|
||||||
│ ├── android-logical-imaging.md
|
|
||||||
│ ├── kali-linux.md
|
|
||||||
│ ├── misp.md
|
|
||||||
│ ├── nextcloud.md
|
|
||||||
│ ├── regular-expressions-regex.md
|
|
||||||
│ └── velociraptor.md
|
|
||||||
├── data
|
|
||||||
│ ├── tools.yaml
|
|
||||||
│ └── tools.yaml.example
|
|
||||||
├── env.d.ts
|
|
||||||
├── layouts
|
|
||||||
│ └── BaseLayout.astro
|
|
||||||
├── pages
|
|
||||||
│ ├── about.astro
|
|
||||||
│ ├── api
|
|
||||||
│ │ ├── ai
|
|
||||||
│ │ │ ├── query.ts
|
|
||||||
│ │ │ └── queue-status.ts
|
|
||||||
│ │ ├── auth
|
|
||||||
│ │ │ ├── login.ts
|
|
||||||
│ │ │ ├── process.ts
|
|
||||||
│ │ │ └── status.ts
|
|
||||||
│ │ ├── contribute
|
|
||||||
│ │ │ ├── knowledgebase.ts
|
|
||||||
│ │ │ └── tool.ts
|
|
||||||
│ │ ├── health.ts
|
|
||||||
│ │ └── upload
|
|
||||||
│ │ └── media.ts
|
|
||||||
│ ├── auth
|
|
||||||
│ │ └── callback.astro
|
|
||||||
│ ├── contribute
|
|
||||||
│ │ ├── index.astro
|
|
||||||
│ │ ├── knowledgebase.astro
|
|
||||||
│ │ └── tool.astro
|
|
||||||
│ ├── impressum.astro
|
|
||||||
│ ├── index.astro
|
|
||||||
│ ├── knowledgebase
|
|
||||||
│ │ └── [slug].astro
|
|
||||||
│ ├── knowledgebase.astro
|
|
||||||
│ └── status.astro
|
|
||||||
├── styles
|
|
||||||
│ └── global.css
|
|
||||||
└── utils
|
|
||||||
├── api.ts
|
|
||||||
├── auth.ts
|
|
||||||
├── dataService.ts
|
|
||||||
├── gitContributions.ts
|
|
||||||
├── nextcloud.ts
|
|
||||||
├── rateLimitedQueue.ts
|
|
||||||
└── toolHelpers.ts
|
|
||||||
17 directories, 47 files
|
|
File diff suppressed because it is too large
Load Diff
@ -20,7 +20,11 @@
|
|||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/js-yaml": "^4.0.9"
|
"@types/js-yaml": "^4.0.9",
|
||||||
|
"fast-glob": "^3.3.3",
|
||||||
|
"picocolors": "^1.1.1",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"postcss-safe-parser": "^7.0.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
import { getToolsData } from '../utils/dataService.js';
|
import { getToolsData } from '../utils/dataService.js';
|
||||||
|
import { isToolHosted } from '../utils/toolHelpers.js';
|
||||||
|
|
||||||
const data = await getToolsData();
|
const data = await getToolsData();
|
||||||
const tools = data.tools;
|
const tools = data.tools;
|
||||||
@ -15,7 +16,7 @@ const domainAgnosticSoftware = data['domain-agnostic-software'] || [];
|
|||||||
<path d="M9 11H5a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7a2 2 0 0 0-2-2h-4"/>
|
<path d="M9 11H5a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7a2 2 0 0 0-2-2h-4"/>
|
||||||
<path d="M9 11V7a3 3 0 0 1 6 0v4"/>
|
<path d="M9 11V7a3 3 0 0 1 6 0v4"/>
|
||||||
</svg>
|
</svg>
|
||||||
KI-gestützte Workflow-Empfehlungen
|
Forensic AI
|
||||||
</h2>
|
</h2>
|
||||||
<p id="ai-description" class="text-muted" style="max-width: 700px; margin: 0 auto; line-height: 1.6;">
|
<p id="ai-description" class="text-muted" style="max-width: 700px; margin: 0 auto; line-height: 1.6;">
|
||||||
Beschreiben Sie Ihr forensisches Szenario und erhalten Sie maßgeschneiderte Workflow-Empfehlungen
|
Beschreiben Sie Ihr forensisches Szenario und erhalten Sie maßgeschneiderte Workflow-Empfehlungen
|
||||||
@ -169,16 +170,16 @@ const domainAgnosticSoftware = data['domain-agnostic-software'] || [];
|
|||||||
<!-- Micro-task Progress -->
|
<!-- Micro-task Progress -->
|
||||||
<div id="micro-task-progress" class="micro-task-progress hidden">
|
<div id="micro-task-progress" class="micro-task-progress hidden">
|
||||||
<div class="micro-task-header">
|
<div class="micro-task-header">
|
||||||
<span class="micro-task-label">🔬 Micro-Task Analyse</span>
|
<span class="micro-task-label">🔬 micro-Agent-Analysis</span>
|
||||||
<span id="micro-task-counter" class="micro-task-counter">1/6</span>
|
<span id="micro-task-counter" class="micro-task-counter">1/6</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="micro-task-steps">
|
<div class="micro-task-steps">
|
||||||
<div class="micro-step" data-step="scenario">📋 Szenario</div>
|
<div class="micro-step" data-step="scenario">📋 Problemanalyse</div>
|
||||||
<div class="micro-step" data-step="approach">🎯 Ansatz</div>
|
<div class="micro-step" data-step="approach">🎯 Ermittlungsansatz</div>
|
||||||
<div class="micro-step" data-step="considerations">⚠️ Kritisches</div>
|
<div class="micro-step" data-step="considerations">⚠️ Herausforderungen</div>
|
||||||
<div class="micro-step" data-step="tools">🔧 Tools</div>
|
<div class="micro-step" data-step="tools">🔧 Methoden</div>
|
||||||
<div class="micro-step" data-step="knowledge">📚 Wissen</div>
|
<div class="micro-step" data-step="knowledge">📚 Evaluation</div>
|
||||||
<div class="micro-step" data-step="final">✅ Final</div>
|
<div class="micro-step" data-step="final">✅ Audit-Trail</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -292,13 +293,13 @@ class AIQueryInterface {
|
|||||||
return {
|
return {
|
||||||
workflow: {
|
workflow: {
|
||||||
placeholder: "Beschreiben Sie Ihr forensisches Szenario... z.B. 'Verdacht auf Ransomware-Angriff auf Windows-Domänencontroller'",
|
placeholder: "Beschreiben Sie Ihr forensisches Szenario... z.B. 'Verdacht auf Ransomware-Angriff auf Windows-Domänencontroller'",
|
||||||
description: "Beschreiben Sie Ihr forensisches Szenario und erhalten Sie maßgeschneiderte Workflow-Empfehlungen.",
|
description: "Beschreiben Sie Ihre Untersuchungssituation und erhalten Empfehlungen für alle Phasen der Untersuchung.",
|
||||||
submitText: "Empfehlungen generieren",
|
submitText: "Empfehlungen generieren",
|
||||||
loadingText: "Analysiere Szenario und generiere Empfehlungen..."
|
loadingText: "Analysiere Szenario und generiere Empfehlungen..."
|
||||||
},
|
},
|
||||||
tool: {
|
tool: {
|
||||||
placeholder: "Beschreiben Sie Ihr Problem... z.B. 'Analyse von Android-Backups mit WhatsApp-Nachrichten'",
|
placeholder: "Beschreiben Sie Ihr Problem... z.B. 'Analyse von Android-Backups mit WhatsApp-Nachrichten'",
|
||||||
description: "Beschreiben Sie Ihr Problem und erhalten Sie 1-3 gezielt passende Empfehlungen.",
|
description: "Beschreiben Sie Ihre Untersuchungssituation und erhalten Empfehlungen für eine spezifische Aufgabenstellung.",
|
||||||
submitText: "Empfehlungen finden",
|
submitText: "Empfehlungen finden",
|
||||||
loadingText: "Analysiere Anforderungen und suche passende Methode..."
|
loadingText: "Analysiere Anforderungen und suche passende Methode..."
|
||||||
}
|
}
|
||||||
@ -671,6 +672,15 @@ class AIQueryInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
displayResults(recommendation, originalQuery) {
|
displayResults(recommendation, originalQuery) {
|
||||||
|
console.log('[AI DEBUG] Full recommendation object:', recommendation);
|
||||||
|
console.log('[AI DEBUG] Recommended tools:', recommendation.recommended_tools);
|
||||||
|
|
||||||
|
if (recommendation.recommended_tools) {
|
||||||
|
recommendation.recommended_tools.forEach((tool, index) => {
|
||||||
|
console.log(`[AI DEBUG] Tool ${index}:`, tool.name, 'Confidence:', tool.confidence);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (this.currentMode === 'workflow') {
|
if (this.currentMode === 'workflow') {
|
||||||
this.displayWorkflowResults(recommendation, originalQuery);
|
this.displayWorkflowResults(recommendation, originalQuery);
|
||||||
} else {
|
} else {
|
||||||
@ -692,13 +702,22 @@ class AIQueryInterface {
|
|||||||
toolsByPhase[phase] = [];
|
toolsByPhase[phase] = [];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('[AI Results] Recommendation structure:', recommendation);
|
||||||
|
console.log('[AI Results] Recommended tools:', recommendation.recommended_tools);
|
||||||
|
|
||||||
recommendation.recommended_tools?.forEach(recTool => {
|
recommendation.recommended_tools?.forEach(recTool => {
|
||||||
|
console.log('[AI Results] Tool confidence data:', recTool.name, recTool.confidence);
|
||||||
|
|
||||||
if (toolsByPhase[recTool.phase]) {
|
if (toolsByPhase[recTool.phase]) {
|
||||||
const fullTool = tools.find(t => t.name === recTool.name);
|
const fullTool = tools.find(t => t.name === recTool.name);
|
||||||
if (fullTool) {
|
if (fullTool) {
|
||||||
toolsByPhase[recTool.phase].push({
|
toolsByPhase[recTool.phase].push({
|
||||||
...fullTool,
|
...fullTool,
|
||||||
recommendation: recTool
|
recommendation: recTool,
|
||||||
|
confidence: recTool.confidence,
|
||||||
|
justification: recTool.justification,
|
||||||
|
priority: recTool.priority,
|
||||||
|
recommendationStrength: recTool.recommendationStrength
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -706,11 +725,12 @@ class AIQueryInterface {
|
|||||||
|
|
||||||
const html = `
|
const html = `
|
||||||
<div class="workflow-container">
|
<div class="workflow-container">
|
||||||
${this.renderHeader('Empfohlener DFIR-Workflow', originalQuery)}
|
${this.renderHeader('Untersuchungsansatz', originalQuery)}
|
||||||
${this.renderContextualAnalysis(recommendation, 'workflow')}
|
${this.renderContextualAnalysis(recommendation, 'workflow')}
|
||||||
${this.renderBackgroundKnowledge(recommendation.background_knowledge)}
|
${this.renderBackgroundKnowledge(recommendation.background_knowledge)}
|
||||||
${this.renderWorkflowPhases(toolsByPhase, phaseOrder, phaseNames)}
|
${this.renderWorkflowPhases(toolsByPhase, phaseOrder, phaseNames)}
|
||||||
${this.renderWorkflowSuggestion(recommendation.workflow_suggestion)}
|
${this.renderWorkflowSuggestion(recommendation.workflow_suggestion)}
|
||||||
|
${this.renderAuditTrail(recommendation.auditTrail)}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -719,18 +739,434 @@ class AIQueryInterface {
|
|||||||
|
|
||||||
displayToolResults(recommendation, originalQuery) {
|
displayToolResults(recommendation, originalQuery) {
|
||||||
const html = `
|
const html = `
|
||||||
<div class="tool-results-container">
|
<div class="workflow-container">
|
||||||
${this.renderHeader('Passende Empfehlungen', originalQuery)}
|
${this.renderHeader('Handlungsempfehlung', originalQuery)}
|
||||||
${this.renderContextualAnalysis(recommendation, 'tool')}
|
${this.renderContextualAnalysis(recommendation, 'tool')}
|
||||||
${this.renderBackgroundKnowledge(recommendation.background_knowledge)}
|
${this.renderBackgroundKnowledge(recommendation.background_knowledge)}
|
||||||
${this.renderToolRecommendations(recommendation.recommended_tools)}
|
${this.renderToolRecommendations(recommendation.recommended_tools)}
|
||||||
${this.renderAdditionalConsiderations(recommendation.additional_considerations)}
|
${this.renderAdditionalConsiderations(recommendation.additional_considerations)}
|
||||||
|
${this.renderAuditTrail(recommendation.auditTrail)}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
this.elements.results.innerHTML = html;
|
this.elements.results.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderConfidenceTooltip(confidence) {
|
||||||
|
if (!confidence || typeof confidence.overall !== 'number') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const confidenceColor = confidence.overall >= 80 ? 'var(--color-accent)' :
|
||||||
|
confidence.overall >= 60 ? 'var(--color-warning)' : 'var(--color-error)';
|
||||||
|
|
||||||
|
const tooltipId = `tooltip-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<span class="confidence-tooltip-trigger"
|
||||||
|
style="display: inline-flex; align-items: center; gap: 0.125rem; cursor: help; margin-left: 0.25rem;"
|
||||||
|
onmouseenter="document.getElementById('${tooltipId}').style.display = 'block'"
|
||||||
|
onmouseleave="document.getElementById('${tooltipId}').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 id="${tooltipId}" style="display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); 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);">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem;">
|
||||||
|
<strong style="font-size: 0.875rem;">KI-Vertrauenswertung</strong>
|
||||||
|
<span class="badge badge-mini" style="background-color: ${confidenceColor}; color: white; font-weight: 600;">${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;">🔍 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 (Vektor-Ähnlichkeit)
|
||||||
|
</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;">🎯 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 des Tools für Ihre spezifische forensische Aufgabenstellung
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${confidence.strengthIndicators && confidence.strengthIndicators.length > 0 ? `
|
||||||
|
<div style="margin-bottom: 0.75rem; padding: 0.5rem; background: var(--color-oss-bg); border-radius: 0.375rem; border-left: 3px solid var(--color-accent);">
|
||||||
|
<strong style="color: var(--color-accent); font-size: 0.6875rem; display: flex; align-items: center; gap: 0.25rem; margin-bottom: 0.375rem;">
|
||||||
|
<span>✓</span> Stärken dieser Empfehlung:
|
||||||
|
</strong>
|
||||||
|
<ul style="margin: 0; padding-left: 1rem; font-size: 0.625rem; line-height: 1.4;">
|
||||||
|
${confidence.strengthIndicators.slice(0, 3).map(s => `<li style="margin-bottom: 0.25rem;">${this.sanitizeText(s)}</li>`).join('')}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
${confidence.uncertaintyFactors && confidence.uncertaintyFactors.length > 0 ? `
|
||||||
|
<div style="padding: 0.5rem; background: var(--color-hosted-bg); border-radius: 0.375rem; border-left: 3px solid var(--color-warning);">
|
||||||
|
<strong style="color: var(--color-warning); font-size: 0.6875rem; display: flex; align-items: center; gap: 0.25rem; margin-bottom: 0.375rem;">
|
||||||
|
<span>⚠</span> Mögliche Einschränkungen:
|
||||||
|
</strong>
|
||||||
|
<ul style="margin: 0; padding-left: 1rem; font-size: 0.625rem; line-height: 1.4;">
|
||||||
|
${confidence.uncertaintyFactors.slice(0, 3).map(f => `<li style="margin-bottom: 0.25rem;">${this.sanitizeText(f)}</li>`).join('')}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<div style="margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px solid var(--color-border); font-size: 0.625rem; color: var(--color-text-secondary); text-align: center;">
|
||||||
|
Forensisch fundierte KI-Analyse
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderAuditTrail(auditTrail) {
|
||||||
|
if (!auditTrail || !Array.isArray(auditTrail) || auditTrail.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalTime = auditTrail.reduce((sum, entry) => sum + entry.processingTimeMs, 0);
|
||||||
|
const avgConfidence = auditTrail.reduce((sum, entry) => sum + entry.confidence, 0) / auditTrail.length;
|
||||||
|
const lowConfidenceSteps = auditTrail.filter(entry => entry.confidence < 60).length;
|
||||||
|
const highConfidenceSteps = auditTrail.filter(entry => entry.confidence >= 80).length;
|
||||||
|
|
||||||
|
const groupedEntries = auditTrail.reduce((groups, entry) => {
|
||||||
|
if (!groups[entry.phase]) groups[entry.phase] = [];
|
||||||
|
groups[entry.phase].push(entry);
|
||||||
|
return groups;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="card-info-sm mt-4" style="border-left: 4px solid var(--color-accent);">
|
||||||
|
<div class="flex items-center justify-between mb-3 cursor-pointer" onclick="const container = this.closest('.card-info-sm'); const details = container.querySelector('.audit-trail-details'); const isHidden = details.style.display === 'none'; details.style.display = isHidden ? 'block' : 'none'; this.querySelector('.toggle-icon').style.transform = isHidden ? 'rotate(180deg)' : 'rotate(0deg)';">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div style="width: 24px; height: 24px; background: linear-gradient(135deg, var(--color-accent) 0%, var(--color-primary) 100%); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white; font-size: 0.75rem; font-weight: bold;">
|
||||||
|
✓
|
||||||
|
</div>
|
||||||
|
<h4 class="text-sm font-semibold text-accent mb-0">KI-Entscheidungspfad</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-3 text-xs">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<div class="w-2 h-2 rounded-full" style="background-color: var(--color-accent);"></div>
|
||||||
|
<span class="text-muted">${this.formatDuration(totalTime)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<div class="w-2 h-2 rounded-full" style="background-color: ${avgConfidence >= 80 ? 'var(--color-accent)' : avgConfidence >= 60 ? 'var(--color-warning)' : 'var(--color-error)'};"></div>
|
||||||
|
<span class="text-muted">${Math.round(avgConfidence)}% Vertrauen</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<span class="text-muted">${auditTrail.length} Schritte</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toggle-icon" style="transition: transform 0.2s ease;">
|
||||||
|
<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 style="display: none;" class="audit-trail-details">
|
||||||
|
<div class="grid gap-4">
|
||||||
|
<!-- Summary Section -->
|
||||||
|
<div class="p-3 rounded-lg" style="background-color: var(--color-bg-tertiary);">
|
||||||
|
<div class="text-xs font-medium mb-2 text-accent">📊 Analyse-Qualität</div>
|
||||||
|
<div class="grid grid-cols-3 gap-3 text-xs">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="font-semibold" style="color: var(--color-accent);">${highConfidenceSteps}</div>
|
||||||
|
<div class="text-muted">Hohe Sicherheit</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="font-semibold" style="color: ${lowConfidenceSteps > 0 ? 'var(--color-warning)' : 'var(--color-accent)'};">${lowConfidenceSteps}</div>
|
||||||
|
<div class="text-muted">Unsichere Schritte</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="font-semibold">${this.formatDuration(totalTime)}</div>
|
||||||
|
<div class="text-muted">Verarbeitungszeit</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Process Flow -->
|
||||||
|
<div class="audit-process-flow">
|
||||||
|
${Object.entries(groupedEntries).map(([phase, entries]) => this.renderPhaseGroup(phase, entries)).join('')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Technical Details Toggle -->
|
||||||
|
<div class="text-center">
|
||||||
|
<button class="text-xs text-muted hover:text-primary transition-colors cursor-pointer border-none bg-none" onclick="const techDetails = this.nextElementSibling; const isHidden = techDetails.style.display === 'none'; techDetails.style.display = isHidden ? 'block' : 'none'; this.textContent = isHidden ? '🔧 Technische Details ausblenden' : '🔧 Technische Details anzeigen';">
|
||||||
|
🔧 Technische Details anzeigen
|
||||||
|
</button>
|
||||||
|
<div style="display: none;" class="mt-3 p-3 rounded-lg bg-gray-50 dark:bg-gray-800 text-xs">
|
||||||
|
${auditTrail.map(entry => this.renderTechnicalEntry(entry)).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPhaseGroup(phase, entries) {
|
||||||
|
const phaseIcons = {
|
||||||
|
'initialization': '🚀',
|
||||||
|
'retrieval': '🔍',
|
||||||
|
'selection': '🎯',
|
||||||
|
'micro-task': '⚡',
|
||||||
|
'completion': '✅'
|
||||||
|
};
|
||||||
|
|
||||||
|
const phaseNames = {
|
||||||
|
'initialization': 'Initialisierung',
|
||||||
|
'retrieval': 'Datensuche',
|
||||||
|
'selection': 'Tool-Auswahl',
|
||||||
|
'micro-task': 'Detail-Analyse',
|
||||||
|
'completion': 'Finalisierung'
|
||||||
|
};
|
||||||
|
|
||||||
|
const avgConfidence = entries.reduce((sum, entry) => sum + entry.confidence, 0) / entries.length;
|
||||||
|
const totalTime = entries.reduce((sum, entry) => sum + entry.processingTimeMs, 0);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="phase-group mb-4">
|
||||||
|
<div class="flex items-center gap-3 mb-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-lg">${phaseIcons[phase] || '📋'}</span>
|
||||||
|
<span class="font-medium text-sm">${phaseNames[phase] || phase}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 h-px bg-border"></div>
|
||||||
|
<div class="flex items-center gap-2 text-xs text-muted">
|
||||||
|
<div class="confidence-indicator w-12 h-2 rounded-full overflow-hidden" style="background-color: var(--color-bg-tertiary);">
|
||||||
|
<div class="h-full rounded-full transition-all" style="width: ${avgConfidence}%; background-color: ${avgConfidence >= 80 ? 'var(--color-accent)' : avgConfidence >= 60 ? 'var(--color-warning)' : 'var(--color-error)'};"></div>
|
||||||
|
</div>
|
||||||
|
<span>${Math.round(avgConfidence)}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-2 ml-6">
|
||||||
|
${entries.map(entry => this.renderSimplifiedEntry(entry)).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSimplifiedEntry(entry) {
|
||||||
|
const actionIcons = {
|
||||||
|
'pipeline-start': '▶️',
|
||||||
|
'embeddings-search': '🔍',
|
||||||
|
'ai-tool-selection': '🎯',
|
||||||
|
'ai-analysis': '🧠',
|
||||||
|
'phase-tool-selection': '⚙️',
|
||||||
|
'tool-evaluation': '📊',
|
||||||
|
'background-knowledge-selection': '📚',
|
||||||
|
'pipeline-end': '🏁'
|
||||||
|
};
|
||||||
|
|
||||||
|
const actionNames = {
|
||||||
|
'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',
|
||||||
|
'pipeline-end': 'Analyse abgeschlossen'
|
||||||
|
};
|
||||||
|
|
||||||
|
const confidenceColor = entry.confidence >= 80 ? 'var(--color-accent)' :
|
||||||
|
entry.confidence >= 60 ? 'var(--color-warning)' : 'var(--color-error)';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="flex items-center gap-3 py-2 px-3 rounded-lg hover:bg-secondary transition-colors">
|
||||||
|
<span class="text-sm">${actionIcons[entry.action] || '📋'}</span>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="text-sm font-medium">${actionNames[entry.action] || entry.action}</div>
|
||||||
|
${entry.output && typeof entry.output === 'object' && entry.output.selectedToolCount ?
|
||||||
|
`<div class="text-xs text-muted">${entry.output.selectedToolCount} Tools ausgewählt</div>` : ''}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 text-xs">
|
||||||
|
<div class="w-8 h-1.5 rounded-full overflow-hidden" style="background-color: var(--color-bg-tertiary);">
|
||||||
|
<div class="h-full rounded-full" style="width: ${entry.confidence}%; background-color: ${confidenceColor};"></div>
|
||||||
|
</div>
|
||||||
|
<span class="text-muted w-8 text-right">${entry.confidence}%</span>
|
||||||
|
<span class="text-muted w-12 text-right">${entry.processingTimeMs}ms</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTechnicalEntry(entry) {
|
||||||
|
const formattedTime = new Date(entry.timestamp).toLocaleTimeString('de-DE', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
});
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="border rounded p-2 mb-2" style="border-color: var(--color-border);">
|
||||||
|
<div class="flex justify-between items-center mb-1">
|
||||||
|
<span class="font-mono text-xs">${entry.phase}/${entry.action}</span>
|
||||||
|
<span class="text-xs text-muted">${formattedTime} • ${entry.processingTimeMs}ms</span>
|
||||||
|
</div>
|
||||||
|
${entry.input && Object.keys(entry.input).length > 0 ? `
|
||||||
|
<div class="text-xs mb-1">
|
||||||
|
<strong>Input:</strong> ${this.formatAuditData(entry.input)}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
${entry.output && Object.keys(entry.output).length > 0 ? `
|
||||||
|
<div class="text-xs">
|
||||||
|
<strong>Output:</strong> ${this.formatAuditData(entry.output)}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderAuditEntry(entry) {
|
||||||
|
const confidenceColor = entry.confidence >= 80 ? 'var(--color-accent)' :
|
||||||
|
entry.confidence >= 60 ? 'var(--color-warning)' : 'var(--color-error)';
|
||||||
|
|
||||||
|
const formattedTime = new Date(entry.timestamp).toLocaleTimeString('de-DE', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
});
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="border-l-2 pl-3 py-2 mb-2" style="border-left-color: ${confidenceColor};">
|
||||||
|
<div class="flex justify-between items-center mb-1">
|
||||||
|
<span class="text-xs font-medium">${entry.phase} → ${entry.action}</span>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="badge badge-mini" style="background-color: ${confidenceColor}; color: white;">
|
||||||
|
${entry.confidence}% confidence
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-muted">${entry.processingTimeMs}ms</span>
|
||||||
|
<span class="text-xs text-muted">${formattedTime}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-muted grid-cols-2 gap-2" style="display: grid;">
|
||||||
|
<div><strong>Input:</strong> ${this.formatAuditData(entry.input)}</div>
|
||||||
|
<div><strong>Output:</strong> ${this.formatAuditData(entry.output)}</div>
|
||||||
|
</div>
|
||||||
|
${entry.metadata && Object.keys(entry.metadata).length > 0 ? `
|
||||||
|
<div class="text-xs text-muted mt-1 pt-1 border-t border-dashed">
|
||||||
|
<strong>Metadata:</strong> ${this.formatAuditData(entry.metadata)}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
formatAuditData(data) {
|
||||||
|
if (data === null || data === undefined) return 'null';
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
return data.length > 100 ? data.slice(0, 100) + '...' : data;
|
||||||
|
}
|
||||||
|
if (typeof data === 'number') return data.toString();
|
||||||
|
if (typeof data === 'boolean') return data.toString();
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
if (data.length === 0) return '[]';
|
||||||
|
if (data.length <= 3) return JSON.stringify(data);
|
||||||
|
return `[${data.slice(0, 3).map(i => typeof i === 'string' ? i : JSON.stringify(i)).join(', ')}, ...+${data.length - 3}]`;
|
||||||
|
}
|
||||||
|
if (typeof data === 'object') {
|
||||||
|
const keys = Object.keys(data);
|
||||||
|
if (keys.length === 0) return '{}';
|
||||||
|
if (keys.length <= 3) {
|
||||||
|
return '{' + keys.map(k => `${k}: ${typeof data[k] === 'string' ? data[k].slice(0, 20) + (data[k].length > 20 ? '...' : '') : JSON.stringify(data[k])}`).join(', ') + '}';
|
||||||
|
}
|
||||||
|
return `{${keys.slice(0, 3).join(', ')}, ...+${keys.length - 3} keys}`;
|
||||||
|
}
|
||||||
|
return String(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderWorkflowTool(tool) {
|
||||||
|
const hasValidProjectUrl = isToolHosted(tool);
|
||||||
|
const priorityColors = {
|
||||||
|
high: 'var(--color-error)',
|
||||||
|
medium: 'var(--color-warning)',
|
||||||
|
low: 'var(--color-accent)'
|
||||||
|
};
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="tool-recommendation ${this.getToolClass(tool, 'recommendation')}" onclick="window.showToolDetails('${tool.name}')" style="position: relative;">
|
||||||
|
<div class="tool-rec-header">
|
||||||
|
<h4 class="tool-rec-name">
|
||||||
|
${tool.icon ? `<span style="margin-right: 0.5rem;">${tool.icon}</span>` : ''}
|
||||||
|
${tool.name}
|
||||||
|
</h4>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="tool-rec-priority ${tool.recommendation ? tool.recommendation.priority : tool.priority}"
|
||||||
|
style="background-color: ${priorityColors[tool.recommendation ? tool.recommendation.priority : tool.priority]}; color: white; padding: 0.25rem 0.5rem; border-radius: 1rem; font-size: 0.75rem; position: relative;">
|
||||||
|
${tool.recommendation ? tool.recommendation.priority : tool.priority}
|
||||||
|
${tool.confidence ? this.renderConfidenceTooltip(tool.confidence, 'priority') : ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tool-rec-justification" style="background-color: var(--color-bg-tertiary); padding: 0.75rem; border-radius: 0.375rem; border-left: 3px solid var(--color-primary); margin: 0.75rem 0; font-style: italic; white-space: pre-wrap; word-wrap: break-word;">
|
||||||
|
"${this.sanitizeText(tool.justification || (tool.recommendation && tool.recommendation.justification) || `Empfohlen für ${tool.phase}`)}"
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="font-size: 0.75rem; color: var(--color-text-secondary);">
|
||||||
|
${this.renderToolBadges(tool)}
|
||||||
|
<div style="margin-top: 0.5rem;">
|
||||||
|
${tool.type === 'method' ? 'Methode' : tool.platforms.join(', ') + ' • ' + tool.license}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderDetailedTool(tool, recommendation, rank) {
|
||||||
|
const rankColors = { 1: 'var(--color-accent)', 2: 'var(--color-primary)', 3: 'var(--color-warning)' };
|
||||||
|
const suitabilityColors = { high: 'var(--color-accent)', medium: 'var(--color-warning)', low: 'var(--color-text-secondary)' };
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="card ${this.getToolClass(tool, 'card')}" style="cursor: pointer; position: relative;" onclick="window.showToolDetails('${tool.name}')">
|
||||||
|
<div style="position: absolute; top: -8px; right: -8px; width: 32px; height: 32px; background-color: ${rankColors[rank]}; color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 1.125rem;">
|
||||||
|
${rank}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 1rem;">
|
||||||
|
<h3 style="margin: 0 0 0.5rem 0;">${tool.name}</h3>
|
||||||
|
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; margin-bottom: 0.75rem;">
|
||||||
|
<span class="badge" style="background-color: ${suitabilityColors[recommendation.suitability_score]}; color: white; position: relative;">
|
||||||
|
${this.getSuitabilityText(recommendation.suitability_score, recommendation.confidence)}
|
||||||
|
</span>
|
||||||
|
${this.renderToolBadges(tool)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 1.5rem;">
|
||||||
|
<h4 style="margin: 0.8rem 0 0.75rem 0; color: var(--color-accent);">Warum diese Methode?</h4>
|
||||||
|
<div style="margin: 0; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word;">${this.sanitizeText(recommendation.detailed_explanation)}</div>
|
||||||
|
|
||||||
|
${recommendation.implementation_approach ? `
|
||||||
|
<h4 style="margin: 0.8rem 0 0.75rem 0; color: var(--color-primary);">Anwendungsansatz</h4>
|
||||||
|
<div style="margin: 0; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word;">${this.sanitizeText(recommendation.implementation_approach)}</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${this.renderProsAndCons(recommendation.pros, recommendation.cons)}
|
||||||
|
${this.renderToolMetadata(tool)}
|
||||||
|
${recommendation.alternatives ? this.renderAlternatives(recommendation.alternatives) : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
renderHeader(title, query) {
|
renderHeader(title, query) {
|
||||||
return `
|
return `
|
||||||
<div style="text-align: center; margin-bottom: 2rem; padding: 1.5rem; background: linear-gradient(135deg, var(--color-primary) 0%, #525252 100%); color: white; border-radius: 0.75rem;">
|
<div style="text-align: center; margin-bottom: 2rem; padding: 1.5rem; background: linear-gradient(135deg, var(--color-primary) 0%, #525252 100%); color: white; border-radius: 0.75rem;">
|
||||||
@ -813,6 +1249,8 @@ class AIQueryInterface {
|
|||||||
const phaseTools = toolsByPhase[phase];
|
const phaseTools = toolsByPhase[phase];
|
||||||
if (phaseTools.length === 0) return '';
|
if (phaseTools.length === 0) return '';
|
||||||
|
|
||||||
|
console.log(`[AI DEBUG] Phase ${phase} tools:`, phaseTools.map(t => ({name: t.name, hasConfidence: !!t.confidence})));
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="workflow-phase">
|
<div class="workflow-phase">
|
||||||
<div class="phase-header">
|
<div class="phase-header">
|
||||||
@ -820,7 +1258,10 @@ class AIQueryInterface {
|
|||||||
<div class="phase-info">
|
<div class="phase-info">
|
||||||
<h3 class="phase-title">${phaseNames[phase]}</h3>
|
<h3 class="phase-title">${phaseNames[phase]}</h3>
|
||||||
<div class="phase-tools">
|
<div class="phase-tools">
|
||||||
${phaseTools.map(tool => this.renderWorkflowTool(tool)).join('')}
|
${phaseTools.map(tool => {
|
||||||
|
console.log(`[AI DEBUG] Rendering tool ${tool.name} with confidence:`, tool.confidence);
|
||||||
|
return this.renderWorkflowTool(tool);
|
||||||
|
}).join('')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -831,28 +1272,38 @@ class AIQueryInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderWorkflowTool(tool) {
|
renderWorkflowTool(tool) {
|
||||||
const hasValidProjectUrl = this.isToolHosted(tool);
|
console.log(`[AI DEBUG] renderWorkflowTool called for ${tool.name}, confidence:`, tool.confidence);
|
||||||
|
|
||||||
|
const hasValidProjectUrl = isToolHosted(tool);
|
||||||
const priorityColors = {
|
const priorityColors = {
|
||||||
high: 'var(--color-error)',
|
high: 'var(--color-error)',
|
||||||
medium: 'var(--color-warning)',
|
medium: 'var(--color-warning)',
|
||||||
low: 'var(--color-accent)'
|
low: 'var(--color-accent)'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const priority = tool.recommendation ? tool.recommendation.priority : tool.priority;
|
||||||
|
const confidenceTooltip = tool.confidence ? this.renderConfidenceTooltip(tool.confidence) : '';
|
||||||
|
|
||||||
|
console.log(`[AI DEBUG] Priority: ${priority}, Confidence tooltip:`, confidenceTooltip ? 'generated' : 'empty');
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="tool-recommendation ${this.getToolClass(tool, 'recommendation')}" onclick="window.showToolDetails('${tool.name}')">
|
<div class="tool-recommendation ${this.getToolClass(tool, 'recommendation')}" onclick="window.showToolDetails('${tool.name}')" style="position: relative;">
|
||||||
<div class="tool-rec-header">
|
<div class="tool-rec-header">
|
||||||
<h4 class="tool-rec-name">
|
<h4 class="tool-rec-name">
|
||||||
${tool.icon ? `<span style="margin-right: 0.5rem;">${tool.icon}</span>` : ''}
|
${tool.icon ? `<span style="margin-right: 0.5rem;">${tool.icon}</span>` : ''}
|
||||||
${tool.name}
|
${tool.name}
|
||||||
</h4>
|
</h4>
|
||||||
<span class="tool-rec-priority ${tool.recommendation.priority}"
|
<div class="flex items-center gap-2">
|
||||||
style="background-color: ${priorityColors[tool.recommendation.priority]}; color: white; padding: 0.25rem 0.5rem; border-radius: 1rem; font-size: 0.75rem;">
|
<span class="tool-rec-priority ${priority}"
|
||||||
${tool.recommendation.priority}
|
style="background-color: ${priorityColors[priority]}; color: white; padding: 0.25rem 0.5rem; border-radius: 1rem; font-size: 0.75rem; position: relative; display: flex; align-items: center; gap: 0.25rem;">
|
||||||
</span>
|
${priority}
|
||||||
|
${confidenceTooltip}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tool-rec-justification" style="background-color: var(--color-bg-tertiary); padding: 0.75rem; border-radius: 0.375rem; border-left: 3px solid var(--color-primary); margin: 0.75rem 0; font-style: italic; white-space: pre-wrap; word-wrap: break-word;">
|
<div class="tool-rec-justification" style="background-color: var(--color-bg-tertiary); padding: 0.75rem; border-radius: 0.375rem; border-left: 3px solid var(--color-primary); margin: 0.75rem 0; font-style: italic; white-space: pre-wrap; word-wrap: break-word;">
|
||||||
"${this.sanitizeText(tool.recommendation.justification)}"
|
"${this.sanitizeText(tool.justification || (tool.recommendation && tool.recommendation.justification) || `Empfohlen für ${tool.phase}`)}"
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="font-size: 0.75rem; color: var(--color-text-secondary);">
|
<div style="font-size: 0.75rem; color: var(--color-text-secondary);">
|
||||||
@ -874,16 +1325,23 @@ class AIQueryInterface {
|
|||||||
const fullTool = tools.find(t => t.name === toolRec.name);
|
const fullTool = tools.find(t => t.name === toolRec.name);
|
||||||
if (!fullTool) return '';
|
if (!fullTool) return '';
|
||||||
|
|
||||||
return this.renderDetailedTool(fullTool, toolRec, index + 1);
|
return this.renderDetailedTool(fullTool, {
|
||||||
|
...toolRec,
|
||||||
|
confidence: toolRec.confidence
|
||||||
|
}, index + 1);
|
||||||
}).join('')}
|
}).join('')}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderDetailedTool(tool, recommendation, rank) {
|
renderDetailedTool(tool, recommendation, rank) {
|
||||||
|
console.log(`[AI DEBUG] renderDetailedTool called for ${tool.name}, recommendation confidence:`, recommendation.confidence);
|
||||||
|
|
||||||
const rankColors = { 1: 'var(--color-accent)', 2: 'var(--color-primary)', 3: 'var(--color-warning)' };
|
const rankColors = { 1: 'var(--color-accent)', 2: 'var(--color-primary)', 3: 'var(--color-warning)' };
|
||||||
const suitabilityColors = { high: 'var(--color-accent)', medium: 'var(--color-warning)', low: 'var(--color-text-secondary)' };
|
const suitabilityColors = { high: 'var(--color-accent)', medium: 'var(--color-warning)', low: 'var(--color-text-secondary)' };
|
||||||
|
|
||||||
|
const confidenceTooltip = recommendation.confidence ? this.renderConfidenceTooltip(recommendation.confidence) : '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="card ${this.getToolClass(tool, 'card')}" style="cursor: pointer; position: relative;" onclick="window.showToolDetails('${tool.name}')">
|
<div class="card ${this.getToolClass(tool, 'card')}" style="cursor: pointer; position: relative;" onclick="window.showToolDetails('${tool.name}')">
|
||||||
<div style="position: absolute; top: -8px; right: -8px; width: 32px; height: 32px; background-color: ${rankColors[rank]}; color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 1.125rem;">
|
<div style="position: absolute; top: -8px; right: -8px; width: 32px; height: 32px; background-color: ${rankColors[rank]}; color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 1.125rem;">
|
||||||
@ -892,9 +1350,10 @@ class AIQueryInterface {
|
|||||||
|
|
||||||
<div style="margin-bottom: 1rem;">
|
<div style="margin-bottom: 1rem;">
|
||||||
<h3 style="margin: 0 0 0.5rem 0;">${tool.name}</h3>
|
<h3 style="margin: 0 0 0.5rem 0;">${tool.name}</h3>
|
||||||
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem;">
|
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; margin-bottom: 0.75rem;">
|
||||||
<span class="badge" style="background-color: ${suitabilityColors[recommendation.suitability_score]}; color: white;">
|
<span class="badge" style="background-color: ${suitabilityColors[recommendation.suitability_score]}; color: white; position: relative; display: flex; align-items: center; gap: 0.25rem;">
|
||||||
${this.getSuitabilityText(recommendation.suitability_score)}
|
${this.getSuitabilityText(recommendation.suitability_score)}
|
||||||
|
${confidenceTooltip}
|
||||||
</span>
|
</span>
|
||||||
${this.renderToolBadges(tool)}
|
${this.renderToolBadges(tool)}
|
||||||
</div>
|
</div>
|
||||||
@ -988,7 +1447,7 @@ class AIQueryInterface {
|
|||||||
|
|
||||||
renderToolBadges(tool) {
|
renderToolBadges(tool) {
|
||||||
const isMethod = tool.type === 'method';
|
const isMethod = tool.type === 'method';
|
||||||
const hasValidProjectUrl = this.isToolHosted(tool);
|
const hasValidProjectUrl = isToolHosted(tool);
|
||||||
|
|
||||||
let badges = '';
|
let badges = '';
|
||||||
if (isMethod) {
|
if (isMethod) {
|
||||||
@ -1032,16 +1491,9 @@ class AIQueryInterface {
|
|||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
isToolHosted(tool) {
|
|
||||||
return tool.projectUrl !== undefined &&
|
|
||||||
tool.projectUrl !== null &&
|
|
||||||
tool.projectUrl !== "" &&
|
|
||||||
tool.projectUrl.trim() !== "";
|
|
||||||
}
|
|
||||||
|
|
||||||
getToolClass(tool, context = 'card') {
|
getToolClass(tool, context = 'card') {
|
||||||
const isMethod = tool.type === 'method';
|
const isMethod = tool.type === 'method';
|
||||||
const hasValidProjectUrl = this.isToolHosted(tool);
|
const hasValidProjectUrl = isToolHosted(tool);
|
||||||
|
|
||||||
if (context === 'recommendation') {
|
if (context === 'recommendation') {
|
||||||
if (isMethod) return 'method';
|
if (isMethod) return 'method';
|
||||||
@ -1056,13 +1508,18 @@ class AIQueryInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getSuitabilityText(score) {
|
getSuitabilityText(score, confidence = null) {
|
||||||
const texts = {
|
const texts = {
|
||||||
high: 'GUT GEEIGNET',
|
high: 'GUT GEEIGNET',
|
||||||
medium: 'GEEIGNET',
|
medium: 'GEEIGNET',
|
||||||
low: 'VIELLEICHT GEEIGNET'
|
low: 'VIELLEICHT GEEIGNET'
|
||||||
};
|
};
|
||||||
return texts[score] || 'GEEIGNET';
|
const baseText = texts[score] || 'GEEIGNET';
|
||||||
|
|
||||||
|
if (confidence) {
|
||||||
|
return `${baseText} ${this.renderConfidenceTooltip(confidence, 'suitability')}`;
|
||||||
|
}
|
||||||
|
return baseText;
|
||||||
}
|
}
|
||||||
|
|
||||||
escapeHtml(text) {
|
escapeHtml(text) {
|
||||||
@ -1134,5 +1591,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
aiInterface.hideError();
|
aiInterface.hideError();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
window.isToolHosted = window.isToolHosted || isToolHosted
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
243
src/config/prompts.ts
Normal file
243
src/config/prompts.ts
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
// src/config/prompts.ts - Centralized German prompts for AI pipeline
|
||||||
|
|
||||||
|
export const AI_PROMPTS = {
|
||||||
|
|
||||||
|
toolSelection: (mode: string, userQuery: string, selectionMethod: string, maxSelectedItems: number) => {
|
||||||
|
const modeInstruction = mode === 'workflow'
|
||||||
|
? 'Der Benutzer möchte einen UMFASSENDEN WORKFLOW mit mehreren Tools/Methoden über verschiedene Phasen. Wählen Sie 15-25 Tools aus, die den vollständigen Untersuchungslebenszyklus abdecken.'
|
||||||
|
: 'Der Benutzer möchte SPEZIFISCHE TOOLS/METHODEN, die ihr konkretes Problem direkt lösen. Wählen Sie 3-8 Tools aus, die am relevantesten und effektivsten sind.';
|
||||||
|
|
||||||
|
return `Sie sind ein DFIR-Experte mit Zugang zur kompletten forensischen Tool-Datenbank. Sie müssen die relevantesten Tools und Konzepte für diese spezifische Anfrage auswählen.
|
||||||
|
|
||||||
|
AUSWAHLMETHODE: ${selectionMethod}
|
||||||
|
${selectionMethod === 'embeddings_candidates' ?
|
||||||
|
'Diese Tools wurden durch Vektor-Ähnlichkeit vorgefiltert, sie sind bereits relevant. Ihre Aufgabe ist es, die BESTEN aus diesem relevanten Set auszuwählen.' :
|
||||||
|
'Sie haben Zugang zur vollständigen Tool-Datenbank. Wählen Sie die relevantesten Tools für die Anfrage aus.'}
|
||||||
|
|
||||||
|
${modeInstruction}
|
||||||
|
|
||||||
|
BENUTZER-ANFRAGE: "${userQuery}"
|
||||||
|
|
||||||
|
KRITISCHE AUSWAHLPRINZIPIEN:
|
||||||
|
1. **KONTEXT ÜBER POPULARITÄT**: Verwenden Sie nicht automatisch "berühmte" Tools wie Volatility, Wireshark oder Autopsy nur weil sie bekannt sind. Wählen Sie basierend auf den SPEZIFISCHEN Szenario-Anforderungen.
|
||||||
|
|
||||||
|
2. **METHODOLOGIE vs SOFTWARE**:
|
||||||
|
- Für SCHNELLE/DRINGENDE Szenarien → Priorisieren Sie METHODEN und schnelle Antwort-Ansätze
|
||||||
|
- Für ZEITKRITISCHE Vorfälle → Wählen Sie Triage-Methoden über tiefe Analyse-Tools
|
||||||
|
- Für UMFASSENDE Analysen → Dann betrachten Sie detaillierte Software-Tools
|
||||||
|
- METHODEN (Typ: "method") sind oft besser als SOFTWARE für prozedurale Anleitung
|
||||||
|
|
||||||
|
3. **SZENARIO-SPEZIFISCHE LOGIK**:
|
||||||
|
- "Schnell/Quick/Dringend/Triage" Szenarien → Rapid Incident Response und Triage METHODE > Volatility
|
||||||
|
- "Industrial/SCADA/ICS" Szenarien → Spezialisierte ICS-Tools > generische Netzwerk-Tools
|
||||||
|
- "Mobile/Android/iOS" Szenarien → Mobile-spezifische Tools > Desktop-Forensik-Tools
|
||||||
|
- "Speicher-Analyse dringend benötigt" → Schnelle Speicher-Tools/Methoden > umfassende Volatility-Analyse
|
||||||
|
|
||||||
|
ANALYSE-ANWEISUNGEN:
|
||||||
|
1. Lesen Sie die VOLLSTÄNDIGE Beschreibung jedes Tools/Konzepts
|
||||||
|
2. Berücksichtigen Sie ALLE Tags, Plattformen, verwandte Tools und Metadaten
|
||||||
|
3. **PASSENDE DRINGLICHKEIT**: Schnelle Szenarien brauchen schnelle Methoden, nicht tiefe Analyse-Tools
|
||||||
|
4. **PASSENDE SPEZIFITÄT**: Spezialisierte Szenarien brauchen spezialisierte Tools, nicht generische
|
||||||
|
5. **BERÜCKSICHTIGEN SIE DEN TYP**: Methoden bieten prozedurale Anleitung, Software bietet technische Fähigkeiten
|
||||||
|
|
||||||
|
Wählen Sie die relevantesten Elemente aus (max ${maxSelectedItems} insgesamt).
|
||||||
|
|
||||||
|
Antworten Sie NUR mit diesem JSON-Format:
|
||||||
|
{
|
||||||
|
"selectedTools": ["Tool Name 1", "Tool Name 2", ...],
|
||||||
|
"selectedConcepts": ["Konzept Name 1", "Konzept Name 2", ...],
|
||||||
|
"reasoning": "Detaillierte Erklärung, warum diese spezifischen Tools für diese Anfrage ausgewählt wurden, und warum bestimmte populäre Tools NICHT ausgewählt wurden, falls sie für den Szenario-Kontext ungeeignet waren"
|
||||||
|
}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
scenarioAnalysis: (isWorkflow: boolean, userQuery: string) => {
|
||||||
|
const analysisType = isWorkflow ? 'forensische Szenario' : 'technische Problem';
|
||||||
|
const considerations = isWorkflow ?
|
||||||
|
`- Angriffsvektoren und Bedrohungsmodellierung nach MITRE ATT&CK
|
||||||
|
- Betroffene Systeme und kritische Infrastrukturen
|
||||||
|
- Zeitkritische Faktoren und Beweiserhaltung
|
||||||
|
- Forensische Artefakte und Datenquellen` :
|
||||||
|
`- Spezifische forensische Herausforderungen
|
||||||
|
- Verfügbare Datenquellen und deren Integrität
|
||||||
|
- Methodische Anforderungen für rechtssichere Analyse`;
|
||||||
|
|
||||||
|
return `Sie sind ein erfahrener DFIR-Experte. Analysieren Sie das folgende ${analysisType}.
|
||||||
|
|
||||||
|
${isWorkflow ? 'FORENSISCHES SZENARIO' : 'TECHNISCHES PROBLEM'}: "${userQuery}"
|
||||||
|
|
||||||
|
Führen Sie eine systematische ${isWorkflow ? 'Szenario-Analyse' : 'Problem-Analyse'} durch und berücksichtigen Sie dabei:
|
||||||
|
|
||||||
|
${considerations}
|
||||||
|
|
||||||
|
WICHTIG: Antworten Sie NUR in fließendem deutschen Text ohne Listen, Aufzählungen oder Markdown-Formatierung. Maximum 150 Wörter.`;
|
||||||
|
},
|
||||||
|
|
||||||
|
investigationApproach: (isWorkflow: boolean, userQuery: string) => {
|
||||||
|
const approachType = isWorkflow ? 'Untersuchungsansatz' : 'Lösungsansatz';
|
||||||
|
const considerations = isWorkflow ?
|
||||||
|
`- Triage-Prioritäten nach forensischer Dringlichkeit
|
||||||
|
- Phasenabfolge nach NIST-Methodik
|
||||||
|
- Kontaminationsvermeidung und forensische Isolierung` :
|
||||||
|
`- Methodik-Auswahl nach wissenschaftlichen Kriterien
|
||||||
|
- Validierung und Verifizierung der gewählten Ansätze
|
||||||
|
- Integration in bestehende forensische Workflows`;
|
||||||
|
|
||||||
|
return `Basierend auf der Analyse entwickeln Sie einen fundierten ${approachType} nach NIST SP 800-86 Methodik.
|
||||||
|
|
||||||
|
${isWorkflow ? 'SZENARIO' : 'PROBLEM'}: "${userQuery}"
|
||||||
|
|
||||||
|
Entwickeln Sie einen systematischen ${approachType} unter Berücksichtigung von:
|
||||||
|
|
||||||
|
${considerations}
|
||||||
|
|
||||||
|
WICHTIG: Antworten Sie NUR in fließendem deutschen Text ohne Listen oder Markdown. Maximum 150 Wörter.`;
|
||||||
|
},
|
||||||
|
|
||||||
|
criticalConsiderations: (isWorkflow: boolean, userQuery: string) => {
|
||||||
|
const considerationType = isWorkflow ? 'kritische forensische Überlegungen' : 'wichtige methodische Voraussetzungen';
|
||||||
|
const aspects = isWorkflow ?
|
||||||
|
`- Time-sensitive evidence preservation
|
||||||
|
- Chain of custody requirements und rechtliche Verwertbarkeit
|
||||||
|
- Incident containment vs. evidence preservation Dilemma
|
||||||
|
- Privacy- und Compliance-Anforderungen` :
|
||||||
|
`- Tool-Validierung und Nachvollziehbarkeit
|
||||||
|
- False positive/negative Risiken bei der gewählten Methodik
|
||||||
|
- Qualifikationsanforderungen für die Durchführung
|
||||||
|
- Dokumentations- und Reporting-Standards`;
|
||||||
|
|
||||||
|
return `Identifizieren Sie ${considerationType} für diesen Fall.
|
||||||
|
|
||||||
|
${isWorkflow ? 'SZENARIO' : 'PROBLEM'}: "${userQuery}"
|
||||||
|
|
||||||
|
Berücksichtigen Sie folgende forensische Aspekte:
|
||||||
|
|
||||||
|
${aspects}
|
||||||
|
|
||||||
|
WICHTIG: Antworten Sie NUR in fließendem deutschen Text ohne Listen oder Markdown. Maximum 120 Wörter.`;
|
||||||
|
},
|
||||||
|
|
||||||
|
phaseToolSelection: (userQuery: string, phase: any, phaseTools: any[]) => {
|
||||||
|
return `Wählen Sie 2-3 Methoden/Tools für die Phase "${phase.name}" und bewerten Sie deren Aufgaben-Eignung VERGLEICHEND.
|
||||||
|
|
||||||
|
SZENARIO: "${userQuery}"
|
||||||
|
SPEZIFISCHE PHASE: ${phase.name} - ${phase.description || 'Forensische Untersuchungsphase'}
|
||||||
|
|
||||||
|
VERFÜGBARE TOOLS FÜR ${phase.name.toUpperCase()}:
|
||||||
|
${phaseTools.map((tool: any, index: number) => `${index + 1}. ${tool.name}: ${tool.description.slice(0, 150)}...
|
||||||
|
- Plattformen: ${tool.platforms?.join(', ') || 'N/A'}
|
||||||
|
- Skill Level: ${tool.skillLevel}
|
||||||
|
- Tags: ${tool.tags?.join(', ') || 'N/A'}`).join('\n\n')}
|
||||||
|
|
||||||
|
Bewerten Sie ALLE Tools vergleichend für diese spezifische Aufgabe UND Phase. Wählen Sie die 2-3 besten aus.
|
||||||
|
|
||||||
|
BEWERTUNGSKRITERIEN:
|
||||||
|
- Wie gut löst das Tool das forensische Problem im SZENARIO-Kontext?
|
||||||
|
- Wie gut passt es zur spezifischen PHASE "${phase.name}"?
|
||||||
|
- Wie vergleicht es sich mit den anderen verfügbaren Tools für diese Phase?
|
||||||
|
|
||||||
|
Antworten Sie AUSSCHLIESSLICH mit diesem JSON-Format:
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"toolName": "Exakter Tool-Name",
|
||||||
|
"taskRelevance": 85,
|
||||||
|
"justification": "Vergleichende Begründung warum dieses Tool für diese Phase und Aufgabe besser/schlechter als die anderen geeignet ist",
|
||||||
|
"limitations": ["Spezifische Einschränkung 1", "Einschränkung 2"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
WICHTIG:
|
||||||
|
- taskRelevance: 0-100 Score basierend auf Szenario-Eignung UND Phasen-Passung im VERGLEICH zu anderen Tools
|
||||||
|
- Nur die 2-3 BESTEN Tools auswählen und bewerten
|
||||||
|
- justification soll VERGLEICHEND sein ("besser als X weil...", "für diese Phase ideal weil...")`;
|
||||||
|
},
|
||||||
|
|
||||||
|
toolEvaluation: (userQuery: string, tool: any, rank: number, taskRelevance: number) => {
|
||||||
|
return `Sie sind ein DFIR-Experte. Erklären Sie DETAILLIERT die Anwendung dieses bereits bewerteten Tools.
|
||||||
|
|
||||||
|
PROBLEM: "${userQuery}"
|
||||||
|
TOOL: ${tool.name} (bereits bewertet mit ${taskRelevance}% Aufgaben-Eignung)
|
||||||
|
BESCHREIBUNG: ${tool.description}
|
||||||
|
|
||||||
|
Das Tool wurde bereits als Rang ${rank} für diese Aufgabe bewertet. Erklären Sie nun:
|
||||||
|
|
||||||
|
Antworten Sie AUSSCHLIESSLICH mit diesem JSON-Format:
|
||||||
|
{
|
||||||
|
"detailed_explanation": "Detaillierte Erklärung warum und wie dieses Tool für diese spezifische Aufgabe eingesetzt wird",
|
||||||
|
"implementation_approach": "Konkrete Schritt-für-Schritt Anleitung zur korrekten Anwendung",
|
||||||
|
"pros": ["Spezifischer Vorteil 1", "Spezifischer Vorteil 2"],
|
||||||
|
"limitations": ["Spezifische Einschränkung 1", "Spezifische Einschränkung 2"],
|
||||||
|
"alternatives": "Alternative Ansätze oder Tools falls dieses nicht verfügbar ist"
|
||||||
|
}
|
||||||
|
|
||||||
|
WICHTIG:
|
||||||
|
- Keine erneute Bewertung - nur detaillierte Erklärung der bereits bewerteten Eignung
|
||||||
|
- "limitations" soll spezifische technische/methodische Einschränkungen des Tools auflisten
|
||||||
|
- "pros" soll die Stärken für diese spezifische Aufgabe hervorheben`;
|
||||||
|
},
|
||||||
|
|
||||||
|
backgroundKnowledgeSelection: (userQuery: string, mode: string, selectedToolNames: string[], availableConcepts: any[]) => {
|
||||||
|
return `Wählen Sie relevante forensische Konzepte für das Verständnis der empfohlenen Methodik.
|
||||||
|
|
||||||
|
${mode === 'workflow' ? 'SZENARIO' : 'PROBLEM'}: "${userQuery}"
|
||||||
|
EMPFOHLENE TOOLS: ${selectedToolNames.join(', ')}
|
||||||
|
|
||||||
|
VERFÜGBARE KONZEPTE:
|
||||||
|
${availableConcepts.slice(0, 15).map((concept: any) => `- ${concept.name}: ${concept.description.slice(0, 80)}...`).join('\n')}
|
||||||
|
|
||||||
|
Wählen Sie 2-4 Konzepte aus, die für das Verständnis der forensischen Methodik essentiell sind.
|
||||||
|
|
||||||
|
Antworten Sie AUSSCHLIESSLICH mit diesem JSON-Format:
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"conceptName": "Exakter Konzept-Name",
|
||||||
|
"relevance": "Forensische Relevanz: Warum dieses Konzept für das Verständnis der Methodik kritisch ist"
|
||||||
|
}
|
||||||
|
]`;
|
||||||
|
},
|
||||||
|
|
||||||
|
finalRecommendations: (isWorkflow: boolean, userQuery: string, selectedToolNames: string[]) => {
|
||||||
|
const prompt = isWorkflow ?
|
||||||
|
`Erstellen Sie eine Workflow-Empfehlung basierend auf DFIR-Prinzipien.
|
||||||
|
|
||||||
|
SZENARIO: "${userQuery}"
|
||||||
|
AUSGEWÄHLTE TOOLS: ${selectedToolNames.join(', ') || 'Keine Tools ausgewählt'}
|
||||||
|
|
||||||
|
Erstellen Sie konkrete methodische Workflow-Schritte für dieses spezifische Szenario unter Berücksichtigung forensischer Best Practices, Objektivität und rechtlicher Verwertbarkeit.
|
||||||
|
|
||||||
|
WICHTIG: Antworten Sie NUR in fließendem deutschen Text ohne Listen oder Markdown. Maximum 120 Wörter.` :
|
||||||
|
|
||||||
|
`Erstellen Sie wichtige methodische Überlegungen für die korrekte Methoden-/Tool-Anwendung.
|
||||||
|
|
||||||
|
PROBLEM: "${userQuery}"
|
||||||
|
EMPFOHLENE TOOLS: ${selectedToolNames.join(', ') || 'Keine Methoden/Tools ausgewählt'}
|
||||||
|
|
||||||
|
Geben Sie kritische methodische Überlegungen, Validierungsanforderungen und Qualitätssicherungsmaßnahmen für die korrekte Anwendung der empfohlenen Methoden/Tools.
|
||||||
|
|
||||||
|
WICHTIG: Antworten Sie NUR in fließendem deutschen Text ohne Listen oder Markdown. Maximum 100 Wörter.`;
|
||||||
|
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function getPrompt(key: 'toolSelection', mode: string, userQuery: string, selectionMethod: string, maxSelectedItems: number): string;
|
||||||
|
export function getPrompt(key: 'scenarioAnalysis', isWorkflow: boolean, userQuery: string): string;
|
||||||
|
export function getPrompt(key: 'investigationApproach', isWorkflow: boolean, userQuery: string): string;
|
||||||
|
export function getPrompt(key: 'criticalConsiderations', isWorkflow: boolean, userQuery: string): string;
|
||||||
|
export function getPrompt(key: 'phaseToolSelection', userQuery: string, phase: any, phaseTools: any[]): string;
|
||||||
|
export function getPrompt(key: 'toolEvaluation', userQuery: string, tool: any, rank: number, taskRelevance: number): string;
|
||||||
|
export function getPrompt(key: 'backgroundKnowledgeSelection', userQuery: string, mode: string, selectedToolNames: string[], availableConcepts: any[]): string;
|
||||||
|
export function getPrompt(key: 'finalRecommendations', isWorkflow: boolean, userQuery: string, selectedToolNames: string[]): string;
|
||||||
|
export function getPrompt(promptKey: keyof typeof AI_PROMPTS, ...args: any[]): string {
|
||||||
|
try {
|
||||||
|
const promptFunction = AI_PROMPTS[promptKey];
|
||||||
|
if (typeof promptFunction === 'function') {
|
||||||
|
return (promptFunction as (...args: any[]) => string)(...args);
|
||||||
|
} else {
|
||||||
|
console.error(`[PROMPTS] Invalid prompt key: ${promptKey}`);
|
||||||
|
return 'Error: Invalid prompt configuration';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[PROMPTS] Error generating prompt ${promptKey}:`, error);
|
||||||
|
return 'Error: Failed to generate prompt';
|
||||||
|
}
|
||||||
|
}
|
@ -113,64 +113,6 @@ tools:
|
|||||||
accessType: download
|
accessType: download
|
||||||
license: VSL
|
license: VSL
|
||||||
knowledgebase: false
|
knowledgebase: false
|
||||||
- name: TheHive 5
|
|
||||||
icon: 🐝
|
|
||||||
type: software
|
|
||||||
description: >-
|
|
||||||
Die zentrale Incident-Response-Plattform orchestriert komplexe
|
|
||||||
Sicherheitsvorfälle vom ersten Alert bis zum Abschlussbericht. Jeder Case
|
|
||||||
wird strukturiert durch Observables (IOCs), Tasks und Zeitleisten
|
|
||||||
abgebildet. Die Cortex-Integration automatisiert Analysen durch Dutzende
|
|
||||||
Analyzer - von VirusTotal-Checks bis Sandbox-Detonation.
|
|
||||||
MISP-Synchronisation reichert Cases mit Threat-Intelligence an. Das
|
|
||||||
ausgeklügelte Rollen- und Rechtesystem ermöglicht sichere Zusammenarbeit
|
|
||||||
zwischen SOC-Analysten, Forensikern und Management. Templates
|
|
||||||
standardisieren Response-Prozesse nach Incident-Typ. Die RESTful API
|
|
||||||
integriert nahtlos mit SIEM, SOAR und Ticketing-Systemen. Metrics und
|
|
||||||
KPIs messen die Team-Performance. Die Community Edition bleibt kostenlos
|
|
||||||
für kleinere Teams, während Gold/Platinum-Lizenzen Enterprise-Features
|
|
||||||
bieten.
|
|
||||||
domains:
|
|
||||||
- incident-response
|
|
||||||
- static-investigations
|
|
||||||
- malware-analysis
|
|
||||||
- network-forensics
|
|
||||||
- fraud-investigation
|
|
||||||
phases:
|
|
||||||
- data-collection
|
|
||||||
- examination
|
|
||||||
- analysis
|
|
||||||
- reporting
|
|
||||||
platforms:
|
|
||||||
- Web
|
|
||||||
related_software:
|
|
||||||
- MISP
|
|
||||||
- Cortex
|
|
||||||
- Elasticsearch
|
|
||||||
domain-agnostic-software:
|
|
||||||
- collaboration-general
|
|
||||||
skillLevel: intermediate
|
|
||||||
accessType: server-based
|
|
||||||
url: https://strangebee.com/thehive/
|
|
||||||
projectUrl: ''
|
|
||||||
license: Community Edition (Discontinued) / Commercial
|
|
||||||
knowledgebase: false
|
|
||||||
statusUrl: https://uptime.example.lab/api/badge/1/status
|
|
||||||
tags:
|
|
||||||
- web-interface
|
|
||||||
- case-management
|
|
||||||
- collaboration
|
|
||||||
- api
|
|
||||||
- workflow
|
|
||||||
- multi-user-support
|
|
||||||
- cortex-analyzer
|
|
||||||
- misp-integration
|
|
||||||
- playbooks
|
|
||||||
- metrics
|
|
||||||
- rbac
|
|
||||||
- template-driven
|
|
||||||
related_concepts:
|
|
||||||
- Digital Evidence Chain of Custody
|
|
||||||
- name: MISP
|
- name: MISP
|
||||||
icon: 🌐
|
icon: 🌐
|
||||||
type: software
|
type: software
|
||||||
@ -223,7 +165,6 @@ tools:
|
|||||||
related_concepts:
|
related_concepts:
|
||||||
- Hash Functions & Digital Signatures
|
- Hash Functions & Digital Signatures
|
||||||
related_software:
|
related_software:
|
||||||
- TheHive 5
|
|
||||||
- Cortex
|
- Cortex
|
||||||
- OpenCTI
|
- OpenCTI
|
||||||
- name: DFIR-IRIS
|
- name: DFIR-IRIS
|
||||||
@ -260,7 +201,6 @@ tools:
|
|||||||
platforms:
|
platforms:
|
||||||
- Web
|
- Web
|
||||||
related_software:
|
related_software:
|
||||||
- TheHive 5
|
|
||||||
- MISP
|
- MISP
|
||||||
- OpenCTI
|
- OpenCTI
|
||||||
domain-agnostic-software:
|
domain-agnostic-software:
|
||||||
@ -3427,6 +3367,244 @@ tools:
|
|||||||
accessType: download
|
accessType: download
|
||||||
license: "MPL\_/ AGPL"
|
license: "MPL\_/ AGPL"
|
||||||
knowledgebase: false
|
knowledgebase: false
|
||||||
|
- name: ShadowExplorer
|
||||||
|
icon: 🗂️
|
||||||
|
type: software
|
||||||
|
description: >-
|
||||||
|
Das schlanke Windows-Tool macht Volume-Shadow-Copy-Snapshots auch in Home-Editionen sichtbar und erlaubt das komfortable Durchstöbern sowie Wiederherstellen früherer Datei-Versionen. Damit lassen sich versehentlich gelöschte oder überschriebene Dateien in Sekunden zurückholen – geeignet für schnelle Triage und klassische Datenträgerforensik.
|
||||||
|
domains:
|
||||||
|
- static-investigations
|
||||||
|
- incident-response
|
||||||
|
phases:
|
||||||
|
- examination
|
||||||
|
- analysis
|
||||||
|
platforms:
|
||||||
|
- Windows
|
||||||
|
related_software:
|
||||||
|
- OSFMount
|
||||||
|
- PhotoRec
|
||||||
|
domain-agnostic-software: null
|
||||||
|
skillLevel: novice
|
||||||
|
accessType: download
|
||||||
|
url: https://www.shadowexplorer.com/
|
||||||
|
license: Freeware
|
||||||
|
knowledgebase: false
|
||||||
|
tags:
|
||||||
|
- gui
|
||||||
|
- shadow-copy
|
||||||
|
- snapshot-browsing
|
||||||
|
- file-recovery
|
||||||
|
- previous-versions
|
||||||
|
- scenario:file_recovery
|
||||||
|
- point-in-time-restore
|
||||||
|
related_concepts:
|
||||||
|
- Digital Evidence Chain of Custody
|
||||||
|
|
||||||
|
|
||||||
|
- name: Sonic Visualiser
|
||||||
|
icon: 🎵
|
||||||
|
type: software
|
||||||
|
description: >-
|
||||||
|
Die Open-Source-Audio-Analyse-Suite wird in der Forensik eingesetzt,
|
||||||
|
um Wave- und Kompressionsformate bis auf Sample-Ebene zu untersuchen.
|
||||||
|
Spektrogramm-Visualisierung, Zeit-/Frequenz-Annotationen und
|
||||||
|
Transkriptions-Plugins (Vamp) helfen, Manipulationen wie
|
||||||
|
Bandpass-Filter, Time-Stretching oder Insert-Edits nachzuweisen.
|
||||||
|
FFT- und Mel-Spectral-Views decken versteckte Audio-Watermarks oder
|
||||||
|
Steganografie auf. Export-Funktionen in CSV/JSON erlauben die
|
||||||
|
Weiterverarbeitung in Python-Notebooks oder SIEM-Pipelines.
|
||||||
|
Ideal für Voice-Authentication-Checks, Deep-Fake-Erkennung
|
||||||
|
und Beweisaufbereitung vor Gericht.
|
||||||
|
skillLevel: intermediate
|
||||||
|
url: https://www.sonicvisualiser.org/
|
||||||
|
domains:
|
||||||
|
- static-investigations
|
||||||
|
- fraud-investigation
|
||||||
|
phases:
|
||||||
|
- examination
|
||||||
|
- analysis
|
||||||
|
- reporting
|
||||||
|
platforms:
|
||||||
|
- Windows
|
||||||
|
- Linux
|
||||||
|
- macOS
|
||||||
|
accessType: download
|
||||||
|
license: GPL-2.0
|
||||||
|
knowledgebase: false
|
||||||
|
tags:
|
||||||
|
- gui
|
||||||
|
- audio-forensics
|
||||||
|
- spectrogram
|
||||||
|
- plugin-support
|
||||||
|
- annotation
|
||||||
|
- csv-export
|
||||||
|
related_concepts: []
|
||||||
|
related_software:
|
||||||
|
- Audacity
|
||||||
|
|
||||||
|
- name: Dissect
|
||||||
|
icon: 🧩
|
||||||
|
type: software
|
||||||
|
description: >-
|
||||||
|
Fox-ITs Python-Framework abstrahiert Windows- und Linux-Speicherabbilder
|
||||||
|
in virtuelle Objekte (Prozesse, Dateien, Registry, Kernel-Strukturen),
|
||||||
|
ohne zuvor ein Profil definieren zu müssen. Modularer
|
||||||
|
Hypervisor-Layer erlaubt das Mounten und gleichzeitige Analysieren
|
||||||
|
mehrerer Memory-Dumps – perfekt für großflächige Incident-Response.
|
||||||
|
Plugins dekodieren PTEs, handle tables, APC-Queues und liefern
|
||||||
|
YARA-kompatible Scans. Die Zero-Copy-Architektur beschleunigt Queries auf
|
||||||
|
Multi-GB-Images signifikant. Unterstützt Windows 11 24H2-Kernel sowie
|
||||||
|
Linux 6.x-schichten ab Juli 2025.
|
||||||
|
skillLevel: advanced
|
||||||
|
url: https://github.com/fox-it/dissect
|
||||||
|
domains:
|
||||||
|
- incident-response
|
||||||
|
- malware-analysis
|
||||||
|
- static-investigations
|
||||||
|
phases:
|
||||||
|
- examination
|
||||||
|
- analysis
|
||||||
|
platforms:
|
||||||
|
- Windows
|
||||||
|
- Linux
|
||||||
|
- macOS
|
||||||
|
accessType: download
|
||||||
|
license: Apache 2.0
|
||||||
|
knowledgebase: false
|
||||||
|
tags:
|
||||||
|
- command-line
|
||||||
|
- memory-analysis
|
||||||
|
- plugin-support
|
||||||
|
- python-library
|
||||||
|
- zero-copy
|
||||||
|
- profile-less
|
||||||
|
related_concepts:
|
||||||
|
- Regular Expressions (Regex)
|
||||||
|
related_software:
|
||||||
|
- Volatility 3
|
||||||
|
- Rekall
|
||||||
|
|
||||||
|
- name: Docker Explorer
|
||||||
|
icon: 🐳
|
||||||
|
type: software
|
||||||
|
description: >-
|
||||||
|
Googles Forensik-Toolkit zerlegt Offline-Docker-Volumes und
|
||||||
|
Overlay-Dateisysteme ohne laufenden Daemon. Es extrahiert
|
||||||
|
Container-Config, Image-Layer, ENV-Variablen, Mounted-Secrets
|
||||||
|
und schreibt Timeline-fähige Metadata-JSONs. Unterstützt btrfs,
|
||||||
|
overlay2 und zfs Storage-Driver sowie Docker Desktop (macOS/Windows).
|
||||||
|
Perfekt, um bösartige Images nach Supply-Chain-Attacken zu enttarnen
|
||||||
|
oder flüchtige Container nach einem Incident nachträglich zu analysieren.
|
||||||
|
skillLevel: intermediate
|
||||||
|
url: https://github.com/google/docker-explorer
|
||||||
|
domains:
|
||||||
|
- cloud-forensics
|
||||||
|
- incident-response
|
||||||
|
- static-investigations
|
||||||
|
phases:
|
||||||
|
- data-collection
|
||||||
|
- examination
|
||||||
|
- analysis
|
||||||
|
platforms:
|
||||||
|
- Linux
|
||||||
|
- macOS
|
||||||
|
- Windows
|
||||||
|
accessType: download
|
||||||
|
license: Apache 2.0
|
||||||
|
knowledgebase: false
|
||||||
|
tags:
|
||||||
|
- command-line
|
||||||
|
- container-forensics
|
||||||
|
- docker
|
||||||
|
- timeline
|
||||||
|
- json-export
|
||||||
|
- supply-chain
|
||||||
|
related_concepts: []
|
||||||
|
related_software:
|
||||||
|
- Velociraptor
|
||||||
|
- osquery
|
||||||
|
|
||||||
|
- name: Ghiro
|
||||||
|
icon: 🖼️
|
||||||
|
type: software
|
||||||
|
description: >-
|
||||||
|
Die Web-basierte Bildforensik-Plattform automatisiert EXIF-Analyse,
|
||||||
|
Hash-Matching, Error-Level-Evaluation (ELA) und
|
||||||
|
Steganografie-Erkennung für große Dateibatches. Unterstützt
|
||||||
|
Gesichts- und NSFW-Detection sowie GPS-Reverse-Geocoding für
|
||||||
|
Bewegungsprofile. Reports sind gerichtsfest
|
||||||
|
versioniert, REST-API und Celery-Worker skalieren auf
|
||||||
|
Millionen Bilder – ideal für CSAM-Ermittlungen oder Fake-News-Prüfung.
|
||||||
|
skillLevel: intermediate
|
||||||
|
url: https://getghiro.org/
|
||||||
|
domains:
|
||||||
|
- static-investigations
|
||||||
|
- fraud-investigation
|
||||||
|
- mobile-forensics
|
||||||
|
phases:
|
||||||
|
- examination
|
||||||
|
- analysis
|
||||||
|
- reporting
|
||||||
|
platforms:
|
||||||
|
- Web
|
||||||
|
- Linux
|
||||||
|
accessType: server-based
|
||||||
|
license: GPL-2.0
|
||||||
|
knowledgebase: false
|
||||||
|
tags:
|
||||||
|
- web-interface
|
||||||
|
- image-forensics
|
||||||
|
- exif-analysis
|
||||||
|
- steganography
|
||||||
|
- nsfw-detection
|
||||||
|
- batch-processing
|
||||||
|
related_concepts:
|
||||||
|
- Hash Functions & Digital Signatures
|
||||||
|
related_software:
|
||||||
|
- ExifTool
|
||||||
|
- PhotoRec
|
||||||
|
|
||||||
|
- name: Sherloq
|
||||||
|
icon: 🔍
|
||||||
|
type: software
|
||||||
|
description: >-
|
||||||
|
Das Python-GUI-Toolkit für visuelle Datei-Analyse kombiniert
|
||||||
|
klassische Reverse-Steganografie-Techniken (LSB, Palette-Tweaking,
|
||||||
|
DCT-Coefficient-Scanning) mit modernen CV-Algorithmen.
|
||||||
|
Heatmaps und Histogramm-Diffs zeigen Manipulations-Hotspots,
|
||||||
|
während eine „Carve-All-Layers“-Funktion versteckte Daten in PNG,
|
||||||
|
JPEG, BMP, GIF und Audio-Spectra aufspürt. Plugins für zsteg,
|
||||||
|
binwalk und exiftool erweitern die Pipeline.
|
||||||
|
Eine Must-have-Ergänzung zu Ghidra & friends, wenn
|
||||||
|
Malware Dateien als Dead-Drop nutzt.
|
||||||
|
skillLevel: intermediate
|
||||||
|
url: https://github.com/GuidoBartoli/sherloq
|
||||||
|
domains:
|
||||||
|
- malware-analysis
|
||||||
|
- static-investigations
|
||||||
|
phases:
|
||||||
|
- examination
|
||||||
|
- analysis
|
||||||
|
platforms:
|
||||||
|
- Windows
|
||||||
|
- Linux
|
||||||
|
- macOS
|
||||||
|
accessType: download
|
||||||
|
license: MIT
|
||||||
|
knowledgebase: false
|
||||||
|
tags:
|
||||||
|
- gui
|
||||||
|
- image-forensics
|
||||||
|
- steganography
|
||||||
|
- lsb-extraction
|
||||||
|
- histogram-analysis
|
||||||
|
- plugin-support
|
||||||
|
related_concepts:
|
||||||
|
- Regular Expressions (Regex)
|
||||||
|
related_software:
|
||||||
|
- Ghiro
|
||||||
|
- CyberChef
|
||||||
|
|
||||||
- name: Cortex
|
- name: Cortex
|
||||||
type: software
|
type: software
|
||||||
description: >-
|
description: >-
|
||||||
@ -3797,7 +3975,7 @@ tools:
|
|||||||
- name: KAPE
|
- name: KAPE
|
||||||
type: software
|
type: software
|
||||||
description: >-
|
description: >-
|
||||||
Kroll Artifact Parser and Extractor revolutioniert Windows-Forensik durch
|
Kroll Artifact Parser and Extractor versucht sich an Windows-Forensik durch
|
||||||
intelligente Ziel-basierte Sammlung. Statt Full-Disk-Images extrahiert
|
intelligente Ziel-basierte Sammlung. Statt Full-Disk-Images extrahiert
|
||||||
KAPE gezielt kritische Artefakte: Registry-Hives, Event-Logs, Prefetch,
|
KAPE gezielt kritische Artefakte: Registry-Hives, Event-Logs, Prefetch,
|
||||||
Browser- Daten, Scheduled-Tasks in Minuten statt Stunden. Die Target-Files
|
Browser- Daten, Scheduled-Tasks in Minuten statt Stunden. Die Target-Files
|
||||||
@ -3805,12 +3983,10 @@ tools:
|
|||||||
Besonders clever: Compound-Targets gruppieren zusammengehörige Artefakte
|
Besonders clever: Compound-Targets gruppieren zusammengehörige Artefakte
|
||||||
(z.B. "Browser" sammelt Chrome+Firefox+Edge), die gKAPE-GUI macht es auch
|
(z.B. "Browser" sammelt Chrome+Firefox+Edge), die gKAPE-GUI macht es auch
|
||||||
für Nicht-Techniker zugänglich. Batch-Mode verarbeitet mehrere Images
|
für Nicht-Techniker zugänglich. Batch-Mode verarbeitet mehrere Images
|
||||||
parallel. Output direkt kompatibel zu Timeline-Tools wie Plaso. Die
|
parallel. Output direkt kompatibel zu Timeline-Tools wie Plaso.
|
||||||
ständigen Community-Updates halten mit Windows-Entwicklungen Schritt.
|
|
||||||
VSS-Processing analysiert Shadow- Copies automatisch. Der
|
VSS-Processing analysiert Shadow- Copies automatisch. Der
|
||||||
Remote-Collection-Mode sammelt über Netzwerk. Kostenlos aber
|
Remote-Collection-Mode sammelt über Netzwerk. Kostenlos (mit Registrierung) aber
|
||||||
Enterprise-Support verfügbar. Der neue Standard für effiziente
|
Enterprise-Support verfügbar.
|
||||||
Windows-Forensik-Triage.
|
|
||||||
skillLevel: intermediate
|
skillLevel: intermediate
|
||||||
url: https://www.kroll.com/kape
|
url: https://www.kroll.com/kape
|
||||||
icon: 🧰
|
icon: 🧰
|
||||||
@ -3825,7 +4001,7 @@ tools:
|
|||||||
platforms:
|
platforms:
|
||||||
- Windows
|
- Windows
|
||||||
accessType: download
|
accessType: download
|
||||||
license: Freeware
|
license: Proprietary
|
||||||
knowledgebase: false
|
knowledgebase: false
|
||||||
- name: Kibana
|
- name: Kibana
|
||||||
type: software
|
type: software
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
// src/pages/api/ai/enhance-input.ts - ENHANCED with forensics methodology
|
// src/pages/api/ai/enhance-input.ts - Enhanced AI service compatibility
|
||||||
|
|
||||||
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';
|
||||||
@ -15,8 +16,8 @@ function getEnv(key: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const AI_ENDPOINT = getEnv('AI_ANALYZER_ENDPOINT');
|
const AI_ENDPOINT = getEnv('AI_ANALYZER_ENDPOINT');
|
||||||
const AI_API_KEY = getEnv('AI_ANALYZER_API_KEY');
|
const AI_ANALYZER_API_KEY = getEnv('AI_ANALYZER_API_KEY');
|
||||||
const AI_MODEL = getEnv('AI_ANALYZER_MODEL');
|
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; // 1 minute
|
const RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute
|
||||||
@ -93,6 +94,39 @@ ${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');
|
||||||
@ -121,31 +155,11 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
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(() =>
|
const aiResponse = await enqueueApiCall(() => callAIService(systemPrompt), taskId);
|
||||||
fetch(`${AI_ENDPOINT}/v1/chat/completions`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': `Bearer ${AI_API_KEY}`
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
model: AI_MODEL,
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
role: 'user',
|
|
||||||
content: systemPrompt
|
|
||||||
}
|
|
||||||
],
|
|
||||||
max_tokens: 300,
|
|
||||||
temperature: 0.7,
|
|
||||||
top_p: 0.9,
|
|
||||||
frequency_penalty: 0.2,
|
|
||||||
presence_penalty: 0.1
|
|
||||||
})
|
|
||||||
}), taskId);
|
|
||||||
|
|
||||||
if (!aiResponse.ok) {
|
if (!aiResponse.ok) {
|
||||||
console.error('AI enhancement error:', await aiResponse.text());
|
const errorText = await aiResponse.text();
|
||||||
|
console.error('[ENHANCE API] AI enhancement error:', errorText, 'Status:', aiResponse.status);
|
||||||
return apiServerError.unavailable('Enhancement service unavailable');
|
return apiServerError.unavailable('Enhancement service unavailable');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,13 +202,13 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
questions = [];
|
questions = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[AI Enhancement] User: ${userId}, Forensics Questions: ${questions.length}, Input length: ${sanitizedInput.length}`);
|
console.log(`[ENHANCE API] User: ${userId}, Forensics Questions: ${questions.length}, Input length: ${sanitizedInput.length}`);
|
||||||
|
|
||||||
return new Response(JSON.stringify({
|
return new Response(JSON.stringify({
|
||||||
success: true,
|
success: true,
|
||||||
questions,
|
questions,
|
||||||
taskId,
|
taskId,
|
||||||
inputComplete: questions.length === 0 // Flag to indicate if input seems complete
|
inputComplete: questions.length === 0
|
||||||
}), {
|
}), {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
@ -21,6 +21,39 @@ const phases = data.phases;
|
|||||||
Systematische digitale Forensik nach bewährter NIST SP 800-86 Methodik.<br>
|
Systematische digitale Forensik nach bewährter NIST SP 800-86 Methodik.<br>
|
||||||
Wählen Sie Ihren Ansatz für die Werkzeugauswahl:
|
Wählen Sie Ihren Ansatz für die Werkzeugauswahl:
|
||||||
</p>
|
</p>
|
||||||
|
<div class="ai-hero-spotlight">
|
||||||
|
<div class="ai-spotlight-content">
|
||||||
|
<div class="ai-spotlight-icon">
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<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"/>
|
||||||
|
<circle cx="12" cy="12" r="2"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ai-spotlight-text">
|
||||||
|
<h3>Forensic AI-Beratung</h3>
|
||||||
|
<p>Analyse des Untersuchungsszenarios mit Empfehlungen zum Vorgehen</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="ai-query-btn" class="btn btn-accent btn-lg ai-primary-btn">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M9 11H5a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7a2 2 0 0 0-2-2h-4"/>
|
||||||
|
<path d="M9 11V7a3 3 0 0 1 6 0v4"/>
|
||||||
|
</svg>
|
||||||
|
KI-Beratung starten
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="ml-2">
|
||||||
|
<line x1="7" y1="17" x2="17" y2="7"/>
|
||||||
|
<polyline points="7,7 17,7 17,17"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="ai-features-mini">
|
||||||
|
<span class="badge badge-secondary">Workflow-Empfehlungen</span>
|
||||||
|
<span class="badge badge-secondary">Transparenz</span>
|
||||||
|
<span class="badge badge-secondary">Sofortige Analyse</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="approach-selector">
|
<div class="approach-selector">
|
||||||
<div class="approach-card methodology" onclick="selectApproach('methodology')">
|
<div class="approach-card methodology" onclick="selectApproach('methodology')">
|
||||||
@ -74,14 +107,6 @@ const phases = data.phases;
|
|||||||
Infos, SSO & Zugang
|
Infos, SSO & Zugang
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<button id="ai-query-btn" class="btn btn-accent">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<path d="M9 11H5a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7a2 2 0 0 0-2-2h-4"/>
|
|
||||||
<path d="M9 11V7a3 3 0 0 1 6 0v4"/>
|
|
||||||
</svg>
|
|
||||||
KI-Beratung
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<a href="/contribute" class="btn" style="background-color: var(--color-warning); color: white; border-color: var(--color-warning);" data-contribute-button="new">
|
<a href="/contribute" class="btn" style="background-color: var(--color-warning); color: white; border-color: var(--color-warning);" data-contribute-button="new">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||||
@ -182,7 +207,9 @@ const phases = data.phases;
|
|||||||
document.querySelectorAll('.approach-card').forEach(card => {
|
document.querySelectorAll('.approach-card').forEach(card => {
|
||||||
card.classList.remove('selected');
|
card.classList.remove('selected');
|
||||||
});
|
});
|
||||||
document.querySelector(`.approach-card.${approach}`).classList.add('selected');
|
|
||||||
|
const selectedCard = document.querySelector(`.approach-card.${approach}`);
|
||||||
|
if (selectedCard) selectedCard.classList.add('selected');
|
||||||
|
|
||||||
if (approach === 'methodology') {
|
if (approach === 'methodology') {
|
||||||
const methodologySection = document.getElementById('methodology-section');
|
const methodologySection = document.getElementById('methodology-section');
|
||||||
@ -239,60 +266,49 @@ const phases = data.phases;
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (aiQueryBtn) {
|
if (aiQueryBtn) {
|
||||||
aiQueryBtn.addEventListener('click', async () => {
|
aiQueryBtn.addEventListener('click', () => {
|
||||||
if (typeof window.requireClientAuth === 'function') {
|
aiQueryBtn.classList.add('activated');
|
||||||
await window.requireClientAuth(() => switchToView('ai'), `${window.location.pathname}?view=ai`, 'ai');
|
setTimeout(() => aiQueryBtn.classList.remove('activated'), 400);
|
||||||
|
|
||||||
|
switchToView('ai');
|
||||||
|
|
||||||
|
window.dispatchEvent(new CustomEvent('viewChanged', { detail: 'ai' }));
|
||||||
|
|
||||||
|
if (window.scrollToElementById) {
|
||||||
|
window.scrollToElementById('ai-interface');
|
||||||
} else {
|
} else {
|
||||||
console.warn('[AUTH] requireClientAuth not available');
|
aiInterface.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
switchToView('ai');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchToView(view) {
|
function switchToView(view) {
|
||||||
toolsGrid.style.display = 'none';
|
const toolsGrid = document.getElementById('tools-grid');
|
||||||
matrixContainer.style.display = 'none';
|
const matrixContainer = document.getElementById('matrix-container');
|
||||||
aiInterface.style.display = 'none';
|
const aiInterface = document.getElementById('ai-interface');
|
||||||
filtersSection.style.display = 'none';
|
const filtersSection = document.getElementById('filters-section');
|
||||||
|
const noResults = document.getElementById('no-results');
|
||||||
|
|
||||||
const viewToggles = document.querySelectorAll('.view-toggle');
|
if (toolsGrid) toolsGrid.style.display = 'none';
|
||||||
viewToggles.forEach(btn => {
|
if (matrixContainer) matrixContainer.style.display = 'none';
|
||||||
btn.classList.toggle('active', btn.getAttribute('data-view') === view);
|
if (aiInterface) aiInterface.style.display = 'none';
|
||||||
});
|
if (filtersSection) filtersSection.style.display = 'none';
|
||||||
|
if (noResults) noResults.style.display = 'none';
|
||||||
|
|
||||||
switch (view) {
|
switch (view) {
|
||||||
case 'ai':
|
case 'grid':
|
||||||
aiInterface.style.display = 'block';
|
if (toolsGrid) toolsGrid.style.display = 'block';
|
||||||
filtersSection.style.display = 'block';
|
if (filtersSection) filtersSection.style.display = 'block';
|
||||||
hideFilterControls();
|
break;
|
||||||
if (window.restoreAIResults) {
|
case 'matrix':
|
||||||
window.restoreAIResults();
|
if (matrixContainer) matrixContainer.style.display = 'block';
|
||||||
}
|
if (filtersSection) filtersSection.style.display = 'block';
|
||||||
const aiInput = document.getElementById('ai-query-input');
|
break;
|
||||||
if (aiInput) {
|
case 'ai':
|
||||||
setTimeout(() => aiInput.focus(), 100);
|
if (aiInterface) aiInterface.style.display = 'block';
|
||||||
}
|
break;
|
||||||
break;
|
|
||||||
case 'matrix':
|
|
||||||
matrixContainer.style.display = 'block';
|
|
||||||
filtersSection.style.display = 'block';
|
|
||||||
showFilterControls();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
toolsGrid.style.display = 'block';
|
|
||||||
filtersSection.style.display = 'block';
|
|
||||||
showFilterControls();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
window.scrollToElementById('filters-section');
|
|
||||||
}, 150);
|
|
||||||
|
|
||||||
if (window.location.search) {
|
|
||||||
window.history.replaceState({}, '', window.location.pathname);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function hideFilterControls() {
|
function hideFilterControls() {
|
||||||
const filterSections = document.querySelectorAll('.filter-section');
|
const filterSections = document.querySelectorAll('.filter-section');
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -144,24 +144,11 @@ a:hover {
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container-wide {
|
|
||||||
max-width: 1400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Section Utilities */
|
/* Section Utilities */
|
||||||
.section {
|
.section {
|
||||||
padding: 2rem 0;
|
padding: 2rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-sm {
|
|
||||||
padding: 1rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-lg {
|
|
||||||
padding: 3rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Flex Utilities */
|
/* Flex Utilities */
|
||||||
.flex {
|
.flex {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -209,10 +196,6 @@ a:hover {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.items-start {
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.justify-center {
|
.justify-center {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
@ -231,11 +214,6 @@ a:hover {
|
|||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-3 {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-4 {
|
.grid-4 {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||||
@ -248,24 +226,11 @@ a:hover {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-auto-fit-sm {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-auto-fit-lg {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Gap Utilities */
|
/* Gap Utilities */
|
||||||
.gap-1 { gap: 0.25rem; }
|
.gap-1 { gap: 0.25rem; }
|
||||||
.gap-2 { gap: 0.5rem; }
|
.gap-2 { gap: 0.5rem; }
|
||||||
.gap-3 { gap: 0.75rem; }
|
.gap-3 { gap: 0.75rem; }
|
||||||
.gap-4 { gap: 1rem; }
|
.gap-4 { gap: 1rem; }
|
||||||
.gap-6 { gap: 1.5rem; }
|
|
||||||
.gap-8 { gap: 2rem; }
|
.gap-8 { gap: 2rem; }
|
||||||
|
|
||||||
/* ===================================================================
|
/* ===================================================================
|
||||||
@ -273,29 +238,16 @@ a:hover {
|
|||||||
================================================================= */
|
================================================================= */
|
||||||
|
|
||||||
/* Margin Utilities */
|
/* Margin Utilities */
|
||||||
.m-0 { margin: 0; }
|
|
||||||
.m-1 { margin: 0.25rem; }
|
|
||||||
.m-2 { margin: 0.5rem; }
|
|
||||||
.m-3 { margin: 0.75rem; }
|
|
||||||
.m-4 { margin: 1rem; }
|
|
||||||
.m-6 { margin: 1.5rem; }
|
|
||||||
.m-8 { margin: 2rem; }
|
|
||||||
|
|
||||||
.mx-auto { margin-left: auto; margin-right: auto; }
|
.mx-auto { margin-left: auto; margin-right: auto; }
|
||||||
|
|
||||||
.mt-0 { margin-top: 0; }
|
|
||||||
.mt-1 { margin-top: 0.25rem; }
|
.mt-1 { margin-top: 0.25rem; }
|
||||||
.mt-2 { margin-top: 0.5rem; }
|
.mt-2 { margin-top: 0.5rem; }
|
||||||
.mt-3 { margin-top: 0.75rem; }
|
.mt-3 { margin-top: 0.75rem; }
|
||||||
.mt-4 { margin-top: 1rem; }
|
.mt-4 { margin-top: 1rem; }
|
||||||
.mt-6 { margin-top: 1.5rem; }
|
|
||||||
.mt-8 { margin-top: 2rem; }
|
|
||||||
.mt-auto { margin-top: auto; }
|
.mt-auto { margin-top: auto; }
|
||||||
|
|
||||||
.mr-1 { margin-right: 0.25rem; }
|
|
||||||
.mr-2 { margin-right: 0.5rem; }
|
.mr-2 { margin-right: 0.5rem; }
|
||||||
.mr-3 { margin-right: 0.75rem; }
|
.mr-3 { margin-right: 0.75rem; }
|
||||||
.mr-4 { margin-right: 1rem; }
|
|
||||||
|
|
||||||
.mb-0 { margin-bottom: 0; }
|
.mb-0 { margin-bottom: 0; }
|
||||||
.mb-1 { margin-bottom: 0.25rem; }
|
.mb-1 { margin-bottom: 0.25rem; }
|
||||||
@ -305,34 +257,22 @@ a:hover {
|
|||||||
.mb-6 { margin-bottom: 1.5rem; }
|
.mb-6 { margin-bottom: 1.5rem; }
|
||||||
.mb-8 { margin-bottom: 2rem; }
|
.mb-8 { margin-bottom: 2rem; }
|
||||||
|
|
||||||
.ml-1 { margin-left: 0.25rem; }
|
|
||||||
.ml-2 { margin-left: 0.5rem; }
|
.ml-2 { margin-left: 0.5rem; }
|
||||||
.ml-3 { margin-left: 0.75rem; }
|
|
||||||
.ml-4 { margin-left: 1rem; }
|
|
||||||
|
|
||||||
/* Padding Utilities */
|
/* Padding Utilities */
|
||||||
.p-0 { padding: 0; }
|
|
||||||
.p-1 { padding: 0.25rem; }
|
|
||||||
.p-2 { padding: 0.5rem; }
|
.p-2 { padding: 0.5rem; }
|
||||||
.p-3 { padding: 0.75rem; }
|
.p-3 { padding: 0.75rem; }
|
||||||
.p-4 { padding: 1rem; }
|
.p-4 { padding: 1rem; }
|
||||||
.p-6 { padding: 1.5rem; }
|
.p-6 { padding: 1.5rem; }
|
||||||
.p-8 { padding: 2rem; }
|
.p-8 { padding: 2rem; }
|
||||||
|
|
||||||
.px-1 { padding-left: 0.25rem; padding-right: 0.25rem; }
|
|
||||||
.px-2 { padding-left: 0.5rem; padding-right: 0.5rem; }
|
|
||||||
.px-3 { padding-left: 0.75rem; padding-right: 0.75rem; }
|
.px-3 { padding-left: 0.75rem; padding-right: 0.75rem; }
|
||||||
.px-4 { padding-left: 1rem; padding-right: 1rem; }
|
.px-4 { padding-left: 1rem; padding-right: 1rem; }
|
||||||
|
|
||||||
.py-1 { padding-top: 0.25rem; padding-bottom: 0.25rem; }
|
|
||||||
.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
|
.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
|
||||||
.py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
|
|
||||||
.py-4 { padding-top: 1rem; padding-bottom: 1rem; }
|
|
||||||
.py-6 { padding-top: 1.5rem; padding-bottom: 1.5rem; }
|
|
||||||
.py-8 { padding-top: 2rem; padding-bottom: 2rem; }
|
.py-8 { padding-top: 2rem; padding-bottom: 2rem; }
|
||||||
|
|
||||||
.pt-3 { padding-top: 0.75rem; }
|
.pt-3 { padding-top: 0.75rem; }
|
||||||
.pb-2 { padding-bottom: 0.5rem; }
|
|
||||||
|
|
||||||
/* ===================================================================
|
/* ===================================================================
|
||||||
6. TARGETED UTILITY CLASSES (FOR INLINE STYLE CONSOLIDATION)
|
6. TARGETED UTILITY CLASSES (FOR INLINE STYLE CONSOLIDATION)
|
||||||
@ -342,8 +282,6 @@ a:hover {
|
|||||||
.section-padding { padding: 2rem 0; }
|
.section-padding { padding: 2rem 0; }
|
||||||
.content-center { text-align: center; margin-bottom: 1rem; }
|
.content-center { text-align: center; margin-bottom: 1rem; }
|
||||||
.content-center-lg { text-align: center; margin-bottom: 2rem; }
|
.content-center-lg { text-align: center; margin-bottom: 2rem; }
|
||||||
.content-narrow { max-width: 900px; margin: 0 auto; }
|
|
||||||
.content-wide { max-width: 1200px; margin: 0 auto; }
|
|
||||||
|
|
||||||
/* Card Info Variants (for inline background/padding combinations) */
|
/* Card Info Variants (for inline background/padding combinations) */
|
||||||
.card-info-sm {
|
.card-info-sm {
|
||||||
@ -352,20 +290,6 @@ a:hover {
|
|||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-info-md {
|
|
||||||
background-color: var(--color-bg-secondary);
|
|
||||||
padding: 1.5rem;
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-info-lg {
|
|
||||||
background-color: var(--color-bg-secondary);
|
|
||||||
padding: 2rem;
|
|
||||||
border-radius: 1rem;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header/Title Combinations */
|
/* Header/Title Combinations */
|
||||||
.header-center {
|
.header-center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@ -384,42 +308,6 @@ a:hover {
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Metadata/Info Text Combinations */
|
|
||||||
.info-text {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-text-center {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Grid Auto-fit Variants for common inline grid patterns */
|
|
||||||
.grid-auto-300 {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-auto-400 {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-auto-500 {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add to global.css */
|
|
||||||
.pros-cons-section { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
|
|
||||||
.tool-metadata { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 0.75rem; }
|
|
||||||
.grid-cols-2 { grid-template-columns: 1fr 1fr; }
|
.grid-cols-2 { grid-template-columns: 1fr 1fr; }
|
||||||
.flex-shrink-1 { flex-shrink: 1; }
|
.flex-shrink-1 { flex-shrink: 1; }
|
||||||
|
|
||||||
@ -429,7 +317,6 @@ a:hover {
|
|||||||
|
|
||||||
/* Text Utilities */
|
/* Text Utilities */
|
||||||
.text-center { text-align: center; }
|
.text-center { text-align: center; }
|
||||||
.text-left { text-align: left; }
|
|
||||||
.text-right { text-align: right; }
|
.text-right { text-align: right; }
|
||||||
|
|
||||||
.text-xs { font-size: 0.75rem; }
|
.text-xs { font-size: 0.75rem; }
|
||||||
@ -439,10 +326,8 @@ a:hover {
|
|||||||
.text-xl { font-size: 1.25rem; }
|
.text-xl { font-size: 1.25rem; }
|
||||||
.text-2xl { font-size: 1.5rem; }
|
.text-2xl { font-size: 1.5rem; }
|
||||||
|
|
||||||
.font-normal { font-weight: 400; }
|
|
||||||
.font-medium { font-weight: 500; }
|
.font-medium { font-weight: 500; }
|
||||||
.font-semibold { font-weight: 600; }
|
.font-semibold { font-weight: 600; }
|
||||||
.font-bold { font-weight: 700; }
|
|
||||||
|
|
||||||
.leading-tight { line-height: 1.25; }
|
.leading-tight { line-height: 1.25; }
|
||||||
.leading-normal { line-height: 1.5; }
|
.leading-normal { line-height: 1.5; }
|
||||||
@ -454,12 +339,10 @@ a:hover {
|
|||||||
.text-secondary { color: var(--color-text-secondary); }
|
.text-secondary { color: var(--color-text-secondary); }
|
||||||
.text-accent { color: var(--color-accent); }
|
.text-accent { color: var(--color-accent); }
|
||||||
.text-warning { color: var(--color-warning); }
|
.text-warning { color: var(--color-warning); }
|
||||||
.text-error { color: var(--color-error); }
|
|
||||||
|
|
||||||
/* Display Utilities */
|
/* Display Utilities */
|
||||||
.block { display: block; }
|
.block { display: block; }
|
||||||
.inline { display: inline; }
|
.inline { display: inline; }
|
||||||
.inline-block { display: inline-block; }
|
|
||||||
.hidden { display: none; }
|
.hidden { display: none; }
|
||||||
|
|
||||||
/* Size Utilities */
|
/* Size Utilities */
|
||||||
@ -475,18 +358,13 @@ a:hover {
|
|||||||
.fixed { position: fixed; }
|
.fixed { position: fixed; }
|
||||||
.sticky { position: sticky; }
|
.sticky { position: sticky; }
|
||||||
|
|
||||||
.top-0 { top: 0; }
|
|
||||||
.bottom-8 { bottom: 2rem; }
|
.bottom-8 { bottom: 2rem; }
|
||||||
.right-8 { right: 2rem; }
|
.right-8 { right: 2rem; }
|
||||||
.left-0 { left: 0; }
|
|
||||||
|
|
||||||
.z-10 { z-index: 10; }
|
|
||||||
.z-50 { z-index: 50; }
|
.z-50 { z-index: 50; }
|
||||||
.z-100 { z-index: 100; }
|
|
||||||
|
|
||||||
/* Overflow Utilities */
|
/* Overflow Utilities */
|
||||||
.overflow-hidden { overflow: hidden; }
|
.overflow-hidden { overflow: hidden; }
|
||||||
.overflow-auto { overflow: auto; }
|
|
||||||
|
|
||||||
/* Border Utilities */
|
/* Border Utilities */
|
||||||
.border { border: 1px solid var(--color-border); }
|
.border { border: 1px solid var(--color-border); }
|
||||||
@ -494,19 +372,15 @@ a:hover {
|
|||||||
.border-l-4 { border-left: 4px solid; }
|
.border-l-4 { border-left: 4px solid; }
|
||||||
|
|
||||||
.rounded { border-radius: 0.25rem; }
|
.rounded { border-radius: 0.25rem; }
|
||||||
.rounded-md { border-radius: 0.375rem; }
|
|
||||||
.rounded-lg { border-radius: 0.5rem; }
|
.rounded-lg { border-radius: 0.5rem; }
|
||||||
.rounded-xl { border-radius: 0.75rem; }
|
.rounded-xl { border-radius: 0.75rem; }
|
||||||
.rounded-2xl { border-radius: 1rem; }
|
|
||||||
|
|
||||||
/* Background Utilities */
|
/* Background Utilities */
|
||||||
.bg-secondary { background-color: var(--color-bg-secondary); }
|
.bg-secondary { background-color: var(--color-bg-secondary); }
|
||||||
.bg-tertiary { background-color: var(--color-bg-tertiary); }
|
|
||||||
|
|
||||||
/* Cursor Utilities */
|
/* Cursor Utilities */
|
||||||
.cursor-pointer { cursor: pointer; }
|
.cursor-pointer { cursor: pointer; }
|
||||||
|
|
||||||
.h-12 { height: 3rem; }
|
|
||||||
.align-middle { vertical-align: middle; }
|
.align-middle { vertical-align: middle; }
|
||||||
|
|
||||||
|
|
||||||
@ -646,11 +520,6 @@ nav {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Button Sizes */
|
/* Button Sizes */
|
||||||
.btn-xs {
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sm {
|
.btn-sm {
|
||||||
padding: 0.375rem 0.75rem;
|
padding: 0.375rem 0.75rem;
|
||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
@ -808,14 +677,6 @@ input[type="checkbox"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Card Variants */
|
/* Card Variants */
|
||||||
.card-sm {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-lg {
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-info {
|
.card-info {
|
||||||
background-color: var(--color-bg-secondary);
|
background-color: var(--color-bg-secondary);
|
||||||
border-left: 4px solid var(--color-primary);
|
border-left: 4px solid var(--color-primary);
|
||||||
@ -826,15 +687,6 @@ input[type="checkbox"] {
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-gradient {
|
|
||||||
background: linear-gradient(135deg, var(--color-bg-secondary) 0%, var(--color-bg-tertiary) 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-hero {
|
|
||||||
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-accent) 100%);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Card Type Modifiers */
|
/* Card Type Modifiers */
|
||||||
.card-hosted {
|
.card-hosted {
|
||||||
background-color: var(--color-hosted-bg);
|
background-color: var(--color-hosted-bg);
|
||||||
@ -878,7 +730,6 @@ input[type="checkbox"] {
|
|||||||
.badge-primary { background-color: var(--color-primary); color: white; }
|
.badge-primary { background-color: var(--color-primary); color: white; }
|
||||||
.badge-secondary { background-color: var(--color-bg-secondary); color: var(--color-text); border: 1px solid var(--color-border); }
|
.badge-secondary { background-color: var(--color-bg-secondary); color: var(--color-text); border: 1px solid var(--color-border); }
|
||||||
.badge-success { background-color: var(--color-accent); color: white; }
|
.badge-success { background-color: var(--color-accent); color: white; }
|
||||||
.badge-accent { background-color: var(--color-accent); color: white; }
|
|
||||||
.badge-warning { background-color: var(--color-warning); color: white; }
|
.badge-warning { background-color: var(--color-warning); color: white; }
|
||||||
.badge-error { background-color: var(--color-error); color: white; }
|
.badge-error { background-color: var(--color-error); color: white; }
|
||||||
|
|
||||||
@ -1750,7 +1601,93 @@ input[type="checkbox"] {
|
|||||||
border-color: var(--color-accent) !important;
|
border-color: var(--color-accent) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ai-hero-spotlight {
|
||||||
|
background: linear-gradient(135deg, var(--color-bg) 0%, var(--color-bg-secondary) 100%);
|
||||||
|
border: 2px solid var(--color-accent);
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 2rem;
|
||||||
|
margin: 1.5rem auto;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 600px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-hero-spotlight::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 3px;
|
||||||
|
background: linear-gradient(90deg, var(--color-accent) 0%, var(--color-primary) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-spotlight-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-spotlight-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
background: linear-gradient(135deg, var(--color-accent) 0%, var(--color-primary) 100%);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-spotlight-text h3 {
|
||||||
|
margin: 0 0 0.25rem 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-spotlight-text p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-primary-btn {
|
||||||
|
padding: 0.875rem 2rem;
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-primary-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: var(--shadow-lg), 0 8px 20px rgba(5, 150, 105, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-primary-btn svg:last-child {
|
||||||
|
transition: var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-primary-btn:hover svg:last-child {
|
||||||
|
transform: translate(2px, -2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mini features display */
|
||||||
|
.ai-features-mini {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-features-mini .badge {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===================================================================
|
/* ===================================================================
|
||||||
16. AI INTERFACE (CONSOLIDATED)
|
16. AI INTERFACE (CONSOLIDATED)
|
||||||
@ -1985,21 +1922,7 @@ input[type="checkbox"] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Animation for micro-task progress */
|
|
||||||
@keyframes micro-task-pulse {
|
|
||||||
0%, 100% { opacity: 1; }
|
|
||||||
50% { opacity: 0.7; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.micro-step.active {
|
|
||||||
animation: micro-task-pulse 2s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes micro-task-complete {
|
|
||||||
0% { transform: scale(1); }
|
|
||||||
50% { transform: scale(1.1); }
|
|
||||||
100% { transform: scale(1); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.micro-step.completed {
|
.micro-step.completed {
|
||||||
animation: micro-task-complete 0.6s ease-out;
|
animation: micro-task-complete 0.6s ease-out;
|
||||||
@ -2015,6 +1938,7 @@ input[type="checkbox"] {
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.phase-header {
|
.phase-header {
|
||||||
@ -2145,6 +2069,10 @@ input[type="checkbox"] {
|
|||||||
border-color: var(--color-method);
|
border-color: var(--color-method);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tool-recommendation:hover {
|
||||||
|
box-shadow: 0 0 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
.tool-rec-header {
|
.tool-rec-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -2183,14 +2111,6 @@ input[type="checkbox"] {
|
|||||||
border-left: 3px solid var(--color-primary);
|
border-left: 3px solid var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-rec-metadata {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.375rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===================================================================
|
/* ===================================================================
|
||||||
18. APPROACH SELECTION (CONSOLIDATED)
|
18. APPROACH SELECTION (CONSOLIDATED)
|
||||||
================================================================= */
|
================================================================= */
|
||||||
@ -2566,11 +2486,36 @@ footer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Animation for micro-task progress */
|
||||||
|
@keyframes micro-task-pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.7; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.micro-step.active {
|
||||||
|
animation: micro-task-pulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes micro-task-complete {
|
||||||
|
0% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.1); }
|
||||||
|
100% { transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ai-spotlight-pulse {
|
||||||
|
0% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.02); }
|
||||||
|
100% { transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-primary-btn.activated {
|
||||||
|
animation: ai-spotlight-pulse 0.4s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===================================================================
|
/* ===================================================================
|
||||||
21. SMART PROMPTING INTERFACE (MISSING STYLES ADDED BACK)
|
21. SMART PROMPTING INTERFACE (MISSING STYLES ADDED BACK)
|
||||||
================================================================= */
|
================================================================= */
|
||||||
|
|
||||||
/* Smart Prompting Container */
|
|
||||||
.smart-prompting-container {
|
.smart-prompting-container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
animation: smartPromptSlideIn 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
animation: smartPromptSlideIn 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
@ -2852,12 +2797,6 @@ footer {
|
|||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-unit {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
margin-left: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.queue-progress-container {
|
.queue-progress-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@ -3354,6 +3293,30 @@ footer {
|
|||||||
min-height: 100px;
|
min-height: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ai-spotlight-content {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-spotlight-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-spotlight-text h3 {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-primary-btn {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-features-mini {
|
||||||
|
gap: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
.approach-hero {
|
.approach-hero {
|
||||||
padding: 2rem 1rem;
|
padding: 2rem 1rem;
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
@ -3406,7 +3369,6 @@ footer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.grid-2,
|
.grid-2,
|
||||||
.grid-3,
|
|
||||||
.grid-4 {
|
.grid-4 {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
@ -3437,10 +3399,6 @@ footer {
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-row {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-toggles-compact {
|
.filter-toggles-compact {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
@ -3625,6 +3583,14 @@ footer {
|
|||||||
flex: 1 1 100%;
|
flex: 1 1 100%;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
.ai-hero-spotlight {
|
||||||
|
padding: 1.5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-features-mini {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -3632,9 +3598,6 @@ footer {
|
|||||||
MIGRATION UTILITIES - Additional classes for inline style migration
|
MIGRATION UTILITIES - Additional classes for inline style migration
|
||||||
================================================================= */
|
================================================================= */
|
||||||
|
|
||||||
/* Height utilities */
|
|
||||||
.h-12 { height: 3rem; }
|
|
||||||
|
|
||||||
/* Alignment utilities */
|
/* Alignment utilities */
|
||||||
.align-middle { vertical-align: middle; }
|
.align-middle { vertical-align: middle; }
|
||||||
|
|
||||||
@ -3659,17 +3622,8 @@ footer {
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Enhanced card info variants (if not already present) */
|
|
||||||
.card-info-xl {
|
|
||||||
background-color: var(--color-bg-secondary);
|
|
||||||
padding: 2.5rem;
|
|
||||||
border-radius: 1.25rem;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ensure we have all text size variants */
|
/* Ensure we have all text size variants */
|
||||||
.text-2xl { font-size: 1.5rem; }
|
.text-2xl { font-size: 1.5rem; }
|
||||||
.text-3xl { font-size: 1.875rem; }
|
|
||||||
|
|
||||||
/* Additional spacing utilities that might be missing */
|
/* Additional spacing utilities that might be missing */
|
||||||
.py-8 { padding-top: 2rem; padding-bottom: 2rem; }
|
.py-8 { padding-top: 2rem; padding-bottom: 2rem; }
|
||||||
@ -3681,7 +3635,6 @@ footer {
|
|||||||
|
|
||||||
/* Additional rounded variants */
|
/* Additional rounded variants */
|
||||||
.rounded-xl { border-radius: 0.75rem; }
|
.rounded-xl { border-radius: 0.75rem; }
|
||||||
.rounded-2xl { border-radius: 1rem; }
|
|
||||||
|
|
||||||
/* ===================================================================
|
/* ===================================================================
|
||||||
23. MARKDOWN CONTENT
|
23. MARKDOWN CONTENT
|
||||||
@ -3770,3 +3723,49 @@ footer {
|
|||||||
border-top: 1px solid var(--color-border);
|
border-top: 1px solid var(--color-border);
|
||||||
margin: 2rem 0;
|
margin: 2rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===================================================================
|
||||||
|
26. ENHANCED AUDIT TRAIL STYLES
|
||||||
|
================================================================= */
|
||||||
|
|
||||||
|
.audit-process-flow {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phase-group {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phase-group:not(:last-child)::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 13px;
|
||||||
|
bottom: -8px;
|
||||||
|
width: 2px;
|
||||||
|
height: 16px;
|
||||||
|
background: linear-gradient(to bottom, var(--color-border) 0%, transparent 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-icon {
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover effects for audit entries */
|
||||||
|
.audit-trail-details .hover\\:bg-secondary:hover {
|
||||||
|
background-color: var(--color-bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments for audit trail */
|
||||||
|
@media (width <= 768px) {
|
||||||
|
.audit-process-flow .grid-cols-3 {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phase-group .flex {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -77,33 +77,8 @@ interface EnhancedCompressedToolsData {
|
|||||||
domains: any[];
|
domains: any[];
|
||||||
phases: any[];
|
phases: any[];
|
||||||
'domain-agnostic-software': any[];
|
'domain-agnostic-software': any[];
|
||||||
scenarios?: any[]; // Optional for AI processing
|
scenarios?: any[];
|
||||||
skill_levels: any;
|
skill_levels: any;
|
||||||
// Enhanced context for micro-tasks
|
|
||||||
domain_relationships: DomainRelationship[];
|
|
||||||
phase_dependencies: PhaseDependency[];
|
|
||||||
tool_compatibility_matrix: CompatibilityMatrix[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DomainRelationship {
|
|
||||||
domain_id: string;
|
|
||||||
tool_count: number;
|
|
||||||
common_tags: string[];
|
|
||||||
skill_distribution: Record<string, number>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PhaseDependency {
|
|
||||||
phase_id: string;
|
|
||||||
order: number;
|
|
||||||
depends_on: string | null;
|
|
||||||
enables: string | null;
|
|
||||||
is_parallel_capable: boolean;
|
|
||||||
typical_duration: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CompatibilityMatrix {
|
|
||||||
type: string;
|
|
||||||
groups: Record<string, string[]>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let cachedData: ToolsData | null = null;
|
let cachedData: ToolsData | null = null;
|
||||||
@ -146,104 +121,6 @@ function generateDataVersion(data: any): string {
|
|||||||
return Math.abs(hash).toString(36);
|
return Math.abs(hash).toString(36);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enhanced: Generate domain relationships for better AI understanding
|
|
||||||
function generateDomainRelationships(domains: any[], tools: any[]): DomainRelationship[] {
|
|
||||||
const relationships: DomainRelationship[] = [];
|
|
||||||
|
|
||||||
for (const domain of domains) {
|
|
||||||
const domainTools = tools.filter(tool =>
|
|
||||||
tool.domains && tool.domains.includes(domain.id)
|
|
||||||
);
|
|
||||||
|
|
||||||
const commonTags = domainTools
|
|
||||||
.flatMap(tool => tool.tags || [])
|
|
||||||
.reduce((acc: any, tag: string) => {
|
|
||||||
acc[tag] = (acc[tag] || 0) + 1;
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const topTags = Object.entries(commonTags)
|
|
||||||
.sort(([,a], [,b]) => (b as number) - (a as number))
|
|
||||||
.slice(0, 5)
|
|
||||||
.map(([tag]) => tag);
|
|
||||||
|
|
||||||
relationships.push({
|
|
||||||
domain_id: domain.id,
|
|
||||||
tool_count: domainTools.length,
|
|
||||||
common_tags: topTags,
|
|
||||||
skill_distribution: domainTools.reduce((acc: any, tool: any) => {
|
|
||||||
acc[tool.skillLevel] = (acc[tool.skillLevel] || 0) + 1;
|
|
||||||
return acc;
|
|
||||||
}, {})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return relationships;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enhanced: Generate phase dependencies
|
|
||||||
function generatePhaseDependencies(phases: any[]): PhaseDependency[] {
|
|
||||||
const dependencies: PhaseDependency[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < phases.length; i++) {
|
|
||||||
const phase = phases[i];
|
|
||||||
const nextPhase = phases[i + 1];
|
|
||||||
const prevPhase = phases[i - 1];
|
|
||||||
|
|
||||||
dependencies.push({
|
|
||||||
phase_id: phase.id,
|
|
||||||
order: i + 1,
|
|
||||||
depends_on: prevPhase?.id || null,
|
|
||||||
enables: nextPhase?.id || null,
|
|
||||||
is_parallel_capable: ['examination', 'analysis'].includes(phase.id), // Some phases can run in parallel
|
|
||||||
typical_duration: phase.id === 'data-collection' ? 'hours-days' :
|
|
||||||
phase.id === 'examination' ? 'hours-weeks' :
|
|
||||||
phase.id === 'analysis' ? 'days-weeks' :
|
|
||||||
'hours-days'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return dependencies;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enhanced: Generate tool compatibility matrix
|
|
||||||
function generateToolCompatibilityMatrix(tools: any[]): CompatibilityMatrix[] {
|
|
||||||
const matrix: CompatibilityMatrix[] = [];
|
|
||||||
|
|
||||||
// Group tools by common characteristics
|
|
||||||
const platformGroups = tools.reduce((acc: any, tool: any) => {
|
|
||||||
if (tool.platforms) {
|
|
||||||
tool.platforms.forEach((platform: string) => {
|
|
||||||
if (!acc[platform]) acc[platform] = [];
|
|
||||||
acc[platform].push(tool.name);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const phaseGroups = tools.reduce((acc: any, tool: any) => {
|
|
||||||
if (tool.phases) {
|
|
||||||
tool.phases.forEach((phase: string) => {
|
|
||||||
if (!acc[phase]) acc[phase] = [];
|
|
||||||
acc[phase].push(tool.name);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
matrix.push({
|
|
||||||
type: 'platform_compatibility',
|
|
||||||
groups: platformGroups
|
|
||||||
});
|
|
||||||
|
|
||||||
matrix.push({
|
|
||||||
type: 'phase_synergy',
|
|
||||||
groups: phaseGroups
|
|
||||||
});
|
|
||||||
|
|
||||||
return matrix;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadRawData(): Promise<ToolsData> {
|
async function loadRawData(): Promise<ToolsData> {
|
||||||
if (!cachedData) {
|
if (!cachedData) {
|
||||||
const yamlPath = path.join(process.cwd(), 'src/data/tools.yaml');
|
const yamlPath = path.join(process.cwd(), 'src/data/tools.yaml');
|
||||||
@ -253,7 +130,6 @@ async function loadRawData(): Promise<ToolsData> {
|
|||||||
try {
|
try {
|
||||||
cachedData = ToolsDataSchema.parse(rawData);
|
cachedData = ToolsDataSchema.parse(rawData);
|
||||||
|
|
||||||
// Enhanced: Add default skill level descriptions if not provided
|
|
||||||
if (!cachedData.skill_levels || Object.keys(cachedData.skill_levels).length === 0) {
|
if (!cachedData.skill_levels || Object.keys(cachedData.skill_levels).length === 0) {
|
||||||
cachedData.skill_levels = {
|
cachedData.skill_levels = {
|
||||||
novice: "Minimal technical background required, guided interfaces",
|
novice: "Minimal technical background required, guided interfaces",
|
||||||
@ -301,21 +177,18 @@ export async function getCompressedToolsDataForAI(): Promise<EnhancedCompressedT
|
|||||||
if (!cachedCompressedData) {
|
if (!cachedCompressedData) {
|
||||||
const data = await getToolsData();
|
const data = await getToolsData();
|
||||||
|
|
||||||
// Enhanced: More detailed tool information for micro-tasks
|
|
||||||
const compressedTools = data.tools
|
const compressedTools = data.tools
|
||||||
.filter(tool => tool.type !== 'concept')
|
.filter(tool => tool.type !== 'concept')
|
||||||
.map(tool => {
|
.map(tool => {
|
||||||
const { projectUrl, statusUrl, ...compressedTool } = tool;
|
const { projectUrl, statusUrl, ...compressedTool } = tool;
|
||||||
return {
|
return {
|
||||||
...compressedTool,
|
...compressedTool,
|
||||||
// Enhanced: Add computed fields for AI
|
|
||||||
is_hosted: projectUrl !== undefined && projectUrl !== null && projectUrl !== "" && projectUrl.trim() !== "",
|
is_hosted: projectUrl !== undefined && projectUrl !== null && projectUrl !== "" && projectUrl.trim() !== "",
|
||||||
is_open_source: tool.license && tool.license !== 'Proprietary',
|
is_open_source: tool.license && tool.license !== 'Proprietary',
|
||||||
complexity_score: tool.skillLevel === 'expert' ? 5 :
|
complexity_score: tool.skillLevel === 'expert' ? 5 :
|
||||||
tool.skillLevel === 'advanced' ? 4 :
|
tool.skillLevel === 'advanced' ? 4 :
|
||||||
tool.skillLevel === 'intermediate' ? 3 :
|
tool.skillLevel === 'intermediate' ? 3 :
|
||||||
tool.skillLevel === 'beginner' ? 2 : 1,
|
tool.skillLevel === 'beginner' ? 2 : 1,
|
||||||
// Enhanced: Phase-specific suitability hints
|
|
||||||
phase_suitability: tool.phases?.map(phase => ({
|
phase_suitability: tool.phases?.map(phase => ({
|
||||||
phase,
|
phase,
|
||||||
primary_use: tool.tags?.find(tag => tag.includes(phase)) ? 'primary' : 'secondary'
|
primary_use: tool.tags?.find(tag => tag.includes(phase)) ? 'primary' : 'secondary'
|
||||||
@ -329,7 +202,6 @@ export async function getCompressedToolsDataForAI(): Promise<EnhancedCompressedT
|
|||||||
const { projectUrl, statusUrl, platforms, accessType, license, ...compressedConcept } = concept;
|
const { projectUrl, statusUrl, platforms, accessType, license, ...compressedConcept } = concept;
|
||||||
return {
|
return {
|
||||||
...compressedConcept,
|
...compressedConcept,
|
||||||
// Enhanced: Learning difficulty indicator
|
|
||||||
learning_complexity: concept.skillLevel === 'expert' ? 'very_high' :
|
learning_complexity: concept.skillLevel === 'expert' ? 'very_high' :
|
||||||
concept.skillLevel === 'advanced' ? 'high' :
|
concept.skillLevel === 'advanced' ? 'high' :
|
||||||
concept.skillLevel === 'intermediate' ? 'medium' :
|
concept.skillLevel === 'intermediate' ? 'medium' :
|
||||||
@ -337,27 +209,16 @@ export async function getCompressedToolsDataForAI(): Promise<EnhancedCompressedT
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Enhanced: Add rich context data
|
|
||||||
const domainRelationships = generateDomainRelationships(data.domains, compressedTools);
|
|
||||||
const phaseDependencies = generatePhaseDependencies(data.phases);
|
|
||||||
const toolCompatibilityMatrix = generateToolCompatibilityMatrix(compressedTools);
|
|
||||||
|
|
||||||
cachedCompressedData = {
|
cachedCompressedData = {
|
||||||
tools: compressedTools,
|
tools: compressedTools,
|
||||||
concepts: concepts,
|
concepts: concepts,
|
||||||
domains: data.domains,
|
domains: data.domains,
|
||||||
phases: data.phases,
|
phases: data.phases,
|
||||||
'domain-agnostic-software': data['domain-agnostic-software'],
|
'domain-agnostic-software': data['domain-agnostic-software'],
|
||||||
scenarios: data.scenarios, // Include scenarios for context
|
scenarios: data.scenarios,
|
||||||
skill_levels: data.skill_levels || {},
|
skill_levels: data.skill_levels || {},
|
||||||
// Enhanced context for micro-tasks
|
|
||||||
domain_relationships: domainRelationships,
|
|
||||||
phase_dependencies: phaseDependencies,
|
|
||||||
tool_compatibility_matrix: toolCompatibilityMatrix
|
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(`[DATA SERVICE] Generated enhanced compressed data: ${compressedTools.length} tools, ${concepts.length} concepts`);
|
|
||||||
console.log(`[DATA SERVICE] Added context: ${domainRelationships.length} domain relationships, ${phaseDependencies.length} phase dependencies`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return cachedCompressedData;
|
return cachedCompressedData;
|
||||||
|
@ -24,9 +24,14 @@ interface EmbeddingsDatabase {
|
|||||||
embeddings: EmbeddingData[];
|
embeddings: EmbeddingData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SimilarityResult extends EmbeddingData {
|
||||||
|
similarity: number;
|
||||||
|
}
|
||||||
|
|
||||||
class EmbeddingsService {
|
class EmbeddingsService {
|
||||||
private embeddings: EmbeddingData[] = [];
|
private embeddings: EmbeddingData[] = [];
|
||||||
private isInitialized = false;
|
private isInitialized = false;
|
||||||
|
private initializationPromise: Promise<void> | null = null;
|
||||||
private readonly embeddingsPath = path.join(process.cwd(), 'data', 'embeddings.json');
|
private readonly embeddingsPath = path.join(process.cwd(), 'data', 'embeddings.json');
|
||||||
private readonly batchSize: number;
|
private readonly batchSize: number;
|
||||||
private readonly batchDelay: number;
|
private readonly batchDelay: number;
|
||||||
@ -39,6 +44,19 @@ class EmbeddingsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async initialize(): Promise<void> {
|
async initialize(): Promise<void> {
|
||||||
|
if (this.initializationPromise) {
|
||||||
|
return this.initializationPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isInitialized) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.initializationPromise = this.performInitialization();
|
||||||
|
return this.initializationPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async performInitialization(): Promise<void> {
|
||||||
if (!this.enabled) {
|
if (!this.enabled) {
|
||||||
console.log('[EMBEDDINGS] Embeddings disabled, skipping initialization');
|
console.log('[EMBEDDINGS] Embeddings disabled, skipping initialization');
|
||||||
return;
|
return;
|
||||||
@ -47,13 +65,11 @@ class EmbeddingsService {
|
|||||||
try {
|
try {
|
||||||
console.log('[EMBEDDINGS] Initializing embeddings system...');
|
console.log('[EMBEDDINGS] Initializing embeddings system...');
|
||||||
|
|
||||||
// Create data directory if it doesn't exist
|
|
||||||
await fs.mkdir(path.dirname(this.embeddingsPath), { recursive: true });
|
await fs.mkdir(path.dirname(this.embeddingsPath), { recursive: true });
|
||||||
|
|
||||||
const toolsData = await getCompressedToolsDataForAI();
|
const toolsData = await getCompressedToolsDataForAI();
|
||||||
const currentDataHash = this.hashData(toolsData);
|
const currentDataHash = this.hashData(toolsData);
|
||||||
|
|
||||||
// Try to load existing embeddings
|
|
||||||
const existingEmbeddings = await this.loadEmbeddings();
|
const existingEmbeddings = await this.loadEmbeddings();
|
||||||
|
|
||||||
if (existingEmbeddings && existingEmbeddings.version === currentDataHash) {
|
if (existingEmbeddings && existingEmbeddings.version === currentDataHash) {
|
||||||
@ -70,9 +86,29 @@ class EmbeddingsService {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[EMBEDDINGS] Failed to initialize:', error);
|
console.error('[EMBEDDINGS] Failed to initialize:', error);
|
||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
this.initializationPromise = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async waitForInitialization(): Promise<void> {
|
||||||
|
if (!this.enabled) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isInitialized) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.initializationPromise) {
|
||||||
|
await this.initializationPromise;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
private hashData(data: any): string {
|
private hashData(data: any): string {
|
||||||
return Buffer.from(JSON.stringify(data)).toString('base64').slice(0, 32);
|
return Buffer.from(JSON.stringify(data)).toString('base64').slice(0, 32);
|
||||||
}
|
}
|
||||||
@ -115,16 +151,21 @@ class EmbeddingsService {
|
|||||||
const apiKey = process.env.AI_EMBEDDINGS_API_KEY;
|
const apiKey = process.env.AI_EMBEDDINGS_API_KEY;
|
||||||
const model = process.env.AI_EMBEDDINGS_MODEL;
|
const model = process.env.AI_EMBEDDINGS_MODEL;
|
||||||
|
|
||||||
if (!endpoint || !apiKey || !model) {
|
if (!endpoint || !model) {
|
||||||
throw new Error('Missing embeddings API configuration');
|
throw new Error('Missing embeddings API configuration');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (apiKey) {
|
||||||
|
headers['Authorization'] = `Bearer ${apiKey}`;
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(endpoint, {
|
const response = await fetch(endpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers,
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': `Bearer ${apiKey}`
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model,
|
model,
|
||||||
input: contents
|
input: contents
|
||||||
@ -137,7 +178,16 @@ class EmbeddingsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data.data.map((item: any) => item.embedding);
|
|
||||||
|
if (Array.isArray(data.embeddings)) {
|
||||||
|
return data.embeddings;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(data.data)) {
|
||||||
|
return data.data.map((item: any) => item.embedding);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Unknown embeddings API response format');
|
||||||
}
|
}
|
||||||
|
|
||||||
private async generateEmbeddings(toolsData: any, version: string): Promise<void> {
|
private async generateEmbeddings(toolsData: any, version: string): Promise<void> {
|
||||||
@ -149,7 +199,6 @@ class EmbeddingsService {
|
|||||||
const contents = allItems.map(item => this.createContentString(item));
|
const contents = allItems.map(item => this.createContentString(item));
|
||||||
this.embeddings = [];
|
this.embeddings = [];
|
||||||
|
|
||||||
// Process in batches to respect rate limits
|
|
||||||
for (let i = 0; i < contents.length; i += this.batchSize) {
|
for (let i = 0; i < contents.length; i += this.batchSize) {
|
||||||
const batch = contents.slice(i, i + this.batchSize);
|
const batch = contents.slice(i, i + this.batchSize);
|
||||||
const batchItems = allItems.slice(i, i + this.batchSize);
|
const batchItems = allItems.slice(i, i + this.batchSize);
|
||||||
@ -177,7 +226,6 @@ class EmbeddingsService {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Rate limiting delay between batches
|
|
||||||
if (i + this.batchSize < contents.length) {
|
if (i + this.batchSize < contents.length) {
|
||||||
await new Promise(resolve => setTimeout(resolve, this.batchDelay));
|
await new Promise(resolve => setTimeout(resolve, this.batchDelay));
|
||||||
}
|
}
|
||||||
@ -192,7 +240,6 @@ class EmbeddingsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async embedText(text: string): Promise<number[]> {
|
public async embedText(text: string): Promise<number[]> {
|
||||||
// Re‑use the private batch helper to avoid auth duplication
|
|
||||||
const [embedding] = await this.generateEmbeddingsBatch([text.toLowerCase()]);
|
const [embedding] = await this.generateEmbeddingsBatch([text.toLowerCase()]);
|
||||||
return embedding;
|
return embedding;
|
||||||
}
|
}
|
||||||
@ -211,28 +258,56 @@ class EmbeddingsService {
|
|||||||
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
|
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
|
||||||
}
|
}
|
||||||
|
|
||||||
async findSimilar(query: string, maxResults: number = 30, threshold: number = 0.3): Promise<EmbeddingData[]> {
|
async findSimilar(query: string, maxResults: number = 30, threshold: number = 0.3): Promise<SimilarityResult[]> {
|
||||||
if (!this.enabled || !this.isInitialized || this.embeddings.length === 0) {
|
if (!this.enabled || !this.isInitialized || this.embeddings.length === 0) {
|
||||||
|
console.log('[EMBEDDINGS] Service not available for similarity search');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Generate embedding for query
|
|
||||||
const queryEmbeddings = await this.generateEmbeddingsBatch([query.toLowerCase()]);
|
const queryEmbeddings = await this.generateEmbeddingsBatch([query.toLowerCase()]);
|
||||||
const queryEmbedding = queryEmbeddings[0];
|
const queryEmbedding = queryEmbeddings[0];
|
||||||
|
|
||||||
// Calculate similarities
|
console.log(`[EMBEDDINGS] Computing similarities for ${this.embeddings.length} items`);
|
||||||
const similarities = this.embeddings.map(item => ({
|
|
||||||
|
const similarities: SimilarityResult[] = this.embeddings.map(item => ({
|
||||||
...item,
|
...item,
|
||||||
similarity: this.cosineSimilarity(queryEmbedding, item.embedding)
|
similarity: this.cosineSimilarity(queryEmbedding, item.embedding)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Filter by threshold and sort by similarity
|
const results = similarities
|
||||||
return similarities
|
|
||||||
.filter(item => item.similarity >= threshold)
|
.filter(item => item.similarity >= threshold)
|
||||||
.sort((a, b) => b.similarity - a.similarity)
|
.sort((a, b) => b.similarity - a.similarity)
|
||||||
.slice(0, maxResults);
|
.slice(0, maxResults);
|
||||||
|
|
||||||
|
const orderingValid = results.every((item, index) => {
|
||||||
|
if (index === 0) return true;
|
||||||
|
return item.similarity <= results[index - 1].similarity;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!orderingValid) {
|
||||||
|
console.error('[EMBEDDINGS] CRITICAL: Similarity ordering is broken!');
|
||||||
|
results.forEach((item, idx) => {
|
||||||
|
console.error(` ${idx}: ${item.name} = ${item.similarity.toFixed(4)}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[EMBEDDINGS] Found ${results.length} similar items (threshold: ${threshold})`);
|
||||||
|
if (results.length > 0) {
|
||||||
|
console.log('[EMBEDDINGS] Top 10 similarity matches:');
|
||||||
|
results.slice(0, 10).forEach((item, idx) => {
|
||||||
|
console.log(` ${idx + 1}. ${item.name} (${item.type}) = ${item.similarity.toFixed(4)}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
const topSimilarity = results[0].similarity;
|
||||||
|
const hasHigherSimilarity = results.some(item => item.similarity > topSimilarity);
|
||||||
|
if (hasHigherSimilarity) {
|
||||||
|
console.error('[EMBEDDINGS] CRITICAL: Top result is not actually the highest similarity!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[EMBEDDINGS] Failed to find similar items:', error);
|
console.error('[EMBEDDINGS] Failed to find similar items:', error);
|
||||||
return [];
|
return [];
|
||||||
@ -254,12 +329,10 @@ class EmbeddingsService {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Global instance
|
|
||||||
const embeddingsService = new EmbeddingsService();
|
const embeddingsService = new EmbeddingsService();
|
||||||
|
|
||||||
export { embeddingsService, type EmbeddingData };
|
export { embeddingsService, type EmbeddingData, type SimilarityResult };
|
||||||
|
|
||||||
// Auto-initialize on import in server environment
|
|
||||||
if (typeof window === 'undefined' && process.env.NODE_ENV !== 'test') {
|
if (typeof window === 'undefined' && process.env.NODE_ENV !== 'test') {
|
||||||
embeddingsService.initialize().catch(error => {
|
embeddingsService.initialize().catch(error => {
|
||||||
console.error('[EMBEDDINGS] Auto-initialization failed:', error);
|
console.error('[EMBEDDINGS] Auto-initialization failed:', error);
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
// src/utils/rateLimitedQueue.ts - FIXED: Memory leak and better cleanup
|
// src/utils/rateLimitedQueue.ts
|
||||||
|
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
const RATE_LIMIT_DELAY_MS = Number.parseInt(process.env.AI_RATE_LIMIT_DELAY_MS ?? "2000", 10) || 2000;
|
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>;
|
||||||
|
|
||||||
@ -12,7 +16,7 @@ interface QueuedTask {
|
|||||||
id: string;
|
id: string;
|
||||||
task: Task;
|
task: Task;
|
||||||
addedAt: number;
|
addedAt: number;
|
||||||
status: 'queued' | 'processing' | 'completed' | 'failed';
|
status: "queued" | "processing" | "completed" | "failed" | "timedout";
|
||||||
startedAt?: number;
|
startedAt?: number;
|
||||||
completedAt?: number;
|
completedAt?: number;
|
||||||
}
|
}
|
||||||
@ -29,11 +33,12 @@ class RateLimitedQueue {
|
|||||||
private tasks: QueuedTask[] = [];
|
private tasks: QueuedTask[] = [];
|
||||||
private isProcessing = false;
|
private isProcessing = false;
|
||||||
private delayMs = RATE_LIMIT_DELAY_MS;
|
private delayMs = RATE_LIMIT_DELAY_MS;
|
||||||
|
private taskTimeoutMs = TASK_TIMEOUT_MS;
|
||||||
private lastProcessedAt = 0;
|
private lastProcessedAt = 0;
|
||||||
private currentlyProcessingTaskId: string | null = null;
|
private currentlyProcessingTaskId: string | null = null;
|
||||||
|
|
||||||
private cleanupInterval: NodeJS.Timeout;
|
private cleanupInterval: NodeJS.Timeout;
|
||||||
private readonly TASK_RETENTION_MS = 30000;
|
private readonly TASK_RETENTION_MS = 300000; // 5 minutes
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.cleanupInterval = setInterval(() => {
|
this.cleanupInterval = setInterval(() => {
|
||||||
@ -45,12 +50,12 @@ class RateLimitedQueue {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const initialLength = this.tasks.length;
|
const initialLength = this.tasks.length;
|
||||||
|
|
||||||
this.tasks = this.tasks.filter(task => {
|
this.tasks = this.tasks.filter((task) => {
|
||||||
if (task.status === 'queued' || task.status === 'processing') {
|
if (task.status === "queued" || task.status === "processing") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (task.completedAt && (now - task.completedAt) > this.TASK_RETENTION_MS) {
|
if (task.completedAt && now - task.completedAt > this.TASK_RETENTION_MS) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,7 +91,7 @@ class RateLimitedQueue {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
addedAt: Date.now(),
|
addedAt: Date.now(),
|
||||||
status: 'queued'
|
status: "queued",
|
||||||
};
|
};
|
||||||
|
|
||||||
this.tasks.push(queuedTask);
|
this.tasks.push(queuedTask);
|
||||||
@ -98,8 +103,8 @@ class RateLimitedQueue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getStatus(taskId?: string): QueueStatus {
|
getStatus(taskId?: string): QueueStatus {
|
||||||
const queuedTasks = this.tasks.filter(t => t.status === 'queued');
|
const queuedTasks = this.tasks.filter((t) => t.status === "queued");
|
||||||
const processingTasks = this.tasks.filter(t => t.status === 'processing');
|
const processingTasks = this.tasks.filter((t) => t.status === "processing");
|
||||||
const queueLength = queuedTasks.length + processingTasks.length;
|
const queueLength = queuedTasks.length + processingTasks.length;
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@ -118,23 +123,23 @@ class RateLimitedQueue {
|
|||||||
const status: QueueStatus = {
|
const status: QueueStatus = {
|
||||||
queueLength,
|
queueLength,
|
||||||
isProcessing: this.isProcessing,
|
isProcessing: this.isProcessing,
|
||||||
estimatedWaitTime
|
estimatedWaitTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (taskId) {
|
if (taskId) {
|
||||||
const task = this.tasks.find(t => t.id === taskId);
|
const task = this.tasks.find((t) => t.id === taskId);
|
||||||
|
|
||||||
if (task) {
|
if (task) {
|
||||||
status.taskStatus = task.status;
|
status.taskStatus = task.status;
|
||||||
|
|
||||||
if (task.status === 'processing') {
|
if (task.status === "processing") {
|
||||||
status.currentPosition = 1;
|
status.currentPosition = 1;
|
||||||
} else if (task.status === 'queued') {
|
} else if (task.status === "queued") {
|
||||||
const queuedTasksInOrder = this.tasks
|
const queuedTasksInOrder = this.tasks
|
||||||
.filter(t => t.status === 'queued')
|
.filter((t) => t.status === "queued")
|
||||||
.sort((a, b) => a.addedAt - b.addedAt);
|
.sort((a, b) => a.addedAt - b.addedAt);
|
||||||
|
|
||||||
const positionInQueue = queuedTasksInOrder.findIndex(t => t.id === taskId);
|
const positionInQueue = queuedTasksInOrder.findIndex((t) => t.id === taskId);
|
||||||
|
|
||||||
if (positionInQueue >= 0) {
|
if (positionInQueue >= 0) {
|
||||||
const processingOffset = processingTasks.length > 0 ? 1 : 0;
|
const processingOffset = processingTasks.length > 0 ? 1 : 0;
|
||||||
@ -145,11 +150,7 @@ class RateLimitedQueue {
|
|||||||
const taskTimestamp = taskId.match(/ai_(\d+)_/)?.[1];
|
const taskTimestamp = taskId.match(/ai_(\d+)_/)?.[1];
|
||||||
if (taskTimestamp) {
|
if (taskTimestamp) {
|
||||||
const taskAge = now - parseInt(taskTimestamp);
|
const taskAge = now - parseInt(taskTimestamp);
|
||||||
if (taskAge < 30000) {
|
status.taskStatus = taskAge < 300000 ? "starting" : "unknown";
|
||||||
status.taskStatus = 'starting';
|
|
||||||
} else {
|
|
||||||
status.taskStatus = 'unknown';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -157,51 +158,48 @@ class RateLimitedQueue {
|
|||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
setDelay(ms: number): void {
|
|
||||||
if (!Number.isFinite(ms) || ms < 0) return;
|
|
||||||
this.delayMs = ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
getDelay(): number {
|
|
||||||
return this.delayMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async processQueue(): Promise<void> {
|
private async processQueue(): Promise<void> {
|
||||||
if (this.isProcessing) {
|
if (this.isProcessing) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isProcessing = true;
|
this.isProcessing = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (true) {
|
while (true) {
|
||||||
const nextTask = this.tasks
|
const nextTask = this.tasks
|
||||||
.filter(t => t.status === 'queued')
|
.filter((t) => t.status === "queued")
|
||||||
.sort((a, b) => a.addedAt - b.addedAt)[0];
|
.sort((a, b) => a.addedAt - b.addedAt)[0];
|
||||||
|
|
||||||
if (!nextTask) {
|
if (!nextTask) break;
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
nextTask.status = 'processing';
|
nextTask.status = "processing";
|
||||||
nextTask.startedAt = Date.now();
|
nextTask.startedAt = Date.now();
|
||||||
this.currentlyProcessingTaskId = nextTask.id;
|
this.currentlyProcessingTaskId = nextTask.id;
|
||||||
this.lastProcessedAt = Date.now();
|
this.lastProcessedAt = Date.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await nextTask.task();
|
await Promise.race([
|
||||||
nextTask.status = 'completed';
|
nextTask.task(),
|
||||||
|
new Promise((_, reject) =>
|
||||||
|
setTimeout(
|
||||||
|
() => reject(new Error(`Task ${nextTask.id} timed out after ${this.taskTimeoutMs} ms`)),
|
||||||
|
this.taskTimeoutMs,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
nextTask.status = "completed";
|
||||||
nextTask.completedAt = Date.now();
|
nextTask.completedAt = Date.now();
|
||||||
console.log(`[QUEUE] Task ${nextTask.id} completed`);
|
console.log(`[QUEUE] Task ${nextTask.id} completed`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
nextTask.status = 'failed';
|
const err = error as Error;
|
||||||
|
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);
|
console.error(`[QUEUE] Task ${nextTask.id} failed:`, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
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`);
|
console.log(`[QUEUE] Waiting ${this.delayMs}ms before next task`);
|
||||||
await new Promise((r) => setTimeout(r, this.delayMs));
|
await new Promise((r) => setTimeout(r, this.delayMs));
|
||||||
|
@ -1,8 +1,3 @@
|
|||||||
/**
|
|
||||||
* CONSOLIDATED Tool utility functions for consistent tool operations across the app
|
|
||||||
* Works in both server (Node.js) and client (browser) environments
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface Tool {
|
export interface Tool {
|
||||||
name: string;
|
name: string;
|
||||||
type?: 'software' | 'method' | 'concept';
|
type?: 'software' | 'method' | 'concept';
|
||||||
@ -18,10 +13,6 @@ export interface Tool {
|
|||||||
related_concepts?: string[];
|
related_concepts?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a URL-safe slug from a tool name
|
|
||||||
* Used for URLs, IDs, and file names consistently across the app
|
|
||||||
*/
|
|
||||||
export function createToolSlug(toolName: string): string {
|
export function createToolSlug(toolName: string): string {
|
||||||
if (!toolName || typeof toolName !== 'string') {
|
if (!toolName || typeof toolName !== 'string') {
|
||||||
console.warn('[toolHelpers] Invalid toolName provided to createToolSlug:', toolName);
|
console.warn('[toolHelpers] Invalid toolName provided to createToolSlug:', toolName);
|
||||||
@ -35,9 +26,6 @@ export function createToolSlug(toolName: string): string {
|
|||||||
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
|
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds a tool by name or slug from tools array
|
|
||||||
*/
|
|
||||||
export function findToolByIdentifier(tools: Tool[], identifier: string): Tool | undefined {
|
export function findToolByIdentifier(tools: Tool[], identifier: string): Tool | undefined {
|
||||||
if (!identifier || !Array.isArray(tools)) return undefined;
|
if (!identifier || !Array.isArray(tools)) return undefined;
|
||||||
|
|
||||||
@ -47,23 +35,9 @@ export function findToolByIdentifier(tools: Tool[], identifier: string): Tool |
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if tool has a valid project URL (hosted on CC24 server)
|
|
||||||
*/
|
|
||||||
export function isToolHosted(tool: Tool): boolean {
|
export function isToolHosted(tool: Tool): boolean {
|
||||||
return tool.projectUrl !== undefined &&
|
return tool.projectUrl !== undefined &&
|
||||||
tool.projectUrl !== null &&
|
tool.projectUrl !== null &&
|
||||||
tool.projectUrl !== "" &&
|
tool.projectUrl !== "" &&
|
||||||
tool.projectUrl.trim() !== "";
|
tool.projectUrl.trim() !== "";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines tool category for styling/logic
|
|
||||||
*/
|
|
||||||
export function getToolCategory(tool: Tool): 'concept' | 'method' | 'hosted' | 'oss' | 'proprietary' {
|
|
||||||
if (tool.type === 'concept') return 'concept';
|
|
||||||
if (tool.type === 'method') return 'method';
|
|
||||||
if (isToolHosted(tool)) return 'hosted';
|
|
||||||
if (tool.license && tool.license !== 'Proprietary') return 'oss';
|
|
||||||
return 'proprietary';
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user