update ai stuff
This commit is contained in:
		
							parent
							
								
									073f52bada
								
							
						
					
					
						commit
						89f45b85be
					
				@ -0,0 +1,556 @@
 | 
				
			|||||||
 | 
					---
 | 
				
			||||||
 | 
					// src/components/AIQueryInterface.astro
 | 
				
			||||||
 | 
					import { promises as fs } from 'fs';
 | 
				
			||||||
 | 
					import { load } from 'js-yaml';
 | 
				
			||||||
 | 
					import path from 'path';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Load tools data for tool details
 | 
				
			||||||
 | 
					const yamlPath = path.join(process.cwd(), 'src/data/tools.yaml');
 | 
				
			||||||
 | 
					const yamlContent = await fs.readFile(yamlPath, 'utf8');
 | 
				
			||||||
 | 
					const data = load(yamlContent) as any;
 | 
				
			||||||
 | 
					const tools = data.tools;
 | 
				
			||||||
 | 
					const phases = data.phases.filter((phase: any) => phase.id !== 'collaboration-general');
 | 
				
			||||||
 | 
					---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div id="ai-interface" class="ai-interface" style="display: none;">
 | 
				
			||||||
 | 
					  <!-- AI Query Input Section -->
 | 
				
			||||||
 | 
					  <div class="ai-query-section">
 | 
				
			||||||
 | 
					    <div style="text-align: center; margin-bottom: 2rem;">
 | 
				
			||||||
 | 
					      <h2 style="margin-bottom: 1rem; color: var(--color-primary);">
 | 
				
			||||||
 | 
					        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem; vertical-align: text-top;">
 | 
				
			||||||
 | 
					          <path d="M9 11H5a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7a2 2 0 0 0-2-2h-4"/>
 | 
				
			||||||
 | 
					          <path d="M9 11V7a3 3 0 0 1 6 0v4"/>
 | 
				
			||||||
 | 
					        </svg>
 | 
				
			||||||
 | 
					        KI-gestützte Tool-Empfehlungen
 | 
				
			||||||
 | 
					      </h2>
 | 
				
			||||||
 | 
					      <p class="text-muted" style="max-width: 600px; margin: 0 auto; line-height: 1.6;">
 | 
				
			||||||
 | 
					        Beschreiben Sie Ihr Ermittlungsszenario auf Deutsch oder Englisch und erhalten Sie 
 | 
				
			||||||
 | 
					        personalisierte Tool-Empfehlungen basierend auf dem NIST-Framework.
 | 
				
			||||||
 | 
					      </p>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="ai-input-container" style="max-width: 800px; margin: 0 auto 2rem;">
 | 
				
			||||||
 | 
					      <div style="position: relative;">
 | 
				
			||||||
 | 
					        <textarea 
 | 
				
			||||||
 | 
					          id="ai-query-input"
 | 
				
			||||||
 | 
					          placeholder="Beschreiben Sie Ihr Ermittlungsszenario... 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Beispiele:
 | 
				
			||||||
 | 
					• Ich untersuche einen Ransomware-Angriff auf Windows-Systeme und brauche Tools für Memory-Analyse
 | 
				
			||||||
 | 
					• We need to investigate suspicious network traffic from an IoT device
 | 
				
			||||||
 | 
					• Ein Smartphone wurde kompromittiert - welche Tools für Mobile Forensik?"
 | 
				
			||||||
 | 
					          style="width: 100%; min-height: 120px; padding: 1rem; border: 2px solid var(--color-border); border-radius: 0.5rem; resize: vertical; font-family: inherit; font-size: 0.875rem; line-height: 1.5;"
 | 
				
			||||||
 | 
					          maxlength="2000"
 | 
				
			||||||
 | 
					        ></textarea>
 | 
				
			||||||
 | 
					        <div style="position: absolute; bottom: 0.75rem; right: 0.75rem; font-size: 0.75rem; color: var(--color-text-secondary);">
 | 
				
			||||||
 | 
					          <span id="char-count">0</span>/2000
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      <div style="display: flex; gap: 1rem; margin-top: 1rem; justify-content: center;">
 | 
				
			||||||
 | 
					        <button id="ai-submit-btn" class="btn btn-primary" style="padding: 0.75rem 2rem;">
 | 
				
			||||||
 | 
					          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
 | 
				
			||||||
 | 
					            <path d="M22 2L11 13"/>
 | 
				
			||||||
 | 
					            <path d="M22 2l-7 20-4-9-9-4 20-7z"/>
 | 
				
			||||||
 | 
					          </svg>
 | 
				
			||||||
 | 
					          Empfehlungen abrufen
 | 
				
			||||||
 | 
					        </button>
 | 
				
			||||||
 | 
					        <button id="ai-clear-btn" class="btn btn-secondary">
 | 
				
			||||||
 | 
					          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
 | 
				
			||||||
 | 
					            <path d="M3 6h18"/>
 | 
				
			||||||
 | 
					            <path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/>
 | 
				
			||||||
 | 
					            <path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>
 | 
				
			||||||
 | 
					          </svg>
 | 
				
			||||||
 | 
					          Zurücksetzen
 | 
				
			||||||
 | 
					        </button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- Loading State -->
 | 
				
			||||||
 | 
					    <div id="ai-loading" class="ai-loading" style="display: none; text-align: center; padding: 2rem;">
 | 
				
			||||||
 | 
					      <div class="loading-spinner" style="margin-bottom: 1rem;">
 | 
				
			||||||
 | 
					        <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" stroke-width="2" style="animation: spin 1s linear infinite;">
 | 
				
			||||||
 | 
					          <path d="M12 2v4"/>
 | 
				
			||||||
 | 
					          <path d="M12 18v4"/>
 | 
				
			||||||
 | 
					          <path d="M4.93 4.93l2.83 2.83"/>
 | 
				
			||||||
 | 
					          <path d="M16.24 16.24l2.83 2.83"/>
 | 
				
			||||||
 | 
					          <path d="M2 12h4"/>
 | 
				
			||||||
 | 
					          <path d="M18 12h4"/>
 | 
				
			||||||
 | 
					          <path d="M4.93 19.07l2.83-2.83"/>
 | 
				
			||||||
 | 
					          <path d="M16.24 7.76l2.83-2.83"/>
 | 
				
			||||||
 | 
					        </svg>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <h3 style="margin-bottom: 0.5rem; color: var(--color-primary);">KI analysiert Ihr Szenario...</h3>
 | 
				
			||||||
 | 
					      <p class="text-muted">Dies kann einige Sekunden dauern.</p>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- Error State -->
 | 
				
			||||||
 | 
					    <div id="ai-error" class="ai-error" style="display: none; text-align: center; padding: 2rem;">
 | 
				
			||||||
 | 
					      <div style="margin-bottom: 1rem;">
 | 
				
			||||||
 | 
					        <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="var(--color-error)" stroke-width="2">
 | 
				
			||||||
 | 
					          <circle cx="12" cy="12" r="10"/>
 | 
				
			||||||
 | 
					          <line x1="15" y1="9" x2="9" y2="15"/>
 | 
				
			||||||
 | 
					          <line x1="9" y1="9" x2="15" y2="15"/>
 | 
				
			||||||
 | 
					        </svg>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <h3 style="margin-bottom: 0.5rem; color: var(--color-error);">Fehler bei der Analyse</h3>
 | 
				
			||||||
 | 
					      <p class="text-muted" id="ai-error-message">Ein unerwarteter Fehler ist aufgetreten.</p>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <!-- AI Results Section -->
 | 
				
			||||||
 | 
					  <div id="ai-results" class="ai-results" style="display: none;">
 | 
				
			||||||
 | 
					    <!-- Scenario Analysis -->
 | 
				
			||||||
 | 
					    <div class="card" style="margin-bottom: 2rem; border-left: 4px solid var(--color-primary);">
 | 
				
			||||||
 | 
					      <h3 style="margin-bottom: 1rem; color: var(--color-primary);">
 | 
				
			||||||
 | 
					        <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
 | 
				
			||||||
 | 
					          <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>
 | 
				
			||||||
 | 
					        Szenario-Analyse
 | 
				
			||||||
 | 
					      </h3>
 | 
				
			||||||
 | 
					      <div id="scenario-analysis" style="line-height: 1.7;"></div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- Workflow Visualization -->
 | 
				
			||||||
 | 
					    <div style="margin-bottom: 2rem;">
 | 
				
			||||||
 | 
					      <h3 style="margin-bottom: 1.5rem; text-align: center; color: var(--color-text);">
 | 
				
			||||||
 | 
					        Empfohlener DFIR-Workflow
 | 
				
			||||||
 | 
					      </h3>
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      <!-- Phase Flow -->
 | 
				
			||||||
 | 
					      <div class="workflow-container">
 | 
				
			||||||
 | 
					        {phases.map((phase: any, index: number) => (
 | 
				
			||||||
 | 
					          <div class="workflow-phase" data-phase={phase.id}>
 | 
				
			||||||
 | 
					            <div class="phase-header">
 | 
				
			||||||
 | 
					              <div class="phase-number">{index + 1}</div>
 | 
				
			||||||
 | 
					              <div class="phase-info">
 | 
				
			||||||
 | 
					                <h4 class="phase-title">{phase.name}</h4>
 | 
				
			||||||
 | 
					                <div class="phase-tools" id={`phase-tools-${phase.id}`}>
 | 
				
			||||||
 | 
					                  <!-- Tools will be populated by JavaScript -->
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            {index < phases.length - 1 && (
 | 
				
			||||||
 | 
					              <div class="workflow-arrow">
 | 
				
			||||||
 | 
					                <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" stroke-width="2">
 | 
				
			||||||
 | 
					                  <path d="M5 12h14"/>
 | 
				
			||||||
 | 
					                  <path d="M12 5l7 7-7 7"/>
 | 
				
			||||||
 | 
					                </svg>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        ))}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- Additional Information -->
 | 
				
			||||||
 | 
					    <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
 | 
				
			||||||
 | 
					      <div class="card">
 | 
				
			||||||
 | 
					        <h4 style="margin-bottom: 1rem; color: var(--color-accent);">
 | 
				
			||||||
 | 
					          <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
 | 
				
			||||||
 | 
					            <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
 | 
				
			||||||
 | 
					          </svg>
 | 
				
			||||||
 | 
					          Workflow-Empfehlung
 | 
				
			||||||
 | 
					        </h4>
 | 
				
			||||||
 | 
					        <div id="workflow-suggestion" style="line-height: 1.6;"></div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      <div class="card">
 | 
				
			||||||
 | 
					        <h4 style="margin-bottom: 1rem; color: var(--color-warning);">
 | 
				
			||||||
 | 
					          <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
 | 
				
			||||||
 | 
					            <circle cx="12" cy="12" r="10"/>
 | 
				
			||||||
 | 
					            <path d="M12 16v-4"/>
 | 
				
			||||||
 | 
					            <path d="M12 8h.01"/>
 | 
				
			||||||
 | 
					          </svg>
 | 
				
			||||||
 | 
					          Wichtige Hinweise
 | 
				
			||||||
 | 
					        </h4>
 | 
				
			||||||
 | 
					        <div id="additional-notes" style="line-height: 1.6;"></div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- New Query Button -->
 | 
				
			||||||
 | 
					    <div style="text-align: center; margin-top: 2rem;">
 | 
				
			||||||
 | 
					      <button id="new-query-btn" class="btn btn-secondary">
 | 
				
			||||||
 | 
					        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
 | 
				
			||||||
 | 
					          <path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/>
 | 
				
			||||||
 | 
					          <path d="M21 3v5h-5"/>
 | 
				
			||||||
 | 
					          <path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/>
 | 
				
			||||||
 | 
					          <path d="M3 21v-5h5"/>
 | 
				
			||||||
 | 
					        </svg>
 | 
				
			||||||
 | 
					        Neue Anfrage starten
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
					/* Loading spinner animation */
 | 
				
			||||||
 | 
					@keyframes spin {
 | 
				
			||||||
 | 
					  from { transform: rotate(0deg); }
 | 
				
			||||||
 | 
					  to { transform: rotate(360deg); }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Workflow styling */
 | 
				
			||||||
 | 
					.workflow-container {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  gap: 1rem;
 | 
				
			||||||
 | 
					  max-width: 900px;
 | 
				
			||||||
 | 
					  margin: 0 auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.workflow-phase {
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.phase-header {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: flex-start;
 | 
				
			||||||
 | 
					  gap: 1rem;
 | 
				
			||||||
 | 
					  padding: 1.5rem;
 | 
				
			||||||
 | 
					  background-color: var(--color-bg);
 | 
				
			||||||
 | 
					  border: 2px solid var(--color-border);
 | 
				
			||||||
 | 
					  border-radius: 0.75rem;
 | 
				
			||||||
 | 
					  transition: all 0.3s ease;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.phase-header:hover {
 | 
				
			||||||
 | 
					  border-color: var(--color-primary);
 | 
				
			||||||
 | 
					  box-shadow: var(--shadow-md);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.phase-number {
 | 
				
			||||||
 | 
					  background-color: var(--color-primary);
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					  width: 2.5rem;
 | 
				
			||||||
 | 
					  height: 2.5rem;
 | 
				
			||||||
 | 
					  border-radius: 50%;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  font-weight: 600;
 | 
				
			||||||
 | 
					  font-size: 1.125rem;
 | 
				
			||||||
 | 
					  flex-shrink: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.phase-info {
 | 
				
			||||||
 | 
					  flex: 1;
 | 
				
			||||||
 | 
					  min-width: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.phase-title {
 | 
				
			||||||
 | 
					  margin: 0 0 1rem 0;
 | 
				
			||||||
 | 
					  color: var(--color-text);
 | 
				
			||||||
 | 
					  font-size: 1.25rem;
 | 
				
			||||||
 | 
					  font-weight: 600;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.phase-tools {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  gap: 0.75rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.workflow-arrow {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  margin: 0.5rem 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Tool recommendation cards */
 | 
				
			||||||
 | 
					.tool-recommendation {
 | 
				
			||||||
 | 
					  background-color: var(--color-bg-secondary);
 | 
				
			||||||
 | 
					  border: 1px solid var(--color-border);
 | 
				
			||||||
 | 
					  border-radius: 0.5rem;
 | 
				
			||||||
 | 
					  padding: 1rem;
 | 
				
			||||||
 | 
					  transition: all 0.2s ease;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tool-recommendation:hover {
 | 
				
			||||||
 | 
					  border-color: var(--color-primary);
 | 
				
			||||||
 | 
					  box-shadow: var(--shadow-sm);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tool-recommendation.hosted {
 | 
				
			||||||
 | 
					  background-color: var(--color-hosted-bg);
 | 
				
			||||||
 | 
					  border-color: var(--color-hosted);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tool-recommendation.oss {
 | 
				
			||||||
 | 
					  background-color: var(--color-oss-bg);
 | 
				
			||||||
 | 
					  border-color: var(--color-oss);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tool-rec-header {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: space-between;
 | 
				
			||||||
 | 
					  align-items: start;
 | 
				
			||||||
 | 
					  margin-bottom: 0.75rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tool-rec-name {
 | 
				
			||||||
 | 
					  font-weight: 600;
 | 
				
			||||||
 | 
					  font-size: 1rem;
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					  color: var(--color-text);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tool-rec-priority {
 | 
				
			||||||
 | 
					  font-size: 0.75rem;
 | 
				
			||||||
 | 
					  padding: 0.25rem 0.5rem;
 | 
				
			||||||
 | 
					  border-radius: 1rem;
 | 
				
			||||||
 | 
					  font-weight: 500;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tool-rec-priority.high {
 | 
				
			||||||
 | 
					  background-color: var(--color-error);
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tool-rec-priority.medium {
 | 
				
			||||||
 | 
					  background-color: var(--color-warning);
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tool-rec-priority.low {
 | 
				
			||||||
 | 
					  background-color: var(--color-accent);
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tool-rec-justification {
 | 
				
			||||||
 | 
					  font-size: 0.875rem;
 | 
				
			||||||
 | 
					  line-height: 1.5;
 | 
				
			||||||
 | 
					  color: var(--color-text-secondary);
 | 
				
			||||||
 | 
					  margin-bottom: 0.75rem;
 | 
				
			||||||
 | 
					  font-style: italic;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tool-rec-metadata {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-wrap: wrap;
 | 
				
			||||||
 | 
					  gap: 0.5rem;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  font-size: 0.75rem;
 | 
				
			||||||
 | 
					  color: var(--color-text-secondary);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Responsive design */
 | 
				
			||||||
 | 
					@media (max-width: 768px) {
 | 
				
			||||||
 | 
					  .workflow-container {
 | 
				
			||||||
 | 
					    gap: 0.75rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .phase-header {
 | 
				
			||||||
 | 
					    padding: 1rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .phase-number {
 | 
				
			||||||
 | 
					    width: 2rem;
 | 
				
			||||||
 | 
					    height: 2rem;
 | 
				
			||||||
 | 
					    font-size: 1rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .phase-title {
 | 
				
			||||||
 | 
					    font-size: 1.125rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .tool-rec-header {
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    gap: 0.5rem;
 | 
				
			||||||
 | 
					    align-items: start;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script define:vars={{ toolsData: tools }}>
 | 
				
			||||||
 | 
					// Global tool data for quick lookup
 | 
				
			||||||
 | 
					window.aiToolsData = toolsData;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Authentication check function
 | 
				
			||||||
 | 
					async function checkAuthentication() {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const response = await fetch('/api/auth/status');
 | 
				
			||||||
 | 
					    const data = await response.json();
 | 
				
			||||||
 | 
					    return data.authenticated;
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error('Auth check failed:', error);
 | 
				
			||||||
 | 
					    return false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Initialize AI interface
 | 
				
			||||||
 | 
					document.addEventListener('DOMContentLoaded', () => {
 | 
				
			||||||
 | 
					  const queryInput = document.getElementById('ai-query-input');
 | 
				
			||||||
 | 
					  const submitBtn = document.getElementById('ai-submit-btn');
 | 
				
			||||||
 | 
					  const clearBtn = document.getElementById('ai-clear-btn');
 | 
				
			||||||
 | 
					  const charCount = document.getElementById('char-count');
 | 
				
			||||||
 | 
					  const loadingEl = document.getElementById('ai-loading');
 | 
				
			||||||
 | 
					  const errorEl = document.getElementById('ai-error');
 | 
				
			||||||
 | 
					  const resultsEl = document.getElementById('ai-results');
 | 
				
			||||||
 | 
					  const newQueryBtn = document.getElementById('new-query-btn');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Character counter
 | 
				
			||||||
 | 
					  if (queryInput && charCount) {
 | 
				
			||||||
 | 
					    queryInput.addEventListener('input', () => {
 | 
				
			||||||
 | 
					      charCount.textContent = queryInput.value.length;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Submit query
 | 
				
			||||||
 | 
					  if (submitBtn) {
 | 
				
			||||||
 | 
					    submitBtn.addEventListener('click', async () => {
 | 
				
			||||||
 | 
					      const query = queryInput?.value?.trim();
 | 
				
			||||||
 | 
					      if (!query) {
 | 
				
			||||||
 | 
					        alert('Bitte geben Sie eine Beschreibung Ihres Ermittlungsszenarios ein.');
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Check authentication
 | 
				
			||||||
 | 
					      const isAuthenticated = await checkAuthentication();
 | 
				
			||||||
 | 
					      if (!isAuthenticated) {
 | 
				
			||||||
 | 
					        // Redirect to login
 | 
				
			||||||
 | 
					        window.location.href = `/api/auth/login?returnTo=${encodeURIComponent(window.location.pathname)}`;
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Show loading state
 | 
				
			||||||
 | 
					      showLoadingState();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const response = await fetch('/api/ai/query', {
 | 
				
			||||||
 | 
					          method: 'POST',
 | 
				
			||||||
 | 
					          headers: {
 | 
				
			||||||
 | 
					            'Content-Type': 'application/json',
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          body: JSON.stringify({ query }),
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const data = await response.json();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (data.success) {
 | 
				
			||||||
 | 
					          showResults(data.recommendation);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          showError(data.error || 'Unbekannter Fehler');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        console.error('AI query failed:', error);
 | 
				
			||||||
 | 
					        showError('Verbindungsfehler. Bitte versuchen Sie es erneut.');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Clear button
 | 
				
			||||||
 | 
					  if (clearBtn) {
 | 
				
			||||||
 | 
					    clearBtn.addEventListener('click', () => {
 | 
				
			||||||
 | 
					      queryInput.value = '';
 | 
				
			||||||
 | 
					      charCount.textContent = '0';
 | 
				
			||||||
 | 
					      hideAllStates();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // New query button
 | 
				
			||||||
 | 
					  if (newQueryBtn) {
 | 
				
			||||||
 | 
					    newQueryBtn.addEventListener('click', () => {
 | 
				
			||||||
 | 
					      hideAllStates();
 | 
				
			||||||
 | 
					      queryInput?.focus();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // State management functions
 | 
				
			||||||
 | 
					  function showLoadingState() {
 | 
				
			||||||
 | 
					    hideAllStates();
 | 
				
			||||||
 | 
					    loadingEl.style.display = 'block';
 | 
				
			||||||
 | 
					    submitBtn.disabled = true;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function showError(message) {
 | 
				
			||||||
 | 
					    hideAllStates();
 | 
				
			||||||
 | 
					    errorEl.style.display = 'block';
 | 
				
			||||||
 | 
					    document.getElementById('ai-error-message').textContent = message;
 | 
				
			||||||
 | 
					    submitBtn.disabled = false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function showResults(recommendation) {
 | 
				
			||||||
 | 
					    hideAllStates();
 | 
				
			||||||
 | 
					    populateResults(recommendation);
 | 
				
			||||||
 | 
					    resultsEl.style.display = 'block';
 | 
				
			||||||
 | 
					    submitBtn.disabled = false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function hideAllStates() {
 | 
				
			||||||
 | 
					    loadingEl.style.display = 'none';
 | 
				
			||||||
 | 
					    errorEl.style.display = 'none';
 | 
				
			||||||
 | 
					    resultsEl.style.display = 'none';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Populate results
 | 
				
			||||||
 | 
					  function populateResults(recommendation) {
 | 
				
			||||||
 | 
					    // Scenario analysis
 | 
				
			||||||
 | 
					    document.getElementById('scenario-analysis').textContent = recommendation.scenario_analysis || 'Keine Analyse verfügbar.';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Workflow suggestion
 | 
				
			||||||
 | 
					    document.getElementById('workflow-suggestion').textContent = recommendation.workflow_suggestion || 'Keine Workflow-Empfehlung verfügbar.';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Additional notes
 | 
				
			||||||
 | 
					    document.getElementById('additional-notes').textContent = recommendation.additional_notes || 'Keine zusätzlichen Hinweise verfügbar.';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Populate phase tools
 | 
				
			||||||
 | 
					    populatePhaseTools(recommendation.recommended_tools || []);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Populate tools by phase
 | 
				
			||||||
 | 
					  function populatePhaseTools(recommendedTools) {
 | 
				
			||||||
 | 
					    const phases = ['data-collection', 'examination', 'analysis', 'reporting'];
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    phases.forEach(phaseId => {
 | 
				
			||||||
 | 
					      const container = document.getElementById(`phase-tools-${phaseId}`);
 | 
				
			||||||
 | 
					      if (!container) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const phaseTools = recommendedTools.filter(tool => tool.phase === phaseId);
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      if (phaseTools.length === 0) {
 | 
				
			||||||
 | 
					        container.innerHTML = '<p class="text-muted" style="font-size: 0.875rem; margin: 0; font-style: italic;">Keine spezifischen Tools für diese Phase empfohlen.</p>';
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      container.innerHTML = phaseTools.map(recTool => {
 | 
				
			||||||
 | 
					        const toolData = window.aiToolsData.find(t => t.name === recTool.name);
 | 
				
			||||||
 | 
					        if (!toolData) return '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const hasValidProjectUrl = toolData.projectUrl && toolData.projectUrl.trim() !== '';
 | 
				
			||||||
 | 
					        const isOSS = toolData.license !== 'Proprietary';
 | 
				
			||||||
 | 
					        const hasKnowledgebase = toolData.knowledgebase === true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const cardClass = hasValidProjectUrl ? 'hosted' : (isOSS ? 'oss' : '');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return `
 | 
				
			||||||
 | 
					          <div class="tool-recommendation ${cardClass}" onclick="window.showToolDetails('${toolData.name}')">
 | 
				
			||||||
 | 
					            <div class="tool-rec-header">
 | 
				
			||||||
 | 
					              <h5 class="tool-rec-name">${toolData.name}</h5>
 | 
				
			||||||
 | 
					              <span class="tool-rec-priority ${recTool.priority}">${recTool.priority.toUpperCase()}</span>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            <div class="tool-rec-justification">
 | 
				
			||||||
 | 
					              "${recTool.justification}"
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            <div class="tool-rec-metadata">
 | 
				
			||||||
 | 
					              <span>💻 ${(toolData.platforms || []).join(', ')}</span>
 | 
				
			||||||
 | 
					              <span>•</span>
 | 
				
			||||||
 | 
					              <span>📈 ${toolData.skillLevel}</span>
 | 
				
			||||||
 | 
					              <span>•</span>
 | 
				
			||||||
 | 
					              <span>📄 ${toolData.license}</span>
 | 
				
			||||||
 | 
					              ${hasValidProjectUrl ? '<span class="badge badge-primary">Self-Hosted</span>' : ''}
 | 
				
			||||||
 | 
					              ${isOSS ? '<span class="badge badge-success">Open Source</span>' : ''}
 | 
				
			||||||
 | 
					              ${hasKnowledgebase ? '<span class="badge badge-error">Infos 📖</span>' : ''}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        `;
 | 
				
			||||||
 | 
					      }).join('');
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
@ -108,9 +108,23 @@ const sortedTags = Object.entries(tagFrequency)
 | 
				
			|||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  <!-- View Toggle -->
 | 
					  <!-- View Toggle -->
 | 
				
			||||||
  <div style="display: flex; gap: 1rem; margin-bottom: 1.5rem;">
 | 
					  <div style="display: flex; gap: 1rem; margin-bottom: 1.5rem; align-items: center; flex-wrap: wrap;">
 | 
				
			||||||
    <button class="btn btn-secondary view-toggle active" data-view="grid">Kachelansicht</button>
 | 
					    <button class="btn btn-secondary view-toggle active" data-view="grid">Kachelansicht</button>
 | 
				
			||||||
    <button class="btn btn-secondary view-toggle" data-view="matrix">Matrix-Ansicht</button>
 | 
					    <button class="btn btn-secondary view-toggle" data-view="matrix">Matrix-Ansicht</button>
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    <!-- AI Recommendations Button (only visible when authenticated) -->
 | 
				
			||||||
 | 
					    <button 
 | 
				
			||||||
 | 
					      id="ai-view-toggle" 
 | 
				
			||||||
 | 
					      class="btn btn-secondary view-toggle" 
 | 
				
			||||||
 | 
					      data-view="ai"
 | 
				
			||||||
 | 
					      style="display: none; background-color: var(--color-accent); color: white; border-color: var(--color-accent);"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
 | 
				
			||||||
 | 
					        <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-Empfehlungen
 | 
				
			||||||
 | 
					    </button>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -129,12 +143,30 @@ const sortedTags = Object.entries(tagFrequency)
 | 
				
			|||||||
    const tagCloud = document.getElementById('tag-cloud');
 | 
					    const tagCloud = document.getElementById('tag-cloud');
 | 
				
			||||||
    const tagCloudToggle = document.getElementById('tag-cloud-toggle');
 | 
					    const tagCloudToggle = document.getElementById('tag-cloud-toggle');
 | 
				
			||||||
    const viewToggles = document.querySelectorAll('.view-toggle');
 | 
					    const viewToggles = document.querySelectorAll('.view-toggle');
 | 
				
			||||||
 | 
					    const aiViewToggle = document.getElementById('ai-view-toggle');
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // Track selected tags and phase
 | 
					    // Track selected tags and phase
 | 
				
			||||||
    let selectedTags = new Set();
 | 
					    let selectedTags = new Set();
 | 
				
			||||||
    let selectedPhase = '';
 | 
					    let selectedPhase = '';
 | 
				
			||||||
    let isTagCloudExpanded = false;
 | 
					    let isTagCloudExpanded = false;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    // Check authentication status and show/hide AI button
 | 
				
			||||||
 | 
					    async function checkAuthAndShowAIButton() {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const response = await fetch('/api/auth/status');
 | 
				
			||||||
 | 
					        const data = await response.json();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (data.authenticated && aiViewToggle) {
 | 
				
			||||||
 | 
					          aiViewToggle.style.display = 'inline-flex';
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        console.log('Auth check failed, AI button remains hidden');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Call auth check on page load
 | 
				
			||||||
 | 
					    checkAuthAndShowAIButton();
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    // Initialize tag cloud state
 | 
					    // Initialize tag cloud state
 | 
				
			||||||
    function initTagCloud() {
 | 
					    function initTagCloud() {
 | 
				
			||||||
      const visibleCount = 22;
 | 
					      const visibleCount = 22;
 | 
				
			||||||
 | 
				
			|||||||
@ -5,10 +5,8 @@ import { promises as fs } from 'fs';
 | 
				
			|||||||
import { load } from 'js-yaml';
 | 
					import { load } from 'js-yaml';
 | 
				
			||||||
import path from 'path';
 | 
					import path from 'path';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
export const prerender = false;
 | 
					export const prerender = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
function getEnv(key: string): string {
 | 
					function getEnv(key: string): string {
 | 
				
			||||||
  const value = process.env[key];
 | 
					  const value = process.env[key];
 | 
				
			||||||
  if (!value) {
 | 
					  if (!value) {
 | 
				
			||||||
@ -211,7 +209,7 @@ export const POST: APIRoute = async ({ request }) => {
 | 
				
			|||||||
        'Authorization': `Bearer ${process.env.AI_API_KEY}`
 | 
					        'Authorization': `Bearer ${process.env.AI_API_KEY}`
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      body: JSON.stringify({
 | 
					      body: JSON.stringify({
 | 
				
			||||||
        model: AI_MODEL, // or whatever model is available
 | 
					        model: 'gpt-4o-mini', // or whatever model is available
 | 
				
			||||||
        messages: [
 | 
					        messages: [
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
            role: 'system',
 | 
					            role: 'system',
 | 
				
			||||||
@ -248,8 +246,7 @@ export const POST: APIRoute = async ({ request }) => {
 | 
				
			|||||||
    // Parse AI JSON response
 | 
					    // Parse AI JSON response
 | 
				
			||||||
    let recommendation;
 | 
					    let recommendation;
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const cleanedContent = stripMarkdownJson(aiContent);
 | 
					      recommendation = JSON.parse(aiContent);
 | 
				
			||||||
      recommendation = JSON.parse(cleanedContent);
 | 
					 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      console.error('Failed to parse AI response:', aiContent);
 | 
					      console.error('Failed to parse AI response:', aiContent);
 | 
				
			||||||
      return new Response(JSON.stringify({ error: 'Invalid AI response format' }), {
 | 
					      return new Response(JSON.stringify({ error: 'Invalid AI response format' }), {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,13 +1,31 @@
 | 
				
			|||||||
 | 
					// src/pages/api/auth/status.ts
 | 
				
			||||||
import type { APIRoute } from 'astro';
 | 
					import type { APIRoute } from 'astro';
 | 
				
			||||||
import { getSessionFromRequest, verifySession } from '../../../utils/auth.js';
 | 
					import { getSessionFromRequest, verifySession } from '../../../utils/auth.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const prerender = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const GET: APIRoute = async ({ request }) => {
 | 
					export const GET: APIRoute = async ({ request }) => {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
 | 
					    // Check if authentication is required
 | 
				
			||||||
 | 
					    const authRequired = process.env.AUTHENTICATION_NECESSARY !== 'false';
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if (!authRequired) {
 | 
				
			||||||
 | 
					      // If authentication is not required, always return authenticated
 | 
				
			||||||
 | 
					      return new Response(JSON.stringify({ 
 | 
				
			||||||
 | 
					        authenticated: true,
 | 
				
			||||||
 | 
					        authRequired: false
 | 
				
			||||||
 | 
					      }), {
 | 
				
			||||||
 | 
					        status: 200,
 | 
				
			||||||
 | 
					        headers: { 'Content-Type': 'application/json' }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const sessionToken = getSessionFromRequest(request);
 | 
					    const sessionToken = getSessionFromRequest(request);
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    if (!sessionToken) {
 | 
					    if (!sessionToken) {
 | 
				
			||||||
      return new Response(JSON.stringify({ 
 | 
					      return new Response(JSON.stringify({ 
 | 
				
			||||||
        authenticated: false 
 | 
					        authenticated: false,
 | 
				
			||||||
 | 
					        authRequired: true
 | 
				
			||||||
      }), {
 | 
					      }), {
 | 
				
			||||||
        status: 200,
 | 
					        status: 200,
 | 
				
			||||||
        headers: { 'Content-Type': 'application/json' }
 | 
					        headers: { 'Content-Type': 'application/json' }
 | 
				
			||||||
@ -18,6 +36,7 @@ export const GET: APIRoute = async ({ request }) => {
 | 
				
			|||||||
    
 | 
					    
 | 
				
			||||||
    return new Response(JSON.stringify({ 
 | 
					    return new Response(JSON.stringify({ 
 | 
				
			||||||
      authenticated: session !== null,
 | 
					      authenticated: session !== null,
 | 
				
			||||||
 | 
					      authRequired: true,
 | 
				
			||||||
      expires: session?.exp ? new Date(session.exp * 1000).toISOString() : null
 | 
					      expires: session?.exp ? new Date(session.exp * 1000).toISOString() : null
 | 
				
			||||||
    }), {
 | 
					    }), {
 | 
				
			||||||
      status: 200,
 | 
					      status: 200,
 | 
				
			||||||
@ -27,6 +46,7 @@ export const GET: APIRoute = async ({ request }) => {
 | 
				
			|||||||
  } catch (error) {
 | 
					  } catch (error) {
 | 
				
			||||||
    return new Response(JSON.stringify({ 
 | 
					    return new Response(JSON.stringify({ 
 | 
				
			||||||
      authenticated: false,
 | 
					      authenticated: false,
 | 
				
			||||||
 | 
					      authRequired: process.env.AUTHENTICATION_NECESSARY !== 'false',
 | 
				
			||||||
      error: 'Session verification failed'
 | 
					      error: 'Session verification failed'
 | 
				
			||||||
    }), {
 | 
					    }), {
 | 
				
			||||||
      status: 200,
 | 
					      status: 200,
 | 
				
			||||||
 | 
				
			|||||||
@ -3,6 +3,7 @@ import BaseLayout from '../layouts/BaseLayout.astro';
 | 
				
			|||||||
import ToolCard from '../components/ToolCard.astro';
 | 
					import ToolCard from '../components/ToolCard.astro';
 | 
				
			||||||
import ToolFilters from '../components/ToolFilters.astro';
 | 
					import ToolFilters from '../components/ToolFilters.astro';
 | 
				
			||||||
import ToolMatrix from '../components/ToolMatrix.astro';
 | 
					import ToolMatrix from '../components/ToolMatrix.astro';
 | 
				
			||||||
 | 
					import AIQueryInterface from '../components/AIQueryInterface.astro';
 | 
				
			||||||
import { promises as fs } from 'fs';
 | 
					import { promises as fs } from 'fs';
 | 
				
			||||||
import { load } from 'js-yaml';
 | 
					import { load } from 'js-yaml';
 | 
				
			||||||
import path from 'path';
 | 
					import path from 'path';
 | 
				
			||||||
@ -45,6 +46,16 @@ const tools = data.tools;
 | 
				
			|||||||
        </svg>
 | 
					        </svg>
 | 
				
			||||||
        SSO & Zugang erfahren
 | 
					        SSO & Zugang erfahren
 | 
				
			||||||
      </a>
 | 
					      </a>
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      <!-- New AI Query Button -->
 | 
				
			||||||
 | 
					      <button id="ai-query-btn" class="btn btn-accent" style="padding: 0.75rem 1.5rem; background-color: var(--color-accent); color: white;">
 | 
				
			||||||
 | 
					        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
 | 
				
			||||||
 | 
					          <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 befragen
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
      <a href="#filters-section" class="btn btn-secondary" style="padding: 0.75rem 1.5rem;">
 | 
					      <a href="#filters-section" class="btn btn-secondary" style="padding: 0.75rem 1.5rem;">
 | 
				
			||||||
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
 | 
					        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
 | 
				
			||||||
          <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
 | 
					          <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
 | 
				
			||||||
@ -61,6 +72,9 @@ const tools = data.tools;
 | 
				
			|||||||
  <section id="filters-section" style="padding: 2rem 0;">
 | 
					  <section id="filters-section" style="padding: 2rem 0;">
 | 
				
			||||||
    <ToolFilters />
 | 
					    <ToolFilters />
 | 
				
			||||||
  </section>
 | 
					  </section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <!-- AI Query Interface -->
 | 
				
			||||||
 | 
					  <AIQueryInterface />
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  <!-- Tools Grid -->
 | 
					  <!-- Tools Grid -->
 | 
				
			||||||
  <section id="tools-grid" style="padding-bottom: 2rem;">
 | 
					  <section id="tools-grid" style="padding-bottom: 2rem;">
 | 
				
			||||||
@ -86,10 +100,13 @@ const tools = data.tools;
 | 
				
			|||||||
    const toolsContainer = document.getElementById('tools-container');
 | 
					    const toolsContainer = document.getElementById('tools-container');
 | 
				
			||||||
    const toolsGrid = document.getElementById('tools-grid');
 | 
					    const toolsGrid = document.getElementById('tools-grid');
 | 
				
			||||||
    const matrixContainer = document.getElementById('matrix-container');
 | 
					    const matrixContainer = document.getElementById('matrix-container');
 | 
				
			||||||
 | 
					    const aiInterface = document.getElementById('ai-interface');
 | 
				
			||||||
 | 
					    const filtersSection = document.getElementById('filters-section');
 | 
				
			||||||
    const noResults = document.getElementById('no-results');
 | 
					    const noResults = document.getElementById('no-results');
 | 
				
			||||||
 | 
					    const aiQueryBtn = document.getElementById('ai-query-btn');
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // Guard against null elements
 | 
					    // Guard against null elements
 | 
				
			||||||
    if (!toolsContainer || !toolsGrid || !matrixContainer || !noResults) {
 | 
					    if (!toolsContainer || !toolsGrid || !matrixContainer || !noResults || !aiInterface || !filtersSection) {
 | 
				
			||||||
      console.error('Required DOM elements not found');
 | 
					      console.error('Required DOM elements not found');
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -97,14 +114,90 @@ const tools = data.tools;
 | 
				
			|||||||
    // Initial tools HTML
 | 
					    // Initial tools HTML
 | 
				
			||||||
    const initialToolsHTML = toolsContainer.innerHTML;
 | 
					    const initialToolsHTML = toolsContainer.innerHTML;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    // Authentication check function
 | 
				
			||||||
 | 
					    async function checkAuthentication() {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const response = await fetch('/api/auth/status');
 | 
				
			||||||
 | 
					        const data = await response.json();
 | 
				
			||||||
 | 
					        return data.authenticated;
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        console.error('Auth check failed:', error);
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // AI Query Button Handler
 | 
				
			||||||
 | 
					    if (aiQueryBtn) {
 | 
				
			||||||
 | 
					      aiQueryBtn.addEventListener('click', async () => {
 | 
				
			||||||
 | 
					        const isAuthenticated = await checkAuthentication();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (!isAuthenticated) {
 | 
				
			||||||
 | 
					          // Redirect to login, then back to AI view
 | 
				
			||||||
 | 
					          const returnUrl = `${window.location.pathname}?view=ai`;
 | 
				
			||||||
 | 
					          window.location.href = `/api/auth/login?returnTo=${encodeURIComponent(returnUrl)}`;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          // Switch to AI view
 | 
				
			||||||
 | 
					          switchToView('ai');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Check URL parameters on page load for view switching
 | 
				
			||||||
 | 
					    const urlParams = new URLSearchParams(window.location.search);
 | 
				
			||||||
 | 
					    const viewParam = urlParams.get('view');
 | 
				
			||||||
 | 
					    if (viewParam === 'ai') {
 | 
				
			||||||
 | 
					      // User was redirected after authentication, switch to AI view
 | 
				
			||||||
 | 
					      switchToView('ai');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Function to switch between different views
 | 
				
			||||||
 | 
					    function switchToView(view) {
 | 
				
			||||||
 | 
					      // Hide all views first (using non-null assertions since we've already checked)
 | 
				
			||||||
 | 
					      toolsGrid!.style.display = 'none';
 | 
				
			||||||
 | 
					      matrixContainer!.style.display = 'none';
 | 
				
			||||||
 | 
					      aiInterface!.style.display = 'none';
 | 
				
			||||||
 | 
					      filtersSection!.style.display = 'none';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Update view toggle buttons
 | 
				
			||||||
 | 
					      const viewToggles = document.querySelectorAll('.view-toggle');
 | 
				
			||||||
 | 
					      viewToggles.forEach(btn => {
 | 
				
			||||||
 | 
					        btn.classList.toggle('active', btn.getAttribute('data-view') === view);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Show appropriate view
 | 
				
			||||||
 | 
					      switch (view) {
 | 
				
			||||||
 | 
					        case 'ai':
 | 
				
			||||||
 | 
					          aiInterface!.style.display = 'block';
 | 
				
			||||||
 | 
					          // Focus on the input
 | 
				
			||||||
 | 
					          const aiInput = document.getElementById('ai-query-input');
 | 
				
			||||||
 | 
					          if (aiInput) {
 | 
				
			||||||
 | 
					            setTimeout(() => aiInput.focus(), 100);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        case 'matrix':
 | 
				
			||||||
 | 
					          matrixContainer!.style.display = 'block';
 | 
				
			||||||
 | 
					          filtersSection!.style.display = 'block';
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        default: // grid
 | 
				
			||||||
 | 
					          toolsGrid!.style.display = 'block';
 | 
				
			||||||
 | 
					          filtersSection!.style.display = 'block';
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Clear URL parameters after switching
 | 
				
			||||||
 | 
					      if (window.location.search) {
 | 
				
			||||||
 | 
					        window.history.replaceState({}, '', window.location.pathname);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    // Handle filtered results
 | 
					    // Handle filtered results
 | 
				
			||||||
    window.addEventListener('toolsFiltered', (event: Event) => {
 | 
					    window.addEventListener('toolsFiltered', (event: Event) => {
 | 
				
			||||||
      const customEvent = event as CustomEvent;
 | 
					      const customEvent = event as CustomEvent;
 | 
				
			||||||
      const filtered = customEvent.detail;
 | 
					      const filtered = customEvent.detail;
 | 
				
			||||||
      const currentView = document.querySelector('.view-toggle.active')?.getAttribute('data-view');
 | 
					      const currentView = document.querySelector('.view-toggle.active')?.getAttribute('data-view');
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
      if (currentView === 'matrix') {
 | 
					      if (currentView === 'matrix' || currentView === 'ai') {
 | 
				
			||||||
        // Matrix view handles its own rendering
 | 
					        // Matrix and AI views handle their own rendering
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
@ -128,16 +221,12 @@ const tools = data.tools;
 | 
				
			|||||||
    window.addEventListener('viewChanged', (event: Event) => {
 | 
					    window.addEventListener('viewChanged', (event: Event) => {
 | 
				
			||||||
      const customEvent = event as CustomEvent;
 | 
					      const customEvent = event as CustomEvent;
 | 
				
			||||||
      const view = customEvent.detail;
 | 
					      const view = customEvent.detail;
 | 
				
			||||||
      
 | 
					      switchToView(view);
 | 
				
			||||||
      if (view === 'matrix') {
 | 
					 | 
				
			||||||
        toolsGrid.style.display = 'none';
 | 
					 | 
				
			||||||
        matrixContainer.style.display = 'block';
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        toolsGrid.style.display = 'block';
 | 
					 | 
				
			||||||
        matrixContainer.style.display = 'none';
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Make switchToView available globally for the AI button
 | 
				
			||||||
 | 
					    (window as any).switchToAIView = () => switchToView('ai');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function createToolCard(tool) {
 | 
					function createToolCard(tool) {
 | 
				
			||||||
  const hasValidProjectUrl = tool.projectUrl !== undefined && 
 | 
					  const hasValidProjectUrl = tool.projectUrl !== undefined && 
 | 
				
			||||||
                            tool.projectUrl !== null && 
 | 
					                            tool.projectUrl !== null && 
 | 
				
			||||||
 | 
				
			|||||||
@ -590,6 +590,28 @@ footer {
 | 
				
			|||||||
  100% { background-color: transparent; }
 | 
					  100% { background-color: transparent; }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Loading spinner enhancement */
 | 
				
			||||||
 | 
					@keyframes pulse {
 | 
				
			||||||
 | 
					  0%, 100% {
 | 
				
			||||||
 | 
					    opacity: 1;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  50% {
 | 
				
			||||||
 | 
					    opacity: 0.5;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* AI Interface animations */
 | 
				
			||||||
 | 
					@keyframes fadeInUp {
 | 
				
			||||||
 | 
					  from {
 | 
				
			||||||
 | 
					    opacity: 0;
 | 
				
			||||||
 | 
					    transform: translateY(20px);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  to {
 | 
				
			||||||
 | 
					    opacity: 1;
 | 
				
			||||||
 | 
					    transform: translateY(0);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.kb-content {
 | 
					.kb-content {
 | 
				
			||||||
  line-height: 1.7;
 | 
					  line-height: 1.7;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -925,6 +947,47 @@ footer {
 | 
				
			|||||||
  .tag-header {
 | 
					  .tag-header {
 | 
				
			||||||
    gap: 0.75rem;
 | 
					    gap: 0.75rem;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  .ai-interface {
 | 
				
			||||||
 | 
					    padding: 1rem 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .workflow-container {
 | 
				
			||||||
 | 
					    gap: 0.75rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .phase-header {
 | 
				
			||||||
 | 
					    padding: 1rem;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    gap: 0.75rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .phase-number {
 | 
				
			||||||
 | 
					    width: 2rem;
 | 
				
			||||||
 | 
					    height: 2rem;
 | 
				
			||||||
 | 
					    font-size: 1rem;
 | 
				
			||||||
 | 
					    align-self: flex-start;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .phase-title {
 | 
				
			||||||
 | 
					    font-size: 1.125rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .tool-rec-header {
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    gap: 0.5rem;
 | 
				
			||||||
 | 
					    align-items: start;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .tool-rec-metadata {
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    align-items: start;
 | 
				
			||||||
 | 
					    gap: 0.25rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .ai-input-container textarea {
 | 
				
			||||||
 | 
					    min-height: 100px;
 | 
				
			||||||
 | 
					    font-size: 0.8125rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@media (max-width: 640px) {
 | 
					@media (max-width: 640px) {
 | 
				
			||||||
@ -956,4 +1019,243 @@ footer {
 | 
				
			|||||||
  .phase-button {
 | 
					  .phase-button {
 | 
				
			||||||
    width: 100%;
 | 
					    width: 100%;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  .ai-query-section {
 | 
				
			||||||
 | 
					    margin-bottom: 2rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .phase-header {
 | 
				
			||||||
 | 
					    padding: 0.75rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .tool-recommendation {
 | 
				
			||||||
 | 
					    padding: 0.75rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .tool-rec-justification {
 | 
				
			||||||
 | 
					    padding: 0.5rem;
 | 
				
			||||||
 | 
					    font-size: 0.8125rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* AI Interface Styles - Add to global.css */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* AI Interface Container */
 | 
				
			||||||
 | 
					.ai-interface {
 | 
				
			||||||
 | 
					  padding: 2rem 0;
 | 
				
			||||||
 | 
					  max-width: 1200px;
 | 
				
			||||||
 | 
					  margin: 0 auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.ai-query-section {
 | 
				
			||||||
 | 
					  margin-bottom: 3rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.ai-input-container textarea {
 | 
				
			||||||
 | 
					  transition: border-color 0.2s ease, box-shadow 0.2s ease;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.ai-input-container textarea:focus {
 | 
				
			||||||
 | 
					  outline: none;
 | 
				
			||||||
 | 
					  border-color: var(--color-primary);
 | 
				
			||||||
 | 
					  box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Loading, Error, and Results States */
 | 
				
			||||||
 | 
					.ai-loading, .ai-error, .ai-results {
 | 
				
			||||||
 | 
					  animation: fadeIn 0.3s ease-in;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Workflow Visualization */
 | 
				
			||||||
 | 
					.workflow-container {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  gap: 1rem;
 | 
				
			||||||
 | 
					  max-width: 900px;
 | 
				
			||||||
 | 
					  margin: 0 auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.workflow-phase {
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.phase-header {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: flex-start;
 | 
				
			||||||
 | 
					  gap: 1rem;
 | 
				
			||||||
 | 
					  padding: 1.5rem;
 | 
				
			||||||
 | 
					  background-color: var(--color-bg);
 | 
				
			||||||
 | 
					  border: 2px solid var(--color-border);
 | 
				
			||||||
 | 
					  border-radius: 0.75rem;
 | 
				
			||||||
 | 
					  transition: all 0.3s ease;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.phase-header:hover {
 | 
				
			||||||
 | 
					  border-color: var(--color-primary);
 | 
				
			||||||
 | 
					  box-shadow: var(--shadow-md);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.phase-number {
 | 
				
			||||||
 | 
					  background-color: var(--color-primary);
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					  width: 2.5rem;
 | 
				
			||||||
 | 
					  height: 2.5rem;
 | 
				
			||||||
 | 
					  border-radius: 50%;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  font-weight: 600;
 | 
				
			||||||
 | 
					  font-size: 1.125rem;
 | 
				
			||||||
 | 
					  flex-shrink: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.phase-info {
 | 
				
			||||||
 | 
					  flex: 1;
 | 
				
			||||||
 | 
					  min-width: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.phase-title {
 | 
				
			||||||
 | 
					  margin: 0 0 1rem 0;
 | 
				
			||||||
 | 
					  color: var(--color-text);
 | 
				
			||||||
 | 
					  font-size: 1.25rem;
 | 
				
			||||||
 | 
					  font-weight: 600;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.phase-tools {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  gap: 0.75rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.workflow-arrow {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  margin: 0.5rem 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Tool Recommendation Cards */
 | 
				
			||||||
 | 
					.tool-recommendation {
 | 
				
			||||||
 | 
					  background-color: var(--color-bg-secondary);
 | 
				
			||||||
 | 
					  border: 1px solid var(--color-border);
 | 
				
			||||||
 | 
					  border-radius: 0.5rem;
 | 
				
			||||||
 | 
					  padding: 1rem;
 | 
				
			||||||
 | 
					  transition: all 0.2s ease;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tool-recommendation:hover {
 | 
				
			||||||
 | 
					  border-color: var(--color-primary);
 | 
				
			||||||
 | 
					  box-shadow: var(--shadow-sm);
 | 
				
			||||||
 | 
					  transform: translateY(-1px);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tool-recommendation.hosted {
 | 
				
			||||||
 | 
					  background-color: var(--color-hosted-bg);
 | 
				
			||||||
 | 
					  border-color: var(--color-hosted);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tool-recommendation.oss {
 | 
				
			||||||
 | 
					  background-color: var(--color-oss-bg);
 | 
				
			||||||
 | 
					  border-color: var(--color-oss);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tool-rec-header {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: space-between;
 | 
				
			||||||
 | 
					  align-items: start;
 | 
				
			||||||
 | 
					  margin-bottom: 0.75rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tool-rec-name {
 | 
				
			||||||
 | 
					  font-weight: 600;
 | 
				
			||||||
 | 
					  font-size: 1rem;
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					  color: var(--color-text);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tool-rec-priority {
 | 
				
			||||||
 | 
					  font-size: 0.75rem;
 | 
				
			||||||
 | 
					  padding: 0.25rem 0.5rem;
 | 
				
			||||||
 | 
					  border-radius: 1rem;
 | 
				
			||||||
 | 
					  font-weight: 500;
 | 
				
			||||||
 | 
					  text-transform: uppercase;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tool-rec-priority.high {
 | 
				
			||||||
 | 
					  background-color: var(--color-error);
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tool-rec-priority.medium {
 | 
				
			||||||
 | 
					  background-color: var(--color-warning);
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tool-rec-priority.low {
 | 
				
			||||||
 | 
					  background-color: var(--color-accent);
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tool-rec-justification {
 | 
				
			||||||
 | 
					  font-size: 0.875rem;
 | 
				
			||||||
 | 
					  line-height: 1.5;
 | 
				
			||||||
 | 
					  color: var(--color-text-secondary);
 | 
				
			||||||
 | 
					  margin-bottom: 0.75rem;
 | 
				
			||||||
 | 
					  font-style: italic;
 | 
				
			||||||
 | 
					  background-color: var(--color-bg-tertiary);
 | 
				
			||||||
 | 
					  padding: 0.75rem;
 | 
				
			||||||
 | 
					  border-radius: 0.375rem;
 | 
				
			||||||
 | 
					  border-left: 3px solid var(--color-primary);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tool-rec-metadata {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-wrap: wrap;
 | 
				
			||||||
 | 
					  gap: 0.5rem;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  font-size: 0.75rem;
 | 
				
			||||||
 | 
					  color: var(--color-text-secondary);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tool-rec-metadata .badge {
 | 
				
			||||||
 | 
					  font-size: 0.625rem;
 | 
				
			||||||
 | 
					  padding: 0.125rem 0.375rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Enhanced button styling for AI context */
 | 
				
			||||||
 | 
					.btn-accent {
 | 
				
			||||||
 | 
					  background-color: var(--color-accent);
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					  border-color: var(--color-accent);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn-accent:hover {
 | 
				
			||||||
 | 
					  background-color: var(--color-accent-hover);
 | 
				
			||||||
 | 
					  border-color: var(--color-accent-hover);
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					  text-decoration: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Character counter styling */
 | 
				
			||||||
 | 
					.ai-input-container {
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.ai-results {
 | 
				
			||||||
 | 
					  animation: fadeInUp 0.5s ease-out;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tool-recommendation {
 | 
				
			||||||
 | 
					  animation: fadeInUp 0.3s ease-out;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tool-recommendation:nth-child(1) { animation-delay: 0.1s; }
 | 
				
			||||||
 | 
					.tool-recommendation:nth-child(2) { animation-delay: 0.2s; }
 | 
				
			||||||
 | 
					.tool-recommendation:nth-child(3) { animation-delay: 0.3s; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.ai-loading p {
 | 
				
			||||||
 | 
					  animation: pulse 2s ease-in-out infinite;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user