diff --git a/src/components/AIQueryInterface.astro b/src/components/AIQueryInterface.astro index e69de29..b1ad746 100644 --- a/src/components/AIQueryInterface.astro +++ b/src/components/AIQueryInterface.astro @@ -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'); +--- + + + + + + \ No newline at end of file diff --git a/src/components/ToolFilters.astro b/src/components/ToolFilters.astro index a43d717..6fe2b62 100644 --- a/src/components/ToolFilters.astro +++ b/src/components/ToolFilters.astro @@ -108,9 +108,23 @@ const sortedTags = Object.entries(tagFrequency) -
+
+ + +
@@ -129,12 +143,30 @@ const sortedTags = Object.entries(tagFrequency) const tagCloud = document.getElementById('tag-cloud'); const tagCloudToggle = document.getElementById('tag-cloud-toggle'); const viewToggles = document.querySelectorAll('.view-toggle'); + const aiViewToggle = document.getElementById('ai-view-toggle'); // Track selected tags and phase let selectedTags = new Set(); let selectedPhase = ''; 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 function initTagCloud() { const visibleCount = 22; diff --git a/src/pages/api/ai/query.ts b/src/pages/api/ai/query.ts index 894153c..d19e89b 100644 --- a/src/pages/api/ai/query.ts +++ b/src/pages/api/ai/query.ts @@ -5,10 +5,8 @@ import { promises as fs } from 'fs'; import { load } from 'js-yaml'; import path from 'path'; - export const prerender = false; - function getEnv(key: string): string { const value = process.env[key]; if (!value) { @@ -211,7 +209,7 @@ export const POST: APIRoute = async ({ request }) => { 'Authorization': `Bearer ${process.env.AI_API_KEY}` }, body: JSON.stringify({ - model: AI_MODEL, // or whatever model is available + model: 'gpt-4o-mini', // or whatever model is available messages: [ { role: 'system', @@ -248,8 +246,7 @@ export const POST: APIRoute = async ({ request }) => { // Parse AI JSON response let recommendation; try { - const cleanedContent = stripMarkdownJson(aiContent); - recommendation = JSON.parse(cleanedContent); + recommendation = JSON.parse(aiContent); } catch (error) { console.error('Failed to parse AI response:', aiContent); return new Response(JSON.stringify({ error: 'Invalid AI response format' }), { diff --git a/src/pages/api/auth/status.ts b/src/pages/api/auth/status.ts index a7dfb28..d8f63ab 100644 --- a/src/pages/api/auth/status.ts +++ b/src/pages/api/auth/status.ts @@ -1,13 +1,31 @@ +// src/pages/api/auth/status.ts import type { APIRoute } from 'astro'; import { getSessionFromRequest, verifySession } from '../../../utils/auth.js'; +export const prerender = false; + export const GET: APIRoute = async ({ request }) => { 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); if (!sessionToken) { return new Response(JSON.stringify({ - authenticated: false + authenticated: false, + authRequired: true }), { status: 200, headers: { 'Content-Type': 'application/json' } @@ -18,6 +36,7 @@ export const GET: APIRoute = async ({ request }) => { return new Response(JSON.stringify({ authenticated: session !== null, + authRequired: true, expires: session?.exp ? new Date(session.exp * 1000).toISOString() : null }), { status: 200, @@ -27,6 +46,7 @@ export const GET: APIRoute = async ({ request }) => { } catch (error) { return new Response(JSON.stringify({ authenticated: false, + authRequired: process.env.AUTHENTICATION_NECESSARY !== 'false', error: 'Session verification failed' }), { status: 200, diff --git a/src/pages/index.astro b/src/pages/index.astro index 30f8f8d..c37a6d9 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -3,6 +3,7 @@ import BaseLayout from '../layouts/BaseLayout.astro'; import ToolCard from '../components/ToolCard.astro'; import ToolFilters from '../components/ToolFilters.astro'; import ToolMatrix from '../components/ToolMatrix.astro'; +import AIQueryInterface from '../components/AIQueryInterface.astro'; import { promises as fs } from 'fs'; import { load } from 'js-yaml'; import path from 'path'; @@ -45,6 +46,16 @@ const tools = data.tools; SSO & Zugang erfahren + + + + @@ -61,6 +72,9 @@ const tools = data.tools;
+ + +
@@ -86,10 +100,13 @@ const tools = data.tools; const toolsContainer = document.getElementById('tools-container'); const toolsGrid = document.getElementById('tools-grid'); 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 aiQueryBtn = document.getElementById('ai-query-btn'); // Guard against null elements - if (!toolsContainer || !toolsGrid || !matrixContainer || !noResults) { + if (!toolsContainer || !toolsGrid || !matrixContainer || !noResults || !aiInterface || !filtersSection) { console.error('Required DOM elements not found'); return; } @@ -97,14 +114,90 @@ const tools = data.tools; // Initial tools HTML 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 window.addEventListener('toolsFiltered', (event: Event) => { const customEvent = event as CustomEvent; const filtered = customEvent.detail; const currentView = document.querySelector('.view-toggle.active')?.getAttribute('data-view'); - if (currentView === 'matrix') { - // Matrix view handles its own rendering + if (currentView === 'matrix' || currentView === 'ai') { + // Matrix and AI views handle their own rendering return; } @@ -128,16 +221,12 @@ const tools = data.tools; window.addEventListener('viewChanged', (event: Event) => { const customEvent = event as CustomEvent; const view = customEvent.detail; - - if (view === 'matrix') { - toolsGrid.style.display = 'none'; - matrixContainer.style.display = 'block'; - } else { - toolsGrid.style.display = 'block'; - matrixContainer.style.display = 'none'; - } + switchToView(view); }); + // Make switchToView available globally for the AI button + (window as any).switchToAIView = () => switchToView('ai'); + function createToolCard(tool) { const hasValidProjectUrl = tool.projectUrl !== undefined && tool.projectUrl !== null && diff --git a/src/styles/global.css b/src/styles/global.css index 76dae54..59b9456 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -590,6 +590,28 @@ footer { 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 { line-height: 1.7; } @@ -925,6 +947,47 @@ footer { .tag-header { 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) { @@ -956,4 +1019,243 @@ footer { .phase-button { 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; } \ No newline at end of file