diff --git a/src/components/AIQueryInterface.astro b/src/components/AIQueryInterface.astro
index 1b58445..6e760e7 100644
--- a/src/components/AIQueryInterface.astro
+++ b/src/components/AIQueryInterface.astro
@@ -81,7 +81,8 @@ const domainAgnosticSoftware = data['domain-agnostic-software'] || [];
-
+
+
@@ -92,6 +93,32 @@ const domainAgnosticSoftware = data['domain-agnostic-software'] || [];
Analysiere Szenario und generiere Empfehlungen...
+
+
+
+
+
+
1
+
Position in Warteschlange
+
+
+
+
+ 0 Anfrage(n) in der Warteschlange
+
+
+ Geschätzte Wartezeit: --
+
+
+ Task-ID: --
+
+
+
+
+
+
@@ -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);
diff --git a/src/pages/api/ai/query.ts b/src/pages/api/ai/query.ts
index 5d38ca7..445d978 100644
--- a/src/pages/api/ai/query.ts
+++ b/src/pages/api/ai/query.ts
@@ -20,7 +20,7 @@ const rateLimitStore = new Map();
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');
}
};
\ No newline at end of file
diff --git a/src/pages/api/ai/queue-status.ts b/src/pages/api/ai/queue-status.ts
new file mode 100644
index 0000000..c0e4b32
--- /dev/null
+++ b/src/pages/api/ai/queue-status.ts
@@ -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');
+ }
+};
\ No newline at end of file
diff --git a/src/styles/global.css b/src/styles/global.css
index 32275d6..582f32c 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -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);
-}
\ No newline at end of file
+}
+
+#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;
+}
+
diff --git a/src/utils/rateLimitedQueue.ts b/src/utils/rateLimitedQueue.ts
index ea1b21a..5639d1a 100644
--- a/src/utils/rateLimitedQueue.ts
+++ b/src/utils/rateLimitedQueue.ts
@@ -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 = () => Promise;
+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(task: Task): Promise {
+ add(task: Task, taskId?: string): Promise {
+ const id = taskId || this.generateTaskId();
+
return new Promise((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 {
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(task: Task): Promise {
- return queue.add(task);
+export function enqueueApiCall(task: Task, taskId?: string): Promise {
+ 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;
\ No newline at end of file