ai queue repr
This commit is contained in:
		
							parent
							
								
									d2fdeccce3
								
							
						
					
					
						commit
						69fc97f7a0
					
				@ -81,7 +81,8 @@ const domainAgnosticSoftware = data['domain-agnostic-software'] || [];
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
  <!-- This should be your loading section in AIQueryInterface.astro -->
 | 
			
		||||
  <!-- Loading State -->
 | 
			
		||||
  <div id="ai-loading" class="ai-loading" style="display: none; text-align: center; padding: 2rem;">
 | 
			
		||||
    <div style="display: inline-block; margin-bottom: 1rem;">
 | 
			
		||||
@ -92,6 +93,32 @@ const domainAgnosticSoftware = data['domain-agnostic-software'] || [];
 | 
			
		||||
      </svg>
 | 
			
		||||
    </div>
 | 
			
		||||
    <p id="loading-text" style="color: var(--color-text-secondary);">Analysiere Szenario und generiere Empfehlungen...</p>
 | 
			
		||||
    
 | 
			
		||||
    <!-- Queue Status Display - THIS SECTION SHOULD BE PRESENT -->
 | 
			
		||||
    <div id="queue-status" style="margin-top: 1rem; padding: 1rem; background-color: var(--color-bg-secondary); border-radius: 0.5rem; border: 1px solid var(--color-border); display: none;">
 | 
			
		||||
      <div style="display: flex; align-items: center; justify-content: center; gap: 1rem; margin-bottom: 0.75rem;">
 | 
			
		||||
        <div style="display: flex; align-items: center; gap: 0.5rem;">
 | 
			
		||||
          <div id="queue-position-badge" style="width: 24px; height: 24px; background-color: var(--color-primary); color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 0.875rem;">1</div>
 | 
			
		||||
          <span style="font-weight: 500;">Position in Warteschlange</span>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div style="font-size: 0.875rem; color: var(--color-text-secondary); text-align: center;">
 | 
			
		||||
        <div id="queue-length-info" style="margin-bottom: 0.5rem;">
 | 
			
		||||
          <span id="queue-length">0</span> Anfrage(n) in der Warteschlange
 | 
			
		||||
        </div>
 | 
			
		||||
        <div id="estimated-time-info">
 | 
			
		||||
          Geschätzte Wartezeit: <span id="estimated-time">--</span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div id="task-id-info" style="margin-top: 0.5rem; font-family: monospace; font-size: 0.75rem; opacity: 0.7;">
 | 
			
		||||
          Task-ID: <span id="current-task-id">--</span>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      
 | 
			
		||||
      <!-- Progress bar -->
 | 
			
		||||
      <div style="margin-top: 1rem; background-color: var(--color-bg-tertiary); border-radius: 0.25rem; height: 4px; overflow: hidden;">
 | 
			
		||||
        <div id="queue-progress" style="height: 100%; background-color: var(--color-primary); width: 0%; transition: width 0.3s ease;"></div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <!-- Error State -->
 | 
			
		||||
@ -240,86 +267,161 @@ document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
  aiInput.addEventListener('input', updateCharacterCount);
 | 
			
		||||
  updateCharacterCount();
 | 
			
		||||
 | 
			
		||||
  // Submit handler
 | 
			
		||||
  const handleSubmit = async () => {
 | 
			
		||||
    const query = aiInput.value.trim();
 | 
			
		||||
    
 | 
			
		||||
    if (!query) {
 | 
			
		||||
      alert('Bitte geben Sie eine Beschreibung ein.');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (query.length < 10) {
 | 
			
		||||
      alert('Bitte geben Sie eine detailliertere Beschreibung ein (mindestens 10 Zeichen).');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Hide previous results and errors
 | 
			
		||||
    aiResults.style.display = 'none';
 | 
			
		||||
    aiError.style.display = 'none';
 | 
			
		||||
    aiLoading.style.display = 'block';
 | 
			
		||||
    
 | 
			
		||||
    // Disable submit button
 | 
			
		||||
    aiSubmitBtn.disabled = true;
 | 
			
		||||
    submitBtnText.textContent = currentMode === 'workflow' ? 'Generiere Empfehlungen...' : 'Suche passende Methode...';
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const response = await fetch('/api/ai/query', {
 | 
			
		||||
        method: 'POST',
 | 
			
		||||
        headers: {
 | 
			
		||||
          'Content-Type': 'application/json',
 | 
			
		||||
        },
 | 
			
		||||
        body: JSON.stringify({ 
 | 
			
		||||
          query,
 | 
			
		||||
          mode: currentMode 
 | 
			
		||||
        })
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      const data = await response.json();
 | 
			
		||||
 | 
			
		||||
      if (!response.ok) {
 | 
			
		||||
        throw new Error(data.error || `HTTP ${response.status}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!data.success) {
 | 
			
		||||
        throw new Error(data.error || 'Unknown error');
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Store recommendation for restoration
 | 
			
		||||
      currentRecommendation = data.recommendation;
 | 
			
		||||
  // Submit handler with enhanced queue feedback
 | 
			
		||||
    const handleSubmit = async () => {
 | 
			
		||||
      const query = aiInput.value.trim();
 | 
			
		||||
      
 | 
			
		||||
      // Display results based on mode
 | 
			
		||||
      if (currentMode === 'workflow') {
 | 
			
		||||
        displayWorkflowResults(data.recommendation, query);
 | 
			
		||||
      } else {
 | 
			
		||||
        displayToolResults(data.recommendation, query);
 | 
			
		||||
      if (!query) {
 | 
			
		||||
        alert('Bitte geben Sie eine Beschreibung ein.');
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (query.length < 10) {
 | 
			
		||||
        alert('Bitte geben Sie eine detailliertere Beschreibung ein (mindestens 10 Zeichen).');
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Generate task ID for tracking
 | 
			
		||||
      const taskId = `ai_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
 | 
			
		||||
 | 
			
		||||
      // Hide previous results and errors
 | 
			
		||||
      aiResults.style.display = 'none';
 | 
			
		||||
      aiError.style.display = 'none';
 | 
			
		||||
      aiLoading.style.display = 'block';
 | 
			
		||||
      
 | 
			
		||||
      // Show queue status section
 | 
			
		||||
      const queueStatus = document.getElementById('queue-status');
 | 
			
		||||
      const taskIdDisplay = document.getElementById('current-task-id');
 | 
			
		||||
      if (queueStatus && taskIdDisplay) {
 | 
			
		||||
        queueStatus.style.display = 'block';
 | 
			
		||||
        taskIdDisplay.textContent = taskId;
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      aiLoading.style.display = 'none';
 | 
			
		||||
      aiResults.style.display = 'block';
 | 
			
		||||
      // Disable submit button
 | 
			
		||||
      aiSubmitBtn.disabled = true;
 | 
			
		||||
      submitBtnText.textContent = currentMode === 'workflow' ? 'Generiere Empfehlungen...' : 'Suche passende Methode...';
 | 
			
		||||
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('AI query failed:', error);
 | 
			
		||||
      aiLoading.style.display = 'none';
 | 
			
		||||
      aiError.style.display = 'block';
 | 
			
		||||
      // Start queue status polling
 | 
			
		||||
      let statusInterval;
 | 
			
		||||
      let startTime = Date.now();
 | 
			
		||||
      
 | 
			
		||||
      // Show user-friendly error messages
 | 
			
		||||
      if (error.message.includes('429')) {
 | 
			
		||||
        aiErrorMessage.textContent = 'Zu viele Anfragen. Bitte warten Sie einen Moment und versuchen Sie es erneut.';
 | 
			
		||||
      } else if (error.message.includes('401')) {
 | 
			
		||||
        aiErrorMessage.textContent = 'Authentifizierung erforderlich. Bitte melden Sie sich an.';
 | 
			
		||||
      } else if (error.message.includes('503')) {
 | 
			
		||||
        aiErrorMessage.textContent = 'KI-Service vorübergehend nicht verfügbar. Bitte versuchen Sie es später erneut.';
 | 
			
		||||
      } else {
 | 
			
		||||
        aiErrorMessage.textContent = `Fehler: ${error.message}`;
 | 
			
		||||
      const updateQueueStatus = async () => {
 | 
			
		||||
        try {
 | 
			
		||||
          const response = await fetch(`/api/ai/queue-status?taskId=${taskId}`);
 | 
			
		||||
          const data = await response.json();
 | 
			
		||||
          
 | 
			
		||||
          if (data.success) {
 | 
			
		||||
            const queueLength = document.getElementById('queue-length');
 | 
			
		||||
            const estimatedTime = document.getElementById('estimated-time');
 | 
			
		||||
            const positionBadge = document.getElementById('queue-position-badge');
 | 
			
		||||
            const progressBar = document.getElementById('queue-progress');
 | 
			
		||||
            
 | 
			
		||||
            if (queueLength) queueLength.textContent = data.queueLength;
 | 
			
		||||
            
 | 
			
		||||
            if (estimatedTime) {
 | 
			
		||||
              if (data.estimatedWaitTime > 0) {
 | 
			
		||||
                estimatedTime.textContent = formatDuration(data.estimatedWaitTime);
 | 
			
		||||
              } else {
 | 
			
		||||
                estimatedTime.textContent = 'Verarbeitung läuft...';
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            if (positionBadge && data.currentPosition) {
 | 
			
		||||
              positionBadge.textContent = data.currentPosition;
 | 
			
		||||
              
 | 
			
		||||
              // Update progress bar (inverse of position)
 | 
			
		||||
              if (progressBar && data.queueLength > 0) {
 | 
			
		||||
                const progress = Math.max(0, ((data.queueLength - data.currentPosition + 1) / data.queueLength) * 100);
 | 
			
		||||
                progressBar.style.width = `${progress}%`;
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            // If processing and no position (request is being handled)
 | 
			
		||||
            if (data.isProcessing && !data.currentPosition) {
 | 
			
		||||
              if (positionBadge) positionBadge.textContent = '⚡';
 | 
			
		||||
              if (progressBar) progressBar.style.width = '100%';
 | 
			
		||||
              if (estimatedTime) estimatedTime.textContent = 'Verarbeitung läuft...';
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
          console.warn('Queue status update failed:', error);
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
      
 | 
			
		||||
      // Initial status update
 | 
			
		||||
      updateQueueStatus();
 | 
			
		||||
      
 | 
			
		||||
      // Poll every 500ms for status updates
 | 
			
		||||
      statusInterval = setInterval(updateQueueStatus, 500);
 | 
			
		||||
 | 
			
		||||
      try {
 | 
			
		||||
        const response = await fetch('/api/ai/query', {
 | 
			
		||||
          method: 'POST',
 | 
			
		||||
          headers: {
 | 
			
		||||
            'Content-Type': 'application/json',
 | 
			
		||||
          },
 | 
			
		||||
          body: JSON.stringify({ 
 | 
			
		||||
            query,
 | 
			
		||||
            mode: currentMode,
 | 
			
		||||
            taskId // Include task ID for backend tracking
 | 
			
		||||
          })
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const data = await response.json();
 | 
			
		||||
 | 
			
		||||
        // Clear status polling
 | 
			
		||||
        if (statusInterval) clearInterval(statusInterval);
 | 
			
		||||
 | 
			
		||||
        if (!response.ok) {
 | 
			
		||||
          throw new Error(data.error || `HTTP ${response.status}`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!data.success) {
 | 
			
		||||
          throw new Error(data.error || 'Unknown error');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Store recommendation for restoration
 | 
			
		||||
        currentRecommendation = data.recommendation;
 | 
			
		||||
        
 | 
			
		||||
        // Display results based on mode
 | 
			
		||||
        if (currentMode === 'workflow') {
 | 
			
		||||
          displayWorkflowResults(data.recommendation, query);
 | 
			
		||||
        } else {
 | 
			
		||||
          displayToolResults(data.recommendation, query);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        aiLoading.style.display = 'none';
 | 
			
		||||
        aiResults.style.display = 'block';
 | 
			
		||||
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        console.error('AI query failed:', error);
 | 
			
		||||
        
 | 
			
		||||
        // Clear status polling
 | 
			
		||||
        if (statusInterval) clearInterval(statusInterval);
 | 
			
		||||
        
 | 
			
		||||
        aiLoading.style.display = 'none';
 | 
			
		||||
        aiError.style.display = 'block';
 | 
			
		||||
        
 | 
			
		||||
        // Show user-friendly error messages
 | 
			
		||||
        if (error.message.includes('429')) {
 | 
			
		||||
          aiErrorMessage.textContent = 'Zu viele Anfragen. Bitte warten Sie einen Moment und versuchen Sie es erneut.';
 | 
			
		||||
        } else if (error.message.includes('401')) {
 | 
			
		||||
          aiErrorMessage.textContent = 'Authentifizierung erforderlich. Bitte melden Sie sich an.';
 | 
			
		||||
        } else if (error.message.includes('503')) {
 | 
			
		||||
          aiErrorMessage.textContent = 'KI-Service vorübergehend nicht verfügbar. Bitte versuchen Sie es später erneut.';
 | 
			
		||||
        } else {
 | 
			
		||||
          aiErrorMessage.textContent = `Fehler: ${error.message}`;
 | 
			
		||||
        }
 | 
			
		||||
      } finally {
 | 
			
		||||
        // Re-enable submit button and hide queue status
 | 
			
		||||
        aiSubmitBtn.disabled = false;
 | 
			
		||||
        const config = modeConfig[currentMode];
 | 
			
		||||
        submitBtnText.textContent = config.submitText;
 | 
			
		||||
        
 | 
			
		||||
        if (queueStatus) queueStatus.style.display = 'none';
 | 
			
		||||
        if (statusInterval) clearInterval(statusInterval);
 | 
			
		||||
      }
 | 
			
		||||
    } finally {
 | 
			
		||||
      // Re-enable submit button
 | 
			
		||||
      aiSubmitBtn.disabled = false;
 | 
			
		||||
      const config = modeConfig[currentMode];
 | 
			
		||||
      submitBtnText.textContent = config.submitText;
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
  // Event listeners
 | 
			
		||||
  aiSubmitBtn.addEventListener('click', handleSubmit);
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,7 @@ const rateLimitStore = new Map<string, { count: number; resetTime: number }>();
 | 
			
		||||
const RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute
 | 
			
		||||
const RATE_LIMIT_MAX = 10; // 10 requests per minute per user
 | 
			
		||||
 | 
			
		||||
// Input validation and sanitization (UNCHANGED)
 | 
			
		||||
// Input validation and sanitization
 | 
			
		||||
function sanitizeInput(input: string): string {
 | 
			
		||||
  // Remove any content that looks like system instructions
 | 
			
		||||
  let sanitized = input
 | 
			
		||||
@ -36,7 +36,7 @@ function sanitizeInput(input: string): string {
 | 
			
		||||
  return sanitized;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Strip markdown code blocks from AI response (UNCHANGED)
 | 
			
		||||
// Strip markdown code blocks from AI response
 | 
			
		||||
function stripMarkdownJson(content: string): string {
 | 
			
		||||
  // Remove ```json and ``` wrappers
 | 
			
		||||
  return content
 | 
			
		||||
@ -45,7 +45,7 @@ function stripMarkdownJson(content: string): string {
 | 
			
		||||
    .trim();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Rate limiting check (UNCHANGED)
 | 
			
		||||
// Rate limiting check
 | 
			
		||||
function checkRateLimit(userId: string): boolean {
 | 
			
		||||
  const now = Date.now();
 | 
			
		||||
  const userLimit = rateLimitStore.get(userId);
 | 
			
		||||
@ -74,7 +74,7 @@ function cleanupExpiredRateLimits() {
 | 
			
		||||
 | 
			
		||||
setInterval(cleanupExpiredRateLimits, 5 * 60 * 1000);
 | 
			
		||||
 | 
			
		||||
// Load tools database (UNCHANGED)
 | 
			
		||||
// Load tools database
 | 
			
		||||
async function loadToolsDatabase() {
 | 
			
		||||
  try {
 | 
			
		||||
    return await getCompressedToolsDataForAI();
 | 
			
		||||
@ -84,7 +84,7 @@ async function loadToolsDatabase() {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Create system prompt for workflow mode (EXACTLY AS ORIGINAL)
 | 
			
		||||
// Create system prompt for workflow mode
 | 
			
		||||
function createWorkflowSystemPrompt(toolsData: any): string {
 | 
			
		||||
  const toolsList = toolsData.tools.map((tool: any) => ({
 | 
			
		||||
    name: tool.name,
 | 
			
		||||
@ -99,7 +99,7 @@ function createWorkflowSystemPrompt(toolsData: any): string {
 | 
			
		||||
    related_concepts: tool.related_concepts || []
 | 
			
		||||
  }));
 | 
			
		||||
 | 
			
		||||
  // NEW: Include concepts for background knowledge
 | 
			
		||||
  // Include concepts for background knowledge
 | 
			
		||||
  const conceptsList = toolsData.concepts.map((concept: any) => ({
 | 
			
		||||
    name: concept.name,
 | 
			
		||||
    description: concept.description,
 | 
			
		||||
@ -109,7 +109,7 @@ function createWorkflowSystemPrompt(toolsData: any): string {
 | 
			
		||||
    tags: concept.tags
 | 
			
		||||
  }));
 | 
			
		||||
 | 
			
		||||
  // Get regular phases (no more filtering needed)
 | 
			
		||||
  // Get regular phases
 | 
			
		||||
  const regularPhases = toolsData.phases || [];
 | 
			
		||||
  
 | 
			
		||||
  // Get domain-agnostic software phases
 | 
			
		||||
@ -201,7 +201,7 @@ ANTWORT-FORMAT (strict JSON):
 | 
			
		||||
Antworte NUR mit validen JSON. Keine zusätzlichen Erklärungen außerhalb des JSON.`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Create system prompt for tool-specific mode (EXACTLY AS ORIGINAL)
 | 
			
		||||
// Create system prompt for tool-specific mode
 | 
			
		||||
function createToolSystemPrompt(toolsData: any): string {
 | 
			
		||||
  const toolsList = toolsData.tools.map((tool: any) => ({
 | 
			
		||||
    name: tool.name,
 | 
			
		||||
@ -217,7 +217,7 @@ function createToolSystemPrompt(toolsData: any): string {
 | 
			
		||||
    related_concepts: tool.related_concepts || []
 | 
			
		||||
  }));
 | 
			
		||||
 | 
			
		||||
  // NEW: Include concepts for background knowledge
 | 
			
		||||
  // Include concepts for background knowledge
 | 
			
		||||
  const conceptsList = toolsData.concepts.map((concept: any) => ({
 | 
			
		||||
    name: concept.name,
 | 
			
		||||
    description: concept.description,
 | 
			
		||||
@ -277,7 +277,7 @@ Antworte NUR mit validen JSON. Keine zusätzlichen Erklärungen außerhalb des J
 | 
			
		||||
 | 
			
		||||
export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
  try {
 | 
			
		||||
    // CONSOLIDATED: Replace 20+ lines with single function call (UNCHANGED)
 | 
			
		||||
    // Authentication check
 | 
			
		||||
    const authResult = await withAPIAuth(request, 'ai');
 | 
			
		||||
    if (!authResult.authenticated) {
 | 
			
		||||
      return createAuthErrorResponse();
 | 
			
		||||
@ -285,16 +285,16 @@ export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
    
 | 
			
		||||
    const userId = authResult.userId;
 | 
			
		||||
 | 
			
		||||
    // Rate limiting (ONLY CHANGE: Use helper for this one response)
 | 
			
		||||
    // Rate limiting
 | 
			
		||||
    if (!checkRateLimit(userId)) {
 | 
			
		||||
      return apiError.rateLimit('Rate limit exceeded');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Parse request body (UNCHANGED)
 | 
			
		||||
    // Parse request body
 | 
			
		||||
    const body = await request.json();
 | 
			
		||||
    const { query, mode = 'workflow' } = body;
 | 
			
		||||
    const { query, mode = 'workflow', taskId: clientTaskId } = body;
 | 
			
		||||
 | 
			
		||||
    // Validation (ONLY CHANGE: Use helpers for error responses)
 | 
			
		||||
    // Validation
 | 
			
		||||
    if (!query || typeof query !== 'string') {
 | 
			
		||||
      return apiError.badRequest('Query required');
 | 
			
		||||
    }
 | 
			
		||||
@ -303,20 +303,24 @@ export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
      return apiError.badRequest('Invalid mode. Must be "workflow" or "tool"');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Sanitize input (UNCHANGED)
 | 
			
		||||
    // Sanitize input
 | 
			
		||||
    const sanitizedQuery = sanitizeInput(query);
 | 
			
		||||
    if (sanitizedQuery.includes('[FILTERED]')) {
 | 
			
		||||
      return apiError.badRequest('Invalid input detected');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Load tools database (UNCHANGED)
 | 
			
		||||
    // Load tools database
 | 
			
		||||
    const toolsData = await loadToolsDatabase();
 | 
			
		||||
 | 
			
		||||
    // Create appropriate system prompt based on mode (UNCHANGED)
 | 
			
		||||
    // Create appropriate system prompt based on mode
 | 
			
		||||
    const systemPrompt = mode === 'workflow' 
 | 
			
		||||
      ? createWorkflowSystemPrompt(toolsData)
 | 
			
		||||
      : createToolSystemPrompt(toolsData);
 | 
			
		||||
    
 | 
			
		||||
    // Generate task ID for queue tracking (use client-provided ID if available)
 | 
			
		||||
    const taskId = clientTaskId || `ai_${userId}_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
 | 
			
		||||
    
 | 
			
		||||
    // Make AI API call through rate-limited queue
 | 
			
		||||
    const aiResponse = await enqueueApiCall(() =>
 | 
			
		||||
      fetch(process.env.AI_API_ENDPOINT + '/v1/chat/completions', {
 | 
			
		||||
        method: 'POST',
 | 
			
		||||
@ -340,9 +344,9 @@ export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
          temperature: 0.3
 | 
			
		||||
        })
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
    , taskId);
 | 
			
		||||
 | 
			
		||||
    // AI response handling (ONLY CHANGE: Use helpers for error responses)
 | 
			
		||||
    // AI response handling
 | 
			
		||||
    if (!aiResponse.ok) {
 | 
			
		||||
      console.error('AI API error:', await aiResponse.text());
 | 
			
		||||
      return apiServerError.unavailable('AI service unavailable');
 | 
			
		||||
@ -355,7 +359,7 @@ export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
      return apiServerError.unavailable('No response from AI');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Parse AI JSON response (UNCHANGED)
 | 
			
		||||
    // Parse AI JSON response
 | 
			
		||||
    let recommendation;
 | 
			
		||||
    try {
 | 
			
		||||
      const cleanedContent = stripMarkdownJson(aiContent);
 | 
			
		||||
@ -365,7 +369,7 @@ export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
      return apiServerError.unavailable('Invalid AI response format');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Validate tool names and concept names against database (EXACTLY AS ORIGINAL)
 | 
			
		||||
    // Validate tool names and concept names against database
 | 
			
		||||
    const validToolNames = new Set(toolsData.tools.map((t: any) => t.name));
 | 
			
		||||
    const validConceptNames = new Set(toolsData.concepts.map((c: any) => c.name));
 | 
			
		||||
    
 | 
			
		||||
@ -415,13 +419,14 @@ export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Log successful query (UNCHANGED)
 | 
			
		||||
    // Log successful query
 | 
			
		||||
    console.log(`[AI Query] Mode: ${mode}, User: ${userId}, Query length: ${sanitizedQuery.length}, Tools: ${validatedRecommendation.recommended_tools.length}, Concepts: ${validatedRecommendation.background_knowledge?.length || 0}`);
 | 
			
		||||
 | 
			
		||||
    // SUCCESS RESPONSE (UNCHANGED - Preserves exact original format)
 | 
			
		||||
    // Success response with task ID
 | 
			
		||||
    return new Response(JSON.stringify({
 | 
			
		||||
      success: true,
 | 
			
		||||
      mode,
 | 
			
		||||
      taskId,
 | 
			
		||||
      recommendation: validatedRecommendation,
 | 
			
		||||
      query: sanitizedQuery
 | 
			
		||||
    }), {
 | 
			
		||||
@ -431,7 +436,6 @@ export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('AI query error:', error);
 | 
			
		||||
    // ONLY CHANGE: Use helper for error response
 | 
			
		||||
    return apiServerError.internal('Internal server error');
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										23
									
								
								src/pages/api/ai/queue-status.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/pages/api/ai/queue-status.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
			
		||||
// src/pages/api/ai/queue-status.ts
 | 
			
		||||
import type { APIRoute } from 'astro';
 | 
			
		||||
import { getQueueStatus } from '../../../utils/rateLimitedQueue.js';
 | 
			
		||||
import { apiResponse, apiServerError } from '../../../utils/api.js';
 | 
			
		||||
 | 
			
		||||
export const prerender = false;
 | 
			
		||||
 | 
			
		||||
export const GET: APIRoute = async ({ request }) => {
 | 
			
		||||
  try {
 | 
			
		||||
    const url = new URL(request.url);
 | 
			
		||||
    const taskId = url.searchParams.get('taskId');
 | 
			
		||||
    
 | 
			
		||||
    const status = getQueueStatus(taskId || undefined);
 | 
			
		||||
    
 | 
			
		||||
    return apiResponse.success({
 | 
			
		||||
      ...status,
 | 
			
		||||
      timestamp: Date.now()
 | 
			
		||||
    });
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('Queue status error:', error);
 | 
			
		||||
    return apiServerError.internal('Failed to get queue status');
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
@ -1031,7 +1031,7 @@ Collaboration Section Collapse */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ai-loading, .ai-error, .ai-results {
 | 
			
		||||
  animation: fadeIn 0.3s ease-in;
 | 
			
		||||
  animation: fadeIn 0.3s ease-in-out;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ai-mode-toggle {
 | 
			
		||||
@ -1428,12 +1428,23 @@ footer {
 | 
			
		||||
    max-height: 0;
 | 
			
		||||
    padding-top: 0;
 | 
			
		||||
    margin-top: 0;
 | 
			
		||||
    transform: translateY(-10px);
 | 
			
		||||
  }
 | 
			
		||||
  to {
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
    max-height: 1000px;
 | 
			
		||||
    padding-top: 1rem;
 | 
			
		||||
    margin-top: 1rem;
 | 
			
		||||
    transform: translateY(0);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes shimmer {
 | 
			
		||||
  0% {
 | 
			
		||||
    opacity: 0.8;
 | 
			
		||||
  }
 | 
			
		||||
  100% {
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1507,9 +1518,16 @@ Strobing borders: Bright colored borders that change with each keyframe
 | 
			
		||||
Higher opacity: More saturated colors (up to 100% on yellow)
 | 
			
		||||
 | 
			
		||||
This will literally assault the user's retinas. They'll need sunglasses to look at their shared tools! 🌈💥👁️🗨️*/
 | 
			
		||||
 | 
			
		||||
@keyframes pulse {
 | 
			
		||||
  0%, 100% { opacity: 1; }
 | 
			
		||||
  50% { opacity: 0.5; }
 | 
			
		||||
  0%, 100% {
 | 
			
		||||
    transform: scale(1);
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
  }
 | 
			
		||||
  50% {
 | 
			
		||||
    transform: scale(1.05);
 | 
			
		||||
    opacity: 0.8;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes fadeInUp {
 | 
			
		||||
@ -2039,4 +2057,19 @@ This will literally assault the user's retinas. They'll need sunglasses to look
 | 
			
		||||
.form-label.required::after {
 | 
			
		||||
  content: " *";
 | 
			
		||||
  color: var(--color-error);
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#queue-status {
 | 
			
		||||
  animation: slideDown 0.3s ease-out;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#queue-position-badge {
 | 
			
		||||
  animation: pulse 2s infinite;
 | 
			
		||||
  transition: all 0.3s ease;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#queue-progress {
 | 
			
		||||
  background: linear-gradient(90deg, var(--color-primary), var(--color-accent));
 | 
			
		||||
  animation: shimmer 2s ease-in-out infinite alternate;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,6 @@
 | 
			
		||||
// src/utils/rateLimitedQueue.ts
 | 
			
		||||
// ------------------------------------------------------------
 | 
			
		||||
// A tiny FIFO, single‑instance queue that spaces API requests by
 | 
			
		||||
// a configurable delay. Import `enqueueApiCall()` wherever you
 | 
			
		||||
// call the AI API and the queue will make sure calls are sent
 | 
			
		||||
// one after another with the defined pause in‑between.
 | 
			
		||||
// Enhanced FIFO queue with status tracking for visual feedback
 | 
			
		||||
// ------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
import dotenv from "dotenv";
 | 
			
		||||
@ -12,53 +9,113 @@ dotenv.config();
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Delay (in **milliseconds**) between two consecutive API calls.
 | 
			
		||||
 *
 | 
			
		||||
 * Configure it in your `.env` file, e.g.
 | 
			
		||||
 *   AI_RATE_LIMIT_DELAY_MS=2000
 | 
			
		||||
 * Defaults to **1000 ms** (≈ 1 request / second) when not set or invalid.
 | 
			
		||||
 * Defaults to **2000 ms** (2 seconds) when not set or invalid.
 | 
			
		||||
 */
 | 
			
		||||
const RATE_LIMIT_DELAY_MS = Number.parseInt(process.env.AI_RATE_LIMIT_DELAY_MS ?? "1000", 10) || 1000;
 | 
			
		||||
const RATE_LIMIT_DELAY_MS = Number.parseInt(process.env.AI_RATE_LIMIT_DELAY_MS ?? "2000", 10) || 2000;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Internal task type. Every task returns a Promise so callers get the
 | 
			
		||||
 * real API response transparently.
 | 
			
		||||
 * Internal task type with ID tracking for status updates
 | 
			
		||||
 */
 | 
			
		||||
export type Task<T = unknown> = () => Promise<T>;
 | 
			
		||||
 | 
			
		||||
interface QueuedTask {
 | 
			
		||||
  id: string;
 | 
			
		||||
  task: Task;
 | 
			
		||||
  addedAt: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface QueueStatus {
 | 
			
		||||
  queueLength: number;
 | 
			
		||||
  isProcessing: boolean;
 | 
			
		||||
  estimatedWaitTime: number; // in milliseconds
 | 
			
		||||
  currentPosition?: number; // position of specific request
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class RateLimitedQueue {
 | 
			
		||||
  private queue: Task[] = [];
 | 
			
		||||
  private queue: QueuedTask[] = [];
 | 
			
		||||
  private processing = false;
 | 
			
		||||
  private delayMs = RATE_LIMIT_DELAY_MS;
 | 
			
		||||
  private lastProcessedAt = 0;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Schedule a task. Returns a Promise that resolves/rejects with the
 | 
			
		||||
   * task result once the queue reaches it.
 | 
			
		||||
   * Schedule a task with ID tracking. Returns a Promise that resolves/rejects 
 | 
			
		||||
   * with the task result once the queue reaches it.
 | 
			
		||||
   */
 | 
			
		||||
  add<T>(task: Task<T>): Promise<T> {
 | 
			
		||||
  add<T>(task: Task<T>, taskId?: string): Promise<T> {
 | 
			
		||||
    const id = taskId || this.generateTaskId();
 | 
			
		||||
    
 | 
			
		||||
    return new Promise<T>((resolve, reject) => {
 | 
			
		||||
      this.queue.push(async () => {
 | 
			
		||||
        try {
 | 
			
		||||
          const result = await task();
 | 
			
		||||
          resolve(result);
 | 
			
		||||
        } catch (err) {
 | 
			
		||||
          reject(err);
 | 
			
		||||
        }
 | 
			
		||||
      this.queue.push({
 | 
			
		||||
        id,
 | 
			
		||||
        task: async () => {
 | 
			
		||||
          try {
 | 
			
		||||
            const result = await task();
 | 
			
		||||
            resolve(result);
 | 
			
		||||
          } catch (err) {
 | 
			
		||||
            reject(err);
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        addedAt: Date.now()
 | 
			
		||||
      });
 | 
			
		||||
      this.process();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Change the delay at runtime – e.g. if you reload env vars without
 | 
			
		||||
   * restarting the server.
 | 
			
		||||
   * Get current queue status for visual feedback
 | 
			
		||||
   */
 | 
			
		||||
  getStatus(taskId?: string): QueueStatus {
 | 
			
		||||
    const queueLength = this.queue.length;
 | 
			
		||||
    const now = Date.now();
 | 
			
		||||
    
 | 
			
		||||
    // Calculate estimated wait time
 | 
			
		||||
    let estimatedWaitTime = 0;
 | 
			
		||||
    if (queueLength > 0) {
 | 
			
		||||
      if (this.processing) {
 | 
			
		||||
        // Time since last request + remaining delay + queue length * delay
 | 
			
		||||
        const timeSinceLastRequest = now - this.lastProcessedAt;
 | 
			
		||||
        const remainingDelay = Math.max(0, this.delayMs - timeSinceLastRequest);
 | 
			
		||||
        estimatedWaitTime = remainingDelay + (queueLength - 1) * this.delayMs;
 | 
			
		||||
      } else {
 | 
			
		||||
        // Queue will start immediately, so just queue length * delay
 | 
			
		||||
        estimatedWaitTime = queueLength * this.delayMs;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const status: QueueStatus = {
 | 
			
		||||
      queueLength,
 | 
			
		||||
      isProcessing: this.processing,
 | 
			
		||||
      estimatedWaitTime
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Find position of specific task if ID provided
 | 
			
		||||
    if (taskId) {
 | 
			
		||||
      const position = this.queue.findIndex(item => item.id === taskId);
 | 
			
		||||
      if (position >= 0) {
 | 
			
		||||
        status.currentPosition = position + 1; // 1-based indexing for user display
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return status;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Change the delay at runtime
 | 
			
		||||
   */
 | 
			
		||||
  setDelay(ms: number): void {
 | 
			
		||||
    if (!Number.isFinite(ms) || ms < 0) return;
 | 
			
		||||
    this.delayMs = ms;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get current delay setting
 | 
			
		||||
   */
 | 
			
		||||
  getDelay(): number {
 | 
			
		||||
    return this.delayMs;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // ---------------------------------------
 | 
			
		||||
  // ️🌐  Internal helpers
 | 
			
		||||
  // Internal helpers
 | 
			
		||||
  // ---------------------------------------
 | 
			
		||||
  private async process(): Promise<void> {
 | 
			
		||||
    if (this.processing) return;
 | 
			
		||||
@ -67,26 +124,41 @@ class RateLimitedQueue {
 | 
			
		||||
    while (this.queue.length > 0) {
 | 
			
		||||
      const next = this.queue.shift();
 | 
			
		||||
      if (!next) continue;
 | 
			
		||||
      await next();
 | 
			
		||||
      // Wait before the next one
 | 
			
		||||
      await new Promise((r) => setTimeout(r, this.delayMs));
 | 
			
		||||
      
 | 
			
		||||
      this.lastProcessedAt = Date.now();
 | 
			
		||||
      await next.task();
 | 
			
		||||
      
 | 
			
		||||
      // Wait before the next one (only if there are more tasks)
 | 
			
		||||
      if (this.queue.length > 0) {
 | 
			
		||||
        await new Promise((r) => setTimeout(r, this.delayMs));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.processing = false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private generateTaskId(): string {
 | 
			
		||||
    return `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ------------------------------------------------------------
 | 
			
		||||
// Export a **singleton** instance so every import shares the
 | 
			
		||||
// same queue. That way the rate‑limit is enforced globally.
 | 
			
		||||
// Export singleton instance and convenience functions
 | 
			
		||||
// ------------------------------------------------------------
 | 
			
		||||
const queue = new RateLimitedQueue();
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Helper for convenience: `enqueueApiCall(() => fetch(...))`.
 | 
			
		||||
 * Helper for convenience: `enqueueApiCall(() => fetch(...), 'optional-id')`.
 | 
			
		||||
 */
 | 
			
		||||
export function enqueueApiCall<T>(task: Task<T>): Promise<T> {
 | 
			
		||||
  return queue.add(task);
 | 
			
		||||
export function enqueueApiCall<T>(task: Task<T>, taskId?: string): Promise<T> {
 | 
			
		||||
  return queue.add(task, taskId);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default queue;
 | 
			
		||||
/**
 | 
			
		||||
 * Get current queue status for visual feedback
 | 
			
		||||
 */
 | 
			
		||||
export function getQueueStatus(taskId?: string): QueueStatus {
 | 
			
		||||
  return queue.getStatus(taskId);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default queue;
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user