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>
|
||||||
@ -62,6 +73,9 @@ const tools = data.tools;
|
|||||||
<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;">
|
||||||
<div class="grid grid-cols-3 gap-4" id="tools-container">
|
<div class="grid grid-cols-3 gap-4" id="tools-container">
|
||||||
@ -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