/** * Debounced scroll-to-bottom. * Prevents multiple competing smooth-scroll animations during bursty SSE streaming. * Uses a 200ms debounce window — if called again within 200ms the earlier call is skipped. */ let scrollTimer: ReturnType | null = null; export const scrollToBottom = ( element: HTMLElement | null, widgetType?: "floating" | "embedded" ): void => { if (!element) return; const scrollContainer = element.closest( '[data-scroll-container="chat-messages"]' ) as HTMLElement | null; if (!scrollContainer) return; if (scrollTimer !== null) return; // Already scheduled scrollTimer = setTimeout(() => { scrollTimer = null; if (widgetType === "embedded") { const messages = scrollContainer.querySelectorAll('[data-message-role]'); let lastUserMessage: Element | null = null; messages.forEach((msg) => { if (msg.getAttribute('data-message-role') === 'user') { lastUserMessage = msg; } }); if (lastUserMessage) { const messageTop = (lastUserMessage as HTMLElement).offsetTop; scrollContainer.scrollTo({ top: messageTop - 10, behavior: "smooth" }); return; } } scrollContainer.scrollTo({ top: scrollContainer.scrollHeight, behavior: "smooth" }); }, 200); }; export const createNameSentence = (name: string | undefined): string => { const finalname = name || "موری"; const persianVowels = ["ا", "و", "د", "ذ", "ر", "ز", "ژ"]; const lastChar = finalname.slice(-1); const endsWithVowel = persianVowels.includes(lastChar); if (endsWithVowel) { return `سلام من ${finalname}م!`; } else { return `سلام من ${finalname}‌ام!`; } }; /** * Fetch with timeout support * @param url - URL to fetch * @param options - Fetch options * @param timeoutMs - Timeout in milliseconds (default: 30000ms = 30s) * @returns Promise with Response */ export async function fetchWithTimeout( url: string, options: RequestInit = {}, timeoutMs: number = 30000 ): Promise { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(url, { ...options, signal: controller.signal }); clearTimeout(timeoutId); return response; } catch (error) { clearTimeout(timeoutId); throw error; } } /** * Retry a request with exponential backoff * @param fn - Function to retry * @param options - Retry options * @returns Promise with result */ export async function retryRequest( fn: () => Promise, options: { retries?: number; delays?: number[]; shouldRetry?: (error: unknown) => boolean; } = {} ): Promise { const { retries = 2, delays = [2000, 4000], // Exponential backoff: 2s, 4s shouldRetry = () => true } = options; let lastError: unknown; for (let attempt = 0; attempt < retries; attempt++) { try { return await fn(); } catch (error) { lastError = error; // Don't retry if it's the last attempt if (attempt === retries - 1) { throw lastError; } // Check if we should retry this error if (!shouldRetry(error)) { throw error; } // Wait before retrying (exponential backoff) const delay = delays[attempt] || delays[delays.length - 1]; await new Promise((resolve) => setTimeout(resolve, delay)); } } throw lastError; }