> ## Documentation Index
> Fetch the complete documentation index at: https://aitutorial.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# RAG Fundamentals

> The core RAG pattern and how it works

export const QuizQuestion = ({question, options, answer, explanation}) => {
  const [selected, setSelected] = useState(null);
  const [revealed, setRevealed] = useState(false);
  const handleSelect = index => {
    if (revealed) return;
    setSelected(index);
    setRevealed(true);
  };
  const isCorrect = selected === answer;
  const getOptionClass = i => {
    const classes = ['quiz-option'];
    if (revealed) {
      classes.push('quiz-option-disabled');
      if (i === answer) classes.push('quiz-option-correct'); else if (i === selected && !isCorrect) classes.push('quiz-option-wrong');
    }
    return classes.join(' ');
  };
  return <div className="quiz-card">
      <p className="quiz-question">{question}</p>
      <div className="quiz-options">
        {options.map((option, i) => <button key={i} onClick={() => handleSelect(i)} className={getOptionClass(i)}>
            <span className="quiz-letter">{String.fromCharCode(65 + i)}</span>
            {option}
          </button>)}
      </div>
      {revealed && <div className={`quiz-feedback ${isCorrect ? 'quiz-feedback-correct' : 'quiz-feedback-wrong'}`}>
          <strong>{isCorrect ? 'Correct!' : 'Incorrect.'}</strong> {explanation}
        </div>}
    </div>;
};

export const Quiz = ({title = "Check Your Understanding", children}) => {
  return <div style={{
    marginTop: '24px'
  }}>
      <div className="quiz-title">{title}</div>
      {children}
    </div>;
};

export const LLMPlayground = ({defaultInput = '', defaultModel = 'gpt-4o-mini', defaultTemperature = 0.7, height = '600px', keepInput = false, defaultMode = 'chat', defaultMessages = [], response = '', forceSettingsOpen = false, title, theme: userTheme}) => {
  const {useState, useEffect, useMemo, useCallback, useRef} = React;
  const OPENAI_STORAGE_KEY = 'openai_api_key';
  const GEMINI_STORAGE_KEY = 'gemini_api_key';
  const ANTHROPIC_STORAGE_KEY = 'anthropic_api_key';
  const PROVIDER_STORAGE_KEY = 'llm_playground_provider';
  const SETTINGS_PANEL_KEY = 'llm_playground_settings_open';
  const SETTINGS_PANEL_WIDTH = 220;
  const OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions';
  const GEMINI_API_URL = 'https://generativelanguage.googleapis.com/v1beta/openai/chat/completions';
  const ANTHROPIC_API_URL = 'https://api.anthropic.com/v1/messages';
  const MAX_TOKENS = 2000;
  const OPENAI_MODELS = [{
    value: 'gpt-4.1-nano',
    label: 'GPT-4.1 Nano'
  }, {
    value: 'gpt-4.1-mini',
    label: 'GPT-4.1 Mini'
  }, {
    value: 'gpt-4.1',
    label: 'GPT-4.1'
  }, {
    value: 'o4-mini',
    label: 'o4 Mini'
  }, {
    value: 'o3',
    label: 'o3'
  }];
  const GEMINI_MODELS = [{
    value: 'gemini-2.5-flash-lite',
    label: 'Gemini 2.5 Flash Lite'
  }, {
    value: 'gemini-2.5-flash',
    label: 'Gemini 2.5 Flash'
  }, {
    value: 'gemini-2.0-flash',
    label: 'Gemini 2.0 Flash'
  }, {
    value: 'gemini-2.5-pro',
    label: 'Gemini 2.5 Pro'
  }];
  const ANTHROPIC_MODELS = [{
    value: 'claude-haiku-4-5-20251001',
    label: 'Claude 4.5 Haiku'
  }, {
    value: 'claude-sonnet-4-6-20260326',
    label: 'Claude 4.6 Sonnet'
  }, {
    value: 'claude-opus-4-6-20260326',
    label: 'Claude 4.6 Opus'
  }];
  const PROVIDERS = {
    gemini: {
      label: 'Gemini',
      models: GEMINI_MODELS,
      storageKey: GEMINI_STORAGE_KEY,
      apiUrl: GEMINI_API_URL
    },
    openai: {
      label: 'OpenAI',
      models: OPENAI_MODELS,
      storageKey: OPENAI_STORAGE_KEY,
      apiUrl: OPENAI_API_URL
    },
    anthropic: {
      label: 'Claude',
      models: ANTHROPIC_MODELS,
      storageKey: ANTHROPIC_STORAGE_KEY,
      apiUrl: ANTHROPIC_API_URL
    }
  };
  const IconWarningSun = ({size = 14, className = ''}) => <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className={className}>
      <path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" />
    </svg>;
  const IconLoadingSpinner = ({size = 14, className = '', strokeColor = 'currentColor', strokeWidth = '2', strokeLinecap = 'butt'}) => <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={strokeColor} strokeWidth={strokeWidth} strokeLinecap={strokeLinecap} className={className}>
      <path d="M21 12a9 9 0 11-6.219-8.56"></path>
    </svg>;
  const IconWarningTriangle = ({size = 16, strokeColor = '#f59e0b', className = ''}) => <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={strokeColor} strokeWidth="2" className={className}>
      <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
      <line x1="12" y1="9" x2="12" y2="13"></line>
      <line x1="12" y1="17" x2="12.01" y2="17"></line>
    </svg>;
  const IconClose = ({size = 10, className = ''}) => <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" className={className}>
      <line x1="18" y1="6" x2="6" y2="18"></line>
      <line x1="6" y1="6" x2="18" y2="18"></line>
    </svg>;
  const IconPlus = ({size = 12, className = ''}) => <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" className={className}>
      <line x1="12" y1="5" x2="12" y2="19"></line>
      <line x1="5" y1="12" x2="19" y2="12"></line>
    </svg>;
  const IconError = ({size = 14, className = ''}) => <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className={className}>
      <circle cx="12" cy="12" r="10"></circle>
      <line x1="12" y1="8" x2="12" y2="12"></line>
      <line x1="12" y1="16" x2="12.01" y2="16"></line>
    </svg>;
  const IconTrash = ({size = 14, className = ''}) => <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
      <polyline points="3 6 5 6 21 6"></polyline>
      <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
      <line x1="10" y1="11" x2="10" y2="17"></line>
      <line x1="14" y1="11" x2="14" y2="17"></line>
    </svg>;
  const IconSend = ({size = 16, fillColor = '#000', className = ''}) => <svg width={size} height={size} viewBox="0 0 24 24" fill={fillColor} className={className}>
      <path d="M1.101 21.757L23.8 12.028 1.101 2.3l.011 7.912 13.623 1.816-13.623 1.817-.011 7.912z" />
    </svg>;
  const IconCheckmark = ({size = 14, strokeColor = '#22c55e', className = ''}) => <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={strokeColor} strokeWidth="2" className={className}>
      <polyline points="9 11 12 14 22 4"></polyline>
      <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
    </svg>;
  const IconXStatus = ({size = 14, strokeColor = '#ef4444', className = ''}) => <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={strokeColor} strokeWidth="2" className={className}>
      <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
      <line x1="9" y1="9" x2="15" y2="15"></line>
      <line x1="15" y1="9" x2="9" y2="15"></line>
    </svg>;
  const IconChevron = ({size = 12, isOpen = false, className = ''}) => <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
      {isOpen ? <polyline points="18 15 12 9 6 15"></polyline> : <polyline points="6 9 12 15 18 9"></polyline>}
    </svg>;
  const IconInfo = ({size = 12, className = ''}) => <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
      <circle cx="12" cy="12" r="10"></circle>
      <line x1="12" y1="16" x2="12" y2="12"></line>
      <line x1="12" y1="8" x2="12.01" y2="8"></line>
    </svg>;
  const isAdvancedMode = defaultMode === 'advanced' || defaultMessages && defaultMessages.length > 0;
  const mode = isAdvancedMode ? 'advanced' : 'chat';
  const [input, setInput] = useState(defaultInput);
  const [output, setOutput] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState('');
  const [provider, setProvider] = useState(() => {
    if (typeof window === 'undefined') return 'gemini';
    return localStorage.getItem(PROVIDER_STORAGE_KEY) || 'gemini';
  });
  const [model, setModel] = useState(() => {
    const initialProvider = typeof window !== 'undefined' && localStorage.getItem(PROVIDER_STORAGE_KEY) || 'gemini';
    return initialProvider === 'gemini' ? 'gemini-2.5-flash-lite' : defaultModel;
  });
  const [temperature, setTemperature] = useState(defaultTemperature);
  const [topP, setTopP] = useState(1.0);
  const [apiKey, setApiKey] = useState('');
  const [responseCount, setResponseCount] = useState(0);
  const responseCountRef = useRef(0);
  const [apiKeyInput, setApiKeyInput] = useState('');
  const [apiKeyProvider, setApiKeyProvider] = useState('gemini');
  const previousProviderRef = useRef(null);
  const [isSavingKey, setIsSavingKey] = useState(false);
  const textareaRef = useRef(null);
  const advancedTextareaRefs = useRef({});
  const responseAreaRef = useRef(null);
  const [messages, setMessages] = useState(() => {
    if (defaultMessages && defaultMessages.length > 0) {
      return defaultMessages;
    }
    return [{
      role: 'system',
      content: ''
    }, {
      role: 'user',
      content: ''
    }];
  });
  const [conversationHistory, setConversationHistory] = useState([]);
  const messagesEndRef = useRef(null);
  const isInitialMount = useRef(true);
  const [lastSentJson, setLastSentJson] = useState(null);
  const [lastResponseJson, setLastResponseJson] = useState(null);
  const [hasSubmitted, setHasSubmitted] = useState(false);
  const [isSettingsOpen, setIsSettingsOpen] = useState(() => {
    if (forceSettingsOpen) return true;
    if (typeof window === 'undefined') return true;
    const stored = localStorage.getItem(SETTINGS_PANEL_KEY);
    return stored !== null ? stored === 'true' : true;
  });
  const [isApiCallsOpen, setIsApiCallsOpen] = useState(false);
  const [apiCallTab, setApiCallTab] = useState('request');
  const [isMaximized, setIsMaximized] = useState(false);
  const [isCollapsed, setIsCollapsed] = useState(false);
  const toggleMaximize = () => setIsMaximized(!isMaximized);
  const toggleCollapse = () => setIsCollapsed(!isCollapsed);
  const headerTitle = title || (defaultMode === 'advanced' ? 'Advanced Playground' : 'LLM Playground');
  const [detectedTheme, setDetectedTheme] = useState('dark');
  useEffect(() => {
    if (typeof window === 'undefined') return;
    const checkTheme = () => {
      const isDark = document.documentElement.classList.contains('dark');
      setDetectedTheme(isDark ? 'dark' : 'light');
    };
    checkTheme();
    const observer = new MutationObserver(checkTheme);
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ['class']
    });
    return () => observer.disconnect();
  }, []);
  const theme = userTheme || detectedTheme;
  useEffect(() => {
    if (typeof window === 'undefined') return;
    const currentProvider = localStorage.getItem(PROVIDER_STORAGE_KEY) || 'gemini';
    const storageKey = PROVIDERS[currentProvider].storageKey;
    const storedKey = localStorage.getItem(storageKey);
    if (storedKey) {
      setApiKey(storedKey);
    } else {
      for (const [provKey, prov] of Object.entries(PROVIDERS)) {
        if (provKey === currentProvider) continue;
        const otherKey = localStorage.getItem(prov.storageKey);
        if (otherKey) {
          setApiKey(otherKey);
          setProvider(provKey);
          setModel(prov.models[0].value);
          localStorage.setItem(PROVIDER_STORAGE_KEY, provKey);
          break;
        }
      }
    }
    const handleApiKeyChanged = () => {
      const activeProvider = localStorage.getItem(PROVIDER_STORAGE_KEY) || 'gemini';
      const key = localStorage.getItem(PROVIDERS[activeProvider].storageKey);
      if (key) {
        setApiKey(key);
        setProvider(activeProvider);
        setModel(PROVIDERS[activeProvider].models[0].value);
      }
    };
    window.addEventListener('apiKeyChanged', handleApiKeyChanged);
    return () => window.removeEventListener('apiKeyChanged', handleApiKeyChanged);
  }, []);
  useEffect(() => {
    if (typeof window === 'undefined') return;
    if (forceSettingsOpen) {
      setIsSettingsOpen(true);
      return;
    }
    localStorage.setItem(SETTINGS_PANEL_KEY, String(isSettingsOpen));
  }, [isSettingsOpen, forceSettingsOpen]);
  useEffect(() => {
    responseCountRef.current = responseCount;
  }, [responseCount]);
  const filterComments = useCallback(text => {
    return text.split('\n').filter(line => !line.trim().startsWith('//')).join('\n').trim();
  }, []);
  const autoResizeTextarea = useCallback(textarea => {
    if (!textarea) return;
    textarea.style.height = '0px';
    const scrollHeight = textarea.scrollHeight;
    const minHeight = 48;
    const maxHeight = 240;
    const newHeight = Math.min(Math.max(scrollHeight, minHeight), maxHeight);
    textarea.style.height = `${newHeight}px`;
  }, []);
  useEffect(() => {
    if (textareaRef.current) {
      textareaRef.current.style.height = '0px';
      textareaRef.current.style.height = `${Math.max(textareaRef.current.scrollHeight, 60)}px`;
    }
  }, []);
  useEffect(() => {
    if (isInitialMount.current) {
      isInitialMount.current = false;
      return;
    }
    if (messagesEndRef.current && conversationHistory.length > 0) {
      setTimeout(() => {
        if (messagesEndRef.current) {
          messagesEndRef.current.scrollIntoView({
            behavior: 'smooth',
            block: 'nearest'
          });
        }
      }, 100);
    }
  }, [conversationHistory, isLoading]);
  useEffect(() => {
    Object.values(advancedTextareaRefs.current).forEach(textarea => {
      if (textarea) {
        autoResizeTextarea(textarea);
      }
    });
  }, [messages, autoResizeTextarea]);
  useEffect(() => {
    if (mode === 'advanced' && hasSubmitted && responseAreaRef.current && !isLoading) {
      setTimeout(() => {
        if (responseAreaRef.current) {
          responseAreaRef.current.scrollIntoView({
            behavior: 'smooth',
            block: 'start'
          });
        }
      }, 100);
    }
  }, [hasSubmitted, output, mode, isLoading]);
  const constructAdvancedMessages = useCallback(() => {
    return messages.filter(msg => msg.content.trim().length > 0);
  }, [messages]);
  const isFormValid = useMemo(() => {
    if (!apiKey) return false;
    if (mode === 'chat') {
      return input.trim().length > 0;
    } else {
      return messages.some(msg => msg.content.trim().length > 0);
    }
  }, [mode, apiKey, input, messages]);
  const handleApiKeySubmit = useCallback(async e => {
    e.preventDefault();
    setIsSavingKey(true);
    setError('');
    if (!apiKeyInput.trim()) {
      setError(`Please enter your ${PROVIDERS[apiKeyProvider].label} API key`);
      setIsSavingKey(false);
      return;
    }
    const trimmedKey = apiKeyInput.trim();
    if (apiKeyProvider === 'openai' && !trimmedKey.startsWith('sk-')) {
      setError('Invalid API key format. OpenAI API keys should start with "sk-"');
      setIsSavingKey(false);
      return;
    }
    try {
      const storageKey = PROVIDERS[apiKeyProvider].storageKey;
      localStorage.setItem(storageKey, trimmedKey);
      localStorage.setItem(PROVIDER_STORAGE_KEY, apiKeyProvider);
      setProvider(apiKeyProvider);
      setApiKey(trimmedKey);
      setApiKeyInput('');
      previousProviderRef.current = null;
      const providerModels = PROVIDERS[apiKeyProvider].models;
      setModel(providerModels[0].value);
      window.dispatchEvent(new CustomEvent('apiKeyChanged', {
        detail: {
          configured: true
        }
      }));
    } catch (err) {
      setError('Failed to save API key. Please try again.');
    } finally {
      setIsSavingKey(false);
    }
  }, [apiKeyInput, apiKeyProvider]);
  const handleRemoveProvider = useCallback(() => {
    if (!provider || typeof window === 'undefined') return;
    const confirmRemove = window.confirm(`Are you sure you want to remove the ${PROVIDERS[provider].label} API key?`);
    if (!confirmRemove) return;
    try {
      const storageKey = PROVIDERS[provider].storageKey;
      localStorage.removeItem(storageKey);
      const otherProviderKey = Object.keys(PROVIDERS).find(p => p !== provider && localStorage.getItem(PROVIDERS[p].storageKey));
      if (otherProviderKey) {
        const newApiKey = localStorage.getItem(PROVIDERS[otherProviderKey].storageKey);
        setProvider(otherProviderKey);
        setApiKey(newApiKey);
        setModel(PROVIDERS[otherProviderKey].models[0].value);
        localStorage.setItem(PROVIDER_STORAGE_KEY, otherProviderKey);
      } else {
        setApiKey('');
        setProvider('gemini');
        localStorage.removeItem(PROVIDER_STORAGE_KEY);
        if (!forceSettingsOpen) {
          setIsSettingsOpen(false);
        }
      }
      window.dispatchEvent(new CustomEvent('apiKeyChanged', {
        detail: {
          configured: !!otherProviderKey
        }
      }));
    } catch (err) {
      setError('Failed to remove provider. Please try again.');
    }
  }, [provider, forceSettingsOpen]);
  const handleSubmit = useCallback(async e => {
    if (e?.preventDefault) {
      e.preventDefault();
    }
    if (!apiKey || isLoading || !isFormValid) {
      return;
    }
    setIsLoading(true);
    setError('');
    setHasSubmitted(true);
    if (mode === 'advanced') {
      setLastResponseJson(null);
    }
    let requestMessages = [];
    let userMessageContent = '';
    if (mode === 'chat') {
      const filteredInput = filterComments(input);
      if (!filteredInput) {
        setError('Please enter a prompt (comments are not sent to the API)');
        setIsLoading(false);
        return;
      }
      userMessageContent = filteredInput;
      requestMessages = [{
        role: 'user',
        content: filteredInput
      }];
      setConversationHistory(prev => [...prev, {
        role: 'user',
        content: filteredInput
      }]);
      if (!keepInput) {
        setInput('');
      }
    } else {
      const advancedMessages = constructAdvancedMessages();
      if (advancedMessages.length === 0) {
        setError('Please fill in at least one field in Advanced mode');
        setIsLoading(false);
        return;
      }
      requestMessages = advancedMessages;
    }
    const isAnthropic = provider === 'anthropic';
    const jsonPayload = isAnthropic ? {
      model,
      messages: requestMessages.filter(m => m.role !== 'system'),
      ...requestMessages.find(m => m.role === 'system') && ({
        system: requestMessages.find(m => m.role === 'system').content
      }),
      temperature,
      top_p: topP,
      max_tokens: MAX_TOKENS
    } : {
      model,
      messages: requestMessages,
      temperature,
      top_p: topP,
      max_tokens: MAX_TOKENS
    };
    setLastSentJson(jsonPayload);
    try {
      const apiUrl = PROVIDERS[provider].apiUrl;
      const maxRetries = 3;
      let response;
      const headers = isAnthropic ? {
        'Content-Type': 'application/json',
        'x-api-key': apiKey,
        'anthropic-version': '2023-06-01',
        'anthropic-dangerous-direct-browser-access': 'true'
      } : {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${apiKey}`
      };
      for (let attempt = 0; attempt <= maxRetries; attempt++) {
        response = await fetch(apiUrl, {
          method: 'POST',
          headers,
          body: JSON.stringify(jsonPayload)
        });
        if (response.status === 429 && attempt < maxRetries) {
          const delay = Math.pow(2, attempt) * 1000;
          setError(`Rate limited. Retrying in ${delay / 1000}s... (attempt ${attempt + 1}/${maxRetries})`);
          await new Promise(resolve => setTimeout(resolve, delay));
          setError('');
          continue;
        }
        break;
      }
      if (!response.ok) {
        const errorData = await response.json().catch(() => ({}));
        if (response.status === 429) {
          throw Object.assign(new Error('Rate limit exceeded. You have used all your free tier quota.'), {
            isRateLimit: true
          });
        }
        throw new Error(errorData.error?.message || `HTTP ${response.status}: Failed to get response from ${PROVIDERS[provider].label}`);
      }
      const data = await response.json();
      const newResponse = isAnthropic ? data.content?.[0]?.text || 'No response generated' : data.choices?.[0]?.message?.content || 'No response generated';
      setLastResponseJson(data);
      if (mode === 'advanced') {
        setOutput(newResponse);
      } else {
        setConversationHistory(prev => [...prev, {
          role: 'assistant',
          content: newResponse
        }]);
        setOutput(newResponse);
      }
    } catch (err) {
      setError(err.isRateLimit ? {
        message: err.message,
        isRateLimit: true
      } : err.message || 'An error occurred while processing your request');
      if (mode === 'chat') {
        setConversationHistory(prev => prev.slice(0, -1));
      }
    } finally {
      setIsLoading(false);
    }
  }, [mode, input, apiKey, provider, isLoading, keepInput, constructAdvancedMessages, filterComments, isFormValid]);
  const errorMessage = typeof error === 'object' ? error.message : error;
  const isRateLimitError = typeof error === 'object' && error.isRateLimit;
  const renderErrorContent = () => <>
      {errorMessage}
      {isRateLimitError && <>
          {' '}
          <a href="https://ai.google.dev/gemini-api/docs/rate-limits" target="_blank" rel="noopener noreferrer" style={{
    color: 'inherit',
    textDecoration: 'underline'
  }}>
            Check your rate limits
          </a>
        </>}
    </>;
  const renderMarkdown = text => {
    const parts = text.split(/(\*\*[^*]+\*\*)/g);
    return parts.map((part, i) => {
      if (part.startsWith('**') && part.endsWith('**')) {
        return <strong key={i}>{part.slice(2, -2)}</strong>;
      }
      return part;
    });
  };
  const renderChatMessage = (message, index, isPlaceholder = false) => {
    const isUser = message.role === 'user';
    return <div key={index} className={isUser ? 'llm-chat-message llm-chat-message-user' : 'llm-chat-message llm-chat-message-assistant'}>
        <div className={`llm-chat-bubble ${isUser ? 'llm-chat-bubble-user' : 'llm-chat-bubble-assistant'} ${isPlaceholder ? 'llm-chat-bubble-placeholder' : ''}`}>
          {renderMarkdown(message.content)}
        </div>
      </div>;
  };
  const renderChatInterface = () => {
    const allMessages = [...conversationHistory];
    const placeholderMessages = [];
    if (response && !hasSubmitted && conversationHistory.length === 0) {
      if (defaultInput && defaultInput.trim()) {
        const filteredInput = filterComments(defaultInput);
        if (filteredInput) {
          placeholderMessages.push({
            role: 'user',
            content: filteredInput,
            isPlaceholder: true
          });
          placeholderMessages.push({
            role: 'assistant',
            content: response,
            isPlaceholder: true
          });
        }
      }
    }
    const hasPlaceholderMessages = placeholderMessages.length > 0;
    return <div className="llm-chat-interface">
        {hasPlaceholderMessages && <div className="llm-warning-banner">
            <IconWarningSun size={14} />
            <span>This is a sample conversation. Click the send button to make a real API call.</span>
          </div>}
        {allMessages.length === 0 && placeholderMessages.length === 0 && !isLoading && <div className="llm-chat-empty-state">
            Start a conversation using the API key you provided!
          </div>}
        {placeholderMessages.map((msg, idx) => renderChatMessage(msg, `placeholder-${idx}`, true))}
        {allMessages.map((msg, idx) => renderChatMessage(msg, `real-${idx}`, false))}
        {isLoading && <div className="llm-chat-loading">
            <div className="llm-chat-loading-bubble">
              <IconLoadingSpinner size={14} className="llm-chat-loading-spinner" />
              <span>Thinking...</span>
            </div>
          </div>}
        <div ref={messagesEndRef} />
      </div>;
  };
  const renderApiKeyForm = () => <div className="llm-api-key-form">
      <div className="llm-api-key-form-section">
        <h3 className="llm-api-key-form-title">
          <IconWarningTriangle size={16} strokeColor="#f59e0b" />
          <span>Configure an API Key to make the most of the tutorial</span>
        </h3>
        <p className="llm-api-key-form-description">
          Your API key is stored locally in your browser's storage and is never transmitted to external servers.
        </p>
      </div>

      <div className="llm-provider-tabs">
        {Object.entries(PROVIDERS).map(([key, prov]) => <button key={key} type="button" onClick={() => {
    setApiKeyProvider(key);
    setError('');
    setApiKeyInput('');
  }} className={`llm-provider-tab ${apiKeyProvider === key ? 'llm-provider-tab-active' : ''}`}>
            {prov.label}
            {key === 'gemini' && <span className="llm-provider-tab-badge">Free Tier</span>}
          </button>)}
      </div>

      {apiKeyProvider === 'gemini' && <div className="llm-gemini-recommendation">
          Gemini offers a generous free tier — great for learning! Get your free API key at{' '}
          <a href="https://aistudio.google.com/apikey" target="_blank" rel="noopener noreferrer" className="llm-api-key-form-link">
            aistudio.google.com/apikey
          </a>
        </div>}

      {apiKeyProvider === 'openai' && <p className="llm-api-key-form-description" style={{
    marginTop: '4px'
  }}>
          <a href="https://platform.openai.com/api-keys" target="_blank" rel="noopener noreferrer" className="llm-api-key-form-link">
            Don't have an API key? Get one here
          </a>
        </p>}

      <form onSubmit={handleApiKeySubmit}>
        <div className="llm-form-group">
          <div className="llm-form-row">
            <input id="api-key-input" type="password" value={apiKeyInput} onChange={e => {
    setApiKeyInput(e.target.value);
    setError('');
  }} placeholder={apiKeyProvider === 'openai' ? 'OpenAI API Key (sk-...)' : 'Gemini API Key'} disabled={isSavingKey} className={error ? 'llm-api-key-input llm-api-key-input-error' : 'llm-api-key-input'} onBlur={e => {
    e.target.style.borderColor = error ? 'var(--llm-accent-red)' : 'var(--llm-border-color)';
  }} />
            <button type="submit" disabled={isSavingKey || !apiKeyInput.trim()} className="llm-api-key-save-button">
              {isSavingKey ? 'Saving...' : 'Save'}
            </button>
            {previousProviderRef.current && <button type="button" className="llm-api-key-cancel-button" onClick={() => {
    const prev = previousProviderRef.current;
    const prevKey = localStorage.getItem(PROVIDERS[prev].storageKey);
    setProvider(prev);
    setApiKey(prevKey || '');
    setModel(PROVIDERS[prev].models[0].value);
    localStorage.setItem(PROVIDER_STORAGE_KEY, prev);
    setApiKeyInput('');
    setError('');
    previousProviderRef.current = null;
  }}>
                Cancel
              </button>}
          </div>
        </div>
        {error && <div className="llm-error-message llm-error-message-small">
            {renderErrorContent()}
          </div>}
      </form>
    </div>;
  const renderChatInput = () => {
    return <div className="llm-chat-input-container">
        <textarea ref={textareaRef} value={input} onChange={e => setInput(e.target.value)} placeholder="Type a message" disabled={isLoading} className="llm-chat-input-textarea" />
      </div>;
  };
  const renderAdvancedInput = () => <div className="llm-advanced-input-container">
      {messages.map((message, index) => <div key={index} className="llm-advanced-message-box">
          <div className="llm-advanced-message-header">
            <div className="llm-advanced-message-header-row">
              <span className="llm-advanced-message-number">
                #{index + 1}
              </span>
              <select value={message.role} onChange={e => {
    const newMessages = [...messages];
    newMessages[index].role = e.target.value;
    setMessages(newMessages);
  }} disabled={isLoading} className={`llm-advanced-role-select ${message.role === 'system' ? 'llm-advanced-role-select-system' : message.role === 'user' ? 'llm-advanced-role-select-user' : 'llm-advanced-role-select-assistant'}`}>
                <option value="system">system</option>
                <option value="user">user</option>
                <option value="assistant">assistant</option>
              </select>
            </div>
            <button type="button" onClick={() => {
    const newMessages = messages.filter((_, i) => i !== index);
    setMessages(newMessages.length > 0 ? newMessages : [{
      role: 'system',
      content: ''
    }]);
  }} disabled={isLoading || messages.length <= 1} className="llm-advanced-remove-button" title="Remove message">
              <IconClose size={10} />
            </button>
          </div>
          <textarea ref={el => {
    if (el) {
      advancedTextareaRefs.current[index] = el;
    }
  }} value={message.content} onChange={e => {
    const newMessages = [...messages];
    newMessages[index].content = e.target.value;
    setMessages(newMessages);
    autoResizeTextarea(e.target);
  }} placeholder={`Enter ${message.role} message content...`} disabled={isLoading} className="llm-textarea-base llm-textarea-enabled llm-advanced-textarea" />
        </div>)}

      <button type="button" onClick={() => {
    setMessages([...messages, {
      role: 'user',
      content: ''
    }]);
  }} disabled={isLoading} className="llm-advanced-add-button">
        <IconPlus size={12} />
        Add Message
      </button>
    </div>;
  return <div className={`code-editor-wrapper ${isMaximized ? 'maximized' : ''} ${isCollapsed ? 'collapsed' : ''}`} data-theme={theme}>
      <div className="code-editor-header">
          <div className="code-editor-title">
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
              <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
            </svg>
            {headerTitle}
          </div>
          <div className="code-editor-controls">
            {!isMaximized && <button className="code-editor-collapse-button" onClick={toggleCollapse} title={isCollapsed ? "Expand" : "Collapse"} type="button">
              <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                {isCollapsed ? <polyline points="6 9 12 15 18 9" /> : <polyline points="6 15 12 9 18 15" />}
              </svg>
            </button>}
            <button className="code-editor-maximize-button" onClick={toggleMaximize} title={isMaximized ? "Minimize" : "Maximize (Focus Mode)"} type="button">
              <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                {isMaximized ? <><path d="M4 14h6v6" /><path d="M20 10h-6V4" /><path d="M14 10l7-7" /><path d="M3 21l7-7" /></> : <><path d="M15 3h6v6" /><path d="M9 21H3v-6" /><path d="M21 3l-7 7" /><path d="M3 21l7-7" /></>}
              </svg>
            </button>
          </div>
        </div>

      {!isCollapsed && <div className={mode === 'chat' ? 'llm-playground-container llm-playground-container-chat llm-playground-container-base' : 'llm-playground-container llm-playground-container-base'} style={{
    height: isMaximized ? 'auto' : height,
    flex: isMaximized ? 1 : 'none'
  }}>
        <div className="llm-playground-main">
          <div className="llm-playground-content">
            {mode === 'chat' ? <>
                {}
                {!apiKey && <div className="llm-api-key-form-wrapper">
                    {renderApiKeyForm()}
                  </div>}

                {}
                {renderChatInterface()}

                {}
                {error && <div className="llm-error-wrapper">
                    <div className="llm-error-message">
                      <IconError size={14} />
                      {renderErrorContent()}
                    </div>
                  </div>}

                {}
                <div className="llm-playground-input-area">
                  <div className="llm-chat-input-wrapper">
                    {renderChatInput()}
                  </div>
                  {apiKey && <button onClick={handleSubmit} disabled={isLoading || !isFormValid} className="llm-chat-send-button" aria-label="Send message">
                      {isLoading ? <IconLoadingSpinner size={16} strokeColor="#000" strokeWidth="2.5" strokeLinecap="round" className="llm-chat-send-spinner" /> : <IconSend size={16} fillColor="#000" className="llm-chat-send-icon" />}
                    </button>}
                </div>
              </> : <>
                {}
                <div className="llm-playground-advanced-input-area" style={{
    flex: hasSubmitted || response && response.trim() ? '0 0 auto' : '1 1 100%',
    borderBottom: hasSubmitted || response && response.trim() ? '1px solid var(--llm-bg-secondary)' : 'none'
  }}>
                  {renderAdvancedInput()}
                  {!apiKey && renderApiKeyForm()}
                  {error && <div className="llm-error-message llm-error-message-top">
                      <IconError size={14} />
                      {renderErrorContent()}
                    </div>}
                </div>

                {(hasSubmitted || response && response.trim()) && <div ref={responseAreaRef} className="llm-playground-response-area">
                    {!apiKey && !(response && response.trim()) ? <>
                        <label className="llm-section-label">Prompt</label>
                        <textarea value={input} onChange={e => setInput(e.target.value)} placeholder="Enter your prompt here... (Configure API key to enable)" disabled={true} className="llm-textarea-base llm-textarea-disabled" />
                      </> : <>
                        {output ? <div className="llm-response-box">{renderMarkdown(output)}</div> : isLoading ? <div className="llm-tab-loading">
                            <span className="llm-tab-loading-content">
                              <IconLoadingSpinner size={14} className="llm-chat-loading-spinner" />
                              Generating response...
                            </span>
                          </div> : response ? <div className="llm-response-section">
                            <label className="llm-section-label">Response (Expected response, press submit to get actual response)</label>
                            <div className="llm-response-box llm-response-box-placeholder">
                              {response}
                            </div>
                          </div> : <div className="llm-tab-empty">
                            Response will appear here
                          </div>}
                      </>}
                  </div>}

                {(apiKey || response && response.trim()) && <div className="llm-playground-actions-area">
                    {apiKey ? <button onClick={handleSubmit} disabled={isLoading || !isFormValid} className="llm-submit-button">
                        {isLoading ? <span className="llm-submit-button-loading">
                            <IconLoadingSpinner size={12} className="llm-submit-button-loading-spinner" />
                            Processing...
                          </span> : 'Submit'}
                      </button> : <div className="llm-config-message">
                        Configure API key above to submit and get actual response
                      </div>}
                  </div>}
              </>}
          </div>

          {isSettingsOpen && <div className="llm-settings-panel" style={{
    width: `${SETTINGS_PANEL_WIDTH}px`
  }}>
              <h4 className="llm-settings-title">Settings</h4>

              <div>
                <label className="llm-settings-label">Provider</label>
                <div className="llm-settings-row-with-action">
                  <select value={provider} onChange={e => {
    const newProvider = e.target.value;
    const storageKey = PROVIDERS[newProvider].storageKey;
    const storedKey = localStorage.getItem(storageKey);
    if (apiKey) {
      previousProviderRef.current = provider;
    }
    setProvider(newProvider);
    setModel(PROVIDERS[newProvider].models[0].value);
    localStorage.setItem(PROVIDER_STORAGE_KEY, newProvider);
    if (storedKey) {
      setApiKey(storedKey);
    } else {
      setApiKey('');
    }
  }} disabled={isLoading} className="llm-settings-select">
                    {Object.entries(PROVIDERS).map(([key, prov]) => {
    const hasKey = typeof window !== 'undefined' && localStorage.getItem(prov.storageKey);
    return <option key={key} value={key} className="llm-settings-option">
                          {prov.label}{!hasKey ? ' (no key)' : ''}
                        </option>;
  })}
                  </select>
                  <button type="button" onClick={handleRemoveProvider} disabled={isLoading} className="llm-settings-remove-button" title="Remove Provider">
                    <IconTrash size={14} />
                  </button>
                </div>
              </div>

              <div>
                <label className="llm-settings-label">
                  Model
                </label>
                <select value={model} onChange={e => setModel(e.target.value)} disabled={isLoading} className="llm-settings-select">
                  {PROVIDERS[provider].models.map(m => <option key={m.value} value={m.value} className="llm-settings-option">
                      {m.label}
                    </option>)}
                </select>
              </div>

              <div>
                <div className="llm-settings-range-container">
                  <label className="llm-settings-label">
                    Temperature
                    <span className="llm-settings-info" title="Controls randomness. Lower values make output more focused and deterministic. Higher values make it more creative and varied.">
                      <IconInfo size={11} />
                    </span>
                  </label>
                  <span className="llm-settings-range-value">
                    {temperature.toFixed(1)}
                  </span>
                </div>
                <input type="range" min="0" max="2" step="0.1" value={temperature} onChange={e => setTemperature(parseFloat(e.target.value))} disabled={isLoading} className="llm-settings-range" />
                <div className="llm-settings-range-labels">
                  <span>0.0</span>
                  <span>1.0</span>
                  <span>2.0</span>
                </div>
              </div>

              <div>
                <div className="llm-settings-range-container">
                  <label className="llm-settings-label">
                    Top P
                    <span className="llm-settings-info" title="Nucleus sampling. Controls the cumulative probability cutoff. Lower values consider fewer tokens, making output more focused. Usually adjusted as an alternative to temperature.">
                      <IconInfo size={11} />
                    </span>
                  </label>
                  <span className="llm-settings-range-value">
                    {topP.toFixed(2)}
                  </span>
                </div>
                <input type="range" min="0" max="1" step="0.05" value={topP} onChange={e => setTopP(parseFloat(e.target.value))} disabled={isLoading} className="llm-settings-range" />
                <div className="llm-settings-range-labels">
                  <span>0.0</span>
                  <span>0.5</span>
                  <span>1.0</span>
                </div>
              </div>

            </div>}
        </div>

        {isApiCallsOpen && (lastSentJson || lastResponseJson) && <div className="llm-api-calls-content">
            <div className="llm-api-calls-tabs">
              {lastSentJson && <button type="button" className={`llm-api-calls-tab${apiCallTab === 'request' ? ' llm-api-calls-tab-active' : ''}`} onClick={() => setApiCallTab('request')}>
                  Request
                </button>}
              {lastResponseJson && <button type="button" className={`llm-api-calls-tab${apiCallTab === 'response' ? ' llm-api-calls-tab-active' : ''}`} onClick={() => setApiCallTab('response')}>
                  Response
                </button>}
            </div>
            {apiCallTab === 'request' && lastSentJson && <div className="llm-api-calls-block">
                <div className="llm-textarea-base llm-textarea-enabled llm-json-viewer">
                  {JSON.stringify(lastSentJson, null, 2)}
                </div>
              </div>}
            {apiCallTab === 'response' && lastResponseJson && <div className="llm-api-calls-block">
                <div className="llm-textarea-base llm-textarea-enabled llm-json-viewer">
                  {JSON.stringify(lastResponseJson, null, 2)}
                </div>
              </div>}
          </div>}

        <div className="llm-footer">
          <div className="llm-footer-status-container">
            <span>{PROVIDERS[provider].label} key:</span>
            <div className="llm-footer-status-icon" title={apiKey ? 'API Key configured' : 'API Key not configured'}>
              {apiKey ? <IconCheckmark size={14} strokeColor="#22c55e" /> : <IconXStatus size={14} strokeColor="#ef4444" />}
            </div>
          </div>
          <div className="llm-footer-actions">
            {hasSubmitted && (lastSentJson || lastResponseJson) && <button type="button" onClick={() => setIsApiCallsOpen(!isApiCallsOpen)} className={isApiCallsOpen ? 'llm-footer-toggle-button llm-footer-toggle-button-active' : 'llm-footer-toggle-button'} aria-label="Toggle API Calls">
                <IconChevron size={12} isOpen={isApiCallsOpen} className={isApiCallsOpen ? 'llm-footer-toggle-icon llm-footer-toggle-icon-open' : 'llm-footer-toggle-icon'} />
                <span>API Calls</span>
              </button>}
            <button onClick={() => {
    if (!forceSettingsOpen) {
      setIsSettingsOpen(!isSettingsOpen);
    }
  }} disabled={forceSettingsOpen} className={isSettingsOpen ? 'llm-footer-toggle-button llm-footer-toggle-button-active' : 'llm-footer-toggle-button'} aria-label="Toggle settings">
              <IconChevron size={12} isOpen={isSettingsOpen} className={isSettingsOpen ? 'llm-footer-toggle-icon llm-footer-toggle-icon-open' : 'llm-footer-toggle-icon'} />
              <span>Settings</span>
            </button>
          </div>
        </div>
      </div>}
    </div>;
};

export const CodeEditor = ({file = 'src/hello_world.ts', lines, title = 'Code Example', repo = 'ai-tutorial/typescript-examples', height = '650px', functionName, theme: userTheme}) => {
  const STORAGE_KEY = 'openai_api_key';
  const GEMINI_STORAGE_KEY = 'gemini_api_key';
  const ANTHROPIC_STORAGE_KEY = 'anthropic_api_key';
  const PROVIDER_STORAGE_KEY = 'llm_playground_provider';
  if (!functionName) {
    console.warn('CodeEditor: functionName parameter is required');
  }
  const hasCreatedEnvRef = useRef(false);
  const vmRef = useRef(null);
  const [isMaximized, setIsMaximized] = useState(false);
  const [isCollapsed, setIsCollapsed] = useState(false);
  const [isStuck, setIsStuck] = useState(false);
  const [iframeKey, setIframeKey] = useState(0);
  const [showApiKeyDialog, setShowApiKeyDialog] = useState(false);
  const [apiKey, setApiKey] = useState('');
  const [error, setError] = useState('');
  const [success, setSuccess] = useState(false);
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [isValidating, setIsValidating] = useState(false);
  const [detectedTheme, setDetectedTheme] = useState('dark');
  useEffect(() => {
    if (typeof window === 'undefined') return;
    const checkTheme = () => {
      const isDark = document.documentElement.classList.contains('dark');
      setDetectedTheme(isDark ? 'dark' : 'light');
    };
    checkTheme();
    const observer = new MutationObserver(checkTheme);
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ['class']
    });
    return () => observer.disconnect();
  }, []);
  const theme = userTheme || detectedTheme;
  const [selectedProvider, setSelectedProvider] = useState(() => {
    if (typeof window === 'undefined') return 'gemini';
    return localStorage.getItem(PROVIDER_STORAGE_KEY) || 'gemini';
  });
  const isApiKeyConfigured = () => {
    const openaiKey = localStorage.getItem(STORAGE_KEY);
    const geminiKey = localStorage.getItem(GEMINI_STORAGE_KEY);
    const anthropicKey = localStorage.getItem(ANTHROPIC_STORAGE_KEY);
    return openaiKey !== null && openaiKey.trim().length > 0 || geminiKey !== null && geminiKey.trim().length > 0 || anthropicKey !== null && anthropicKey.trim().length > 0;
  };
  const dispatchApiKeyChanged = () => {
    if (typeof window !== 'undefined' && window.dispatchEvent) {
      window.dispatchEvent(new CustomEvent('apiKeyChanged', {
        detail: {
          configured: isApiKeyConfigured()
        }
      }));
    }
  };
  const saveApiKey = apiKey => {
    if (apiKey && apiKey.trim()) {
      const trimmedKey = apiKey.trim();
      localStorage.setItem(STORAGE_KEY, trimmedKey);
      dispatchApiKeyChanged();
      return true;
    }
    return false;
  };
  const buildEnvContent = () => {
    const openaiKey = localStorage.getItem(STORAGE_KEY)?.trim();
    const geminiKey = localStorage.getItem(GEMINI_STORAGE_KEY)?.trim();
    const anthropicKey = localStorage.getItem(ANTHROPIC_STORAGE_KEY)?.trim();
    if (!openaiKey && !geminiKey && !anthropicKey) {
      return `OPENAI_MODEL=gpt-4.1-nano
OPENAI_API_KEY=sk-mock-key-1234567890abcdef
GEMINI_MODEL=gemini-2.5-flash-lite
GOOGLE_GENERATIVE_AI_API_KEY=
GOOGLE_API_KEY=
ANTHROPIC_API_KEY=
AI_PROVIDER=openai
# API key not found in browser storage
# To configure your API key:
# 1. For Gemini (free): Go to https://aistudio.google.com/apikey
# 2. For OpenAI: Go to https://platform.openai.com/api-keys
# 3. For Claude: Go to https://console.anthropic.com/settings/keys
# 4. Enter it in the configuration form above this editor
# 5. The .env file will be automatically updated with your key`;
    }
    const envLines = ['# Using the API key(s) you configured. This file will be created when the dialog is loaded.'];
    if (openaiKey) {
      envLines.push(`OPENAI_MODEL=gpt-4.1-nano`);
      envLines.push(`OPENAI_API_KEY=${openaiKey}`);
    }
    if (geminiKey) {
      envLines.push(`GEMINI_MODEL=gemini-2.5-flash-lite`);
      envLines.push(`# Vercel AI SDK uses GOOGLE_GENERATIVE_AI_API_KEY, LangChain uses GOOGLE_API_KEY`);
      envLines.push(`GOOGLE_GENERATIVE_AI_API_KEY=${geminiKey}`);
      envLines.push(`GOOGLE_API_KEY=${geminiKey}`);
    }
    if (anthropicKey) {
      envLines.push(`ANTHROPIC_API_KEY=${anthropicKey}`);
    }
    const provider = anthropicKey ? 'anthropic' : geminiKey ? 'gemini' : 'openai';
    envLines.push(`AI_PROVIDER=${provider}`);
    return envLines.join('\n');
  };
  const updateEnvFile = async vm => {
    if (!vm) return;
    try {
      await vm.applyFsDiff({
        create: {
          'env/.env': buildEnvContent(),
          'env/run.conf': `file=${file}`
        },
        destroy: []
      });
      hasCreatedEnvRef.current = true;
    } catch (error) {
      console.error('Failed to write env files:', error);
      hasCreatedEnvRef.current = false;
    }
  };
  useEffect(() => {
    if (!isApiKeyConfigured()) {
      setShowApiKeyDialog(true);
    }
    const handleApiKeyChanged = () => {
      if (isApiKeyConfigured()) {
        setShowApiKeyDialog(false);
      }
    };
    if (typeof window !== 'undefined') {
      window.addEventListener('apiKeyChanged', handleApiKeyChanged);
      return () => {
        window.removeEventListener('apiKeyChanged', handleApiKeyChanged);
      };
    }
  }, []);
  const validateApiKey = async (key, provider) => {
    try {
      const urls = {
        gemini: 'https://generativelanguage.googleapis.com/v1beta/models?key=' + encodeURIComponent(key.trim()),
        openai: 'https://api.openai.com/v1/models',
        anthropic: 'https://api.anthropic.com/v1/models'
      };
      const headerMap = {
        gemini: {
          'Content-Type': 'application/json'
        },
        openai: {
          'Authorization': `Bearer ${key.trim()}`,
          'Content-Type': 'application/json'
        },
        anthropic: {
          'x-api-key': key.trim(),
          'anthropic-version': '2023-06-01',
          'Content-Type': 'application/json'
        }
      };
      const url = urls[provider];
      const headers = headerMap[provider];
      const response = await fetch(url, {
        method: 'GET',
        headers
      });
      if (response.ok) {
        return {
          valid: true
        };
      } else if (response.status === 401 || response.status === 403) {
        return {
          valid: false,
          error: 'Invalid API key. Please check your key and try again.'
        };
      } else if (response.status === 429) {
        return {
          valid: false,
          error: 'Rate limit exceeded. Please try again later.'
        };
      } else {
        const errorData = await response.json().catch(() => ({}));
        return {
          valid: false,
          error: errorData.error?.message || `API request failed with status ${response.status}`
        };
      }
    } catch (err) {
      if (err.name === 'TypeError' && err.message.includes('fetch')) {
        return {
          valid: false,
          error: 'Network error. Please check your connection and try again.'
        };
      }
      return {
        valid: false,
        error: err.message || 'Failed to validate API key. Please try again.'
      };
    }
  };
  const handleSkipConfiguration = () => {
    const skipKey = 'sk-<configure-your-key>';
    saveApiKey(skipKey);
    setShowApiKeyDialog(false);
  };
  const handleApiKeySubmit = async e => {
    e.preventDefault();
    setError('');
    setSuccess(false);
    setIsSubmitting(true);
    const providerNames = {
      gemini: 'Gemini',
      openai: 'OpenAI',
      anthropic: 'Claude'
    };
    if (!apiKey || !apiKey.trim()) {
      setError(`Please enter your ${providerNames[selectedProvider]} API key`);
      setIsSubmitting(false);
      return;
    }
    const trimmedKey = apiKey.trim();
    if (selectedProvider === 'openai' && !trimmedKey.startsWith('sk-')) {
      setError('Invalid API key format. OpenAI API keys should start with "sk-"');
      setIsSubmitting(false);
      return;
    }
    if (selectedProvider === 'anthropic' && !trimmedKey.startsWith('sk-ant-')) {
      setError('Invalid API key format. Anthropic API keys should start with "sk-ant-"');
      setIsSubmitting(false);
      return;
    }
    setIsValidating(true);
    setError('');
    const validation = await validateApiKey(trimmedKey, selectedProvider);
    setIsValidating(false);
    if (!validation.valid) {
      setError(validation.error || 'Invalid API key. Please check your key and try again.');
      setIsSubmitting(false);
      return;
    }
    try {
      const storageKeys = {
        gemini: GEMINI_STORAGE_KEY,
        openai: STORAGE_KEY,
        anthropic: ANTHROPIC_STORAGE_KEY
      };
      localStorage.setItem(storageKeys[selectedProvider], trimmedKey);
      localStorage.setItem(PROVIDER_STORAGE_KEY, selectedProvider);
      dispatchApiKeyChanged();
      setSuccess(true);
      setApiKey('');
      setTimeout(() => {
        window.location.reload();
      }, 1000);
    } catch (err) {
      setError(err.message || 'Failed to save API key. Please try again.');
      setIsSubmitting(false);
    }
  };
  const baseFilePath = file || 'src/hello_world.ts';
  let filePath = baseFilePath;
  if (typeof lines === 'string' && lines.trim()) {
    const lineParts = lines.split('-');
    if (lineParts.length === 2) {
      filePath = `${filePath}:L${lineParts[0].trim()}-L${lineParts[1].trim()}`;
    } else {
      filePath = `${filePath}:L${lineParts[0].trim()}`;
    }
  } else if (typeof lines === 'object' && lines.start !== undefined) {
    filePath = lines.end !== undefined ? `${filePath}:L${lines.start}-L${lines.end}` : `${filePath}:L${lines.start}`;
  }
  const stackblitzUrl = `https://stackblitz.com/github/${repo}?file=${encodeURIComponent(filePath)}&embed=1&view=editor&theme=${theme}`;
  const loadSDK = () => {
    return new Promise((resolve, reject) => {
      if (window.StackBlitzSDK || window.stackblitzSDK) {
        resolve(window.StackBlitzSDK || window.stackblitzSDK);
        return;
      }
      if (document.querySelector('script[data-stackblitz-sdk]')) {
        const checkInterval = setInterval(() => {
          if (window.StackBlitzSDK || window.stackblitzSDK) {
            clearInterval(checkInterval);
            resolve(window.StackBlitzSDK || window.stackblitzSDK);
          }
        }, 100);
        setTimeout(() => {
          clearInterval(checkInterval);
          reject(new Error('SDK loading timeout'));
        }, 10000);
        return;
      }
      const script = document.createElement('script');
      script.src = 'https://unpkg.com/@stackblitz/sdk/bundles/sdk.umd.js';
      script.async = true;
      script.setAttribute('data-stackblitz-sdk', 'true');
      script.onload = () => {
        const sdk = window.StackBlitzSDK || window.stackblitzSDK;
        if (sdk) {
          resolve(sdk);
        } else {
          reject(new Error('SDK loaded but not available on window'));
        }
      };
      script.onerror = () => {
        reject(new Error('Failed to load StackBlitz SDK'));
      };
      document.head.appendChild(script);
    });
  };
  const LOAD_TIMEOUT_MS = 10000;
  const iframeElRef = useRef(null);
  const reloadCountRef = useRef(0);
  const handleRetry = () => {
    vmRef.current = null;
    hasCreatedEnvRef.current = false;
    reloadCountRef.current = 0;
    setIsStuck(false);
    setIframeKey(prev => prev + 1);
  };
  const iframeRef = iframe => {
    iframeElRef.current = iframe;
  };
  const connectToVM = async iframe => {
    const sdk = await loadSDK();
    return sdk.connect(iframe);
  };
  const handleIframeLoad = async () => {
    const iframe = iframeElRef.current;
    if (!iframe) return;
    if (reloadCountRef.current > 0) {
      try {
        const vm = await connectToVM(iframe);
        vmRef.current = vm;
        await updateEnvFile(vm);
      } catch (_) {}
      return;
    }
    try {
      if (vmRef.current) return;
      const vm = await Promise.race([connectToVM(iframe), new Promise((_, reject) => setTimeout(() => reject(new Error('connect timeout')), LOAD_TIMEOUT_MS))]);
      vmRef.current = vm;
      await updateEnvFile(vm);
    } catch (error) {
      console.error('Failed to connect to StackBlitz VM:', error);
      if (typeof window !== 'undefined' && window.gtag) {
        window.gtag('event', 'load_refresh_error', {
          event_category: 'stackblitz',
          event_label: file,
          error_message: error.message
        });
      }
      reloadCountRef.current = 1;
      setTimeout(() => {
        setIframeKey(prev => prev + 1);
      }, 2000);
    }
  };
  const isSafari = typeof navigator !== 'undefined' && (/^((?!chrome|android).)*safari/i).test(navigator.userAgent);
  if (isSafari) {
    return <div className="code-editor-dialog-container" style={{
      height: height
    }}>
        <div className="code-editor-dialog-box">
          <h2 className="code-editor-dialog-title">
            <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" strokeWidth="2">
              <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
              <line x1="12" y1="9" x2="12" y2="13"></line>
              <line x1="12" y1="17" x2="12.01" y2="17"></line>
            </svg>
            Browser Not Supported
          </h2>
          <p className="code-editor-dialog-description">
            The interactive code editor is not supported on Safari. Please use <strong>Chrome</strong>, <strong>Edge</strong>, or <strong>Firefox</strong> to run the examples.
          </p>
        </div>
      </div>;
  }
  if (showApiKeyDialog) {
    return <div className="code-editor-dialog-container" style={{
      height: height
    }}>
        <div className="code-editor-dialog-box">
          <h2 className="code-editor-dialog-title">
            <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" strokeWidth="2">
              <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
              <line x1="12" y1="9" x2="12" y2="13"></line>
              <line x1="12" y1="17" x2="12.01" y2="17"></line>
            </svg>
            Configure API Key
          </h2>

          <p className="code-editor-dialog-description">
            All interactive examples execute entirely within your browser environment, ensuring complete security and privacy.
            Your API key is stored locally in your browser's storage and is never transmitted to external servers.
          </p>

          <div className="llm-provider-tabs" style={{
      marginBottom: '16px'
    }}>
            <button type="button" onClick={() => {
      setSelectedProvider('gemini');
      setError('');
      setApiKey('');
    }} className={`llm-provider-tab ${selectedProvider === 'gemini' ? 'llm-provider-tab-active' : ''}`}>
              Gemini <span className="llm-provider-tab-badge">Free</span>
            </button>
            <button type="button" onClick={() => {
      setSelectedProvider('openai');
      setError('');
      setApiKey('');
    }} className={`llm-provider-tab ${selectedProvider === 'openai' ? 'llm-provider-tab-active' : ''}`}>
              OpenAI
            </button>
            <button type="button" onClick={() => {
      setSelectedProvider('anthropic');
      setError('');
      setApiKey('');
    }} className={`llm-provider-tab ${selectedProvider === 'anthropic' ? 'llm-provider-tab-active' : ''}`}>
              Claude
            </button>
          </div>

          {selectedProvider === 'gemini' && <div className="llm-gemini-recommendation" style={{
      marginBottom: '16px'
    }}>
              Gemini offers a generous free tier — great for learning! Get your free API key at{' '}
              <a href="https://aistudio.google.com/apikey" target="_blank" rel="noopener noreferrer" className="code-editor-link">
                aistudio.google.com/apikey
              </a>
            </div>}

          {selectedProvider === 'openai' && <div className="code-editor-info-box">
              <p className="code-editor-info-box-title">
                Don't have an API key?
              </p>
              <p className="code-editor-info-box-text">
                Get one at{' '}
                <a href="https://platform.openai.com/api-keys" target="_blank" rel="noopener noreferrer" className="code-editor-link">
                  platform.openai.com/api-keys
                </a>
              </p>
            </div>}

          {selectedProvider === 'anthropic' && <div className="code-editor-info-box">
              <p className="code-editor-info-box-title">
                Don't have an API key?
              </p>
              <p className="code-editor-info-box-text">
                Get one at{' '}
                <a href="https://console.anthropic.com/settings/keys" target="_blank" rel="noopener noreferrer" className="code-editor-link">
                  console.anthropic.com/settings/keys
                </a>
              </p>
            </div>}

          <form onSubmit={handleApiKeySubmit}>
            <div className="code-editor-form-group">
              <label htmlFor="api-key-input" className="code-editor-label">
                {selectedProvider === 'gemini' ? 'Gemini' : 'OpenAI'} API Key
              </label>
              <input id="api-key-input" type="password" value={apiKey} onChange={e => {
      setApiKey(e.target.value);
      setError('');
      setSuccess(false);
    }} placeholder={selectedProvider === 'openai' ? 'sk-...' : 'Gemini API Key'} disabled={isSubmitting} className={`code-editor-input ${error ? 'code-editor-input-error' : ''}`} />
            </div>

            {isValidating && <div className="code-editor-message code-editor-message-info">
                <span className="code-editor-message-icon">⏳</span>
                <span>Validating API key...</span>
              </div>}

            {error && !isValidating && <div className="code-editor-message code-editor-message-error">
                <span className="code-editor-message-icon">⚠️</span>
                <span>{error}</span>
              </div>}

            {success && <div className="code-editor-message code-editor-message-success">
                <span className="code-editor-message-icon">✓</span>
                <span>API key saved successfully! Loading editor...</span>
              </div>}

            <button type="submit" disabled={isSubmitting || isValidating || !apiKey.trim()} className="code-editor-button">
              {isValidating ? 'Validating...' : isSubmitting ? 'Saving...' : 'Save API Key'}
            </button>
          </form>

          <button type="button" onClick={handleSkipConfiguration} disabled={isSubmitting || isValidating} className="code-editor-button-secondary">
            Skip Configuration
          </button>

          <div className="code-editor-footer">
            <p className="code-editor-footer-text">
              Alternatively, you may checkout the source code from{' '}
              <a href="https://github.com/ai-tutorial/typescript-examples" target="_blank" rel="noopener noreferrer" className="code-editor-link code-editor-link-break">
                https://github.com/ai-tutorial/typescript-examples
              </a>
              {' '}and run the examples locally.
            </p>
          </div>
        </div>
      </div>;
  }
  const toggleMaximize = () => setIsMaximized(!isMaximized);
  const toggleCollapse = () => setIsCollapsed(!isCollapsed);
  return <div className={`code-editor-wrapper ${isMaximized ? 'maximized' : ''} ${isCollapsed ? 'collapsed' : ''}`} data-theme={theme}>
      <div className="code-editor-header">
        <div className="code-editor-title">
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
            <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
            <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
          </svg>
          {title}
        </div>
        <div className="code-editor-controls">
          {!isMaximized && <button className="code-editor-collapse-button" onClick={toggleCollapse} title={isCollapsed ? "Expand" : "Collapse"} type="button">
              <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                {isCollapsed ? <polyline points="6 9 12 15 18 9" /> : <polyline points="6 15 12 9 18 15" />}
              </svg>
            </button>}
          <button className="code-editor-maximize-button" onClick={toggleMaximize} title={isMaximized ? "Minimize" : "Maximize (Focus Mode)"} type="button">
            <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
              {isMaximized ? <><path d="M4 14h6v6" /><path d="M20 10h-6V4" /><path d="M14 10l7-7" /><path d="M3 21l7-7" /></> : <><path d="M15 3h6v6" /><path d="M9 21H3v-6" /><path d="M21 3l-7 7" /><path d="M3 21l7-7" /></>}
            </svg>
          </button>
        </div>
      </div>

      {!isCollapsed && <div style={{
    position: 'relative',
    height: isMaximized ? 'auto' : height,
    flex: isMaximized ? 1 : 'none'
  }}>
          <iframe key={iframeKey} ref={iframeRef} onLoad={handleIframeLoad} src={stackblitzUrl} className="code-editor-iframe" style={{
    height: '100%',
    flex: isMaximized ? 1 : 'none'
  }} title={title || 'Code Example'} allow="accelerometer; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts" />

          {isStuck && <div className="code-editor-stuck-overlay">
              <div className="code-editor-stuck-box">
                <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" strokeWidth="2">
                  <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
                  <line x1="12" y1="9" x2="12" y2="13"></line>
                  <line x1="12" y1="17" x2="12.01" y2="17"></line>
                </svg>
                <p>StackBlitz is taking too long to load. This can happen when the repository was recently updated.</p>
                <button type="button" className="code-editor-button" onClick={handleRetry} style={{
    marginTop: '8px'
  }}>
                  Retry
                </button>
              </div>
            </div>}
        </div>}
    </div>;
};

LLMs only know what they were trained on. RAG bridges this gap by retrieving relevant data and injecting it into the prompt. This page covers the core pattern and a production pipeline.

## Why RAG?

LLMs are powerful — but they have a fundamental limitation: **they only know what they were trained on.**

Ask an LLM about your company's Q4 revenue, yesterday's incident report, or a document you uploaded last week, and it will either hallucinate an answer or admit it doesn't know. This is the **knowledge boundary problem**, and RAG is how production systems solve it.

### The Problem RAG Solves

Try asking the model about private company data — it simply doesn't have it:

<LLMPlayground title="Playground: The Knowledge Boundary" defaultMode="chat" defaultInput={`What was MySecretCompany's Q4 2024 revenue?`} response={`I don't have access to MySecretCompany's internal financial data. Based on publicly available information, I cannot determine their Q4 2024 revenue. You would need to check their financial reports or contact the company directly for this information.`} height="400px" keepInput={true} />

<Note>
  If you try this same question in ChatGPT, you might get a real answer — but that's because ChatGPT is not a pure LLM. It has built-in tools (web browsing, code interpreter, etc.) that fetch live data behind the scenes. Under the hood, it's doing exactly what we're about to build: retrieving external data and injecting it into the prompt. The difference is that you don't control the retrieval pipeline — ChatGPT decides what to search, which sources to trust, and what context to use. Building your own RAG gives you full control over these decisions.
</Note>

### When You Need RAG

| Scenario                 | Why the LLM alone fails                                                       | What RAG adds                                                  |
| ------------------------ | ----------------------------------------------------------------------------- | -------------------------------------------------------------- |
| **Data freshness**       | Training data has a cutoff date — the model doesn't know about anything after | Retrieves current data from live sources                       |
| **Proprietary data**     | Your internal docs, databases, and APIs were never in the training set        | Connects the LLM to your private knowledge base                |
| **Accuracy & citations** | The model can hallucinate plausible-sounding but wrong answers                | Constrains generation to retrieved facts with source citations |
| **Cost**                 | Fine-tuning a model on new data is expensive and slow to update               | Just update your document store — no retraining needed         |
| **Auditability**         | You can't trace why the model said something                                  | Every answer links back to specific source documents           |

### The Simplest RAG: Three Lines of Logic

At its core, RAG is just three steps:

```text theme={null}
1. RETRIEVE  →  Find relevant documents for the user's question
2. AUGMENT   →  Insert those documents into the prompt as context
3. GENERATE  →  Have the LLM answer using only the provided context
```

That's it. Everything else — chunking, embeddings, reranking, hybrid search — is about making each step better. Let's start with the simplest working version.

## Basic RAG Implementation

The company documents live in a JSON file (`assets/company_docs.json`), but they could come from anywhere — a database, an API, a CMS, a web scraper. The RAG pattern is the same regardless of the data source.

<CodeEditor file="src/rag/basic_rag.ts" lines="52-86" functionName="main" title="Basic RAG Implementation" />

This prototype works but has clear limitations:

* No error handling
* No caching (repeated queries waste money)
* No retrieval quality measurement
* Single-stage retrieval (accuracy suffers)
* No lexical/vector hybrid search
* No metadata or filtering

We'll fix these throughout the module.

## The Real-Life RAG Pipeline

Production RAG systems go beyond "search + LLM." They use a carefully designed multi-stage pipeline where each stage solves a distinct problem.

```
+------------------+
│    User Query    │
+------------------+
        |
        v
+------------------+
│  Stage 1:        │
│  Fast Retrieval  │  ← Get 10-50 candidates per method
│  (Lexical/Vector)│     (prioritize recall over precision)
+------------------+
        |
        v
+------------------+
│  Stage 2:        │
│  Rank Fusion     │  ← Merge results from multiple retrievers
│  (RRF)           │     into a single ranked list
+------------------+
        |
        v
+------------------+
│  Stage 3:        │
│  Reranking       │  ← Narrow to top 3-5 precisely
│  (Cross-Encoder) │     (prioritize precision over speed)
+------------------+
        |
        v
+------------------+
│  Stage 4:        │
│  LLM Generation  │  ← Use retrieved context to generate
│  (GPT-4/Claude)  │     grounded response
+------------------+
        |
        v
+------------------+
│    Response      │
+------------------+
```

### Why Multi-Stage Retrieval?

**The Fundamental Trade-off:**

* Fast retrieval methods (lexical, vector) can process 100K+ documents in milliseconds
* Accurate ranking methods (cross-encoders) can only handle \~100 documents in reasonable time
* Solution: Use fast methods to filter, fuse their results, then use an accurate method to rank

**Why Rank Fusion (RRF)?**

* Different retrieval methods have different strengths — lexical search excels at exact keyword matches, while vector search captures semantic similarity
* Running both in parallel and then merging the results with **Reciprocal Rank Fusion (RRF)** gives you the best of both worlds
* RRF combines ranked lists by assigning each document a score based on its rank position across all retrievers: `score = Σ 1/(k + rank)` — documents that appear high in multiple lists bubble to the top
* This fused list is then passed to the cross-encoder reranker for precise scoring

**Latency in practice (varies by system):**

* First-pass retrieval returns a small candidate set quickly (implementation-, scale-, and hardware-dependent).
* RRF merging is near-instant — it's just arithmetic over rank positions.
* Cross-encoder reranking narrows to top results but adds additional latency.
* Production systems typically target interactive end-to-end latency budgets on available hardware.

## Production RAG: Key Components

A production RAG system needs:

1. **Document Processing Pipeline**
   * Chunking strategy (size, overlap)
   * Metadata extraction (title, date, source)
   * Quality filtering

2. **Multi-Stage Retrieval**
   * First-pass: Fast, broad recall (lexical and vector in parallel)
   * Rank fusion: Merge results from multiple retrievers via RRF
   * Reranking: Slow, precise scoring with cross-encoders

3. **Context Engineering**
   * Prompt design for grounding
   * Citation formatting
   * Handling insufficient context

4. **Evaluation Framework**
   * Retrieval metrics (Recall\@k, NDCG)
   * Generation metrics (faithfulness, relevance)
   * Component-level debugging

5. **Observability**
   * Retrieval quality monitoring
   * Latency tracking
   * Cost per query

We'll build each component step by step.

<Quiz>
  <QuizQuestion question="Your RAG system answers 'I don't know' to a question you know is in the documents. Where is the failure most likely?" options={["The LLM is too weak to generate a good answer", "Retrieval failed to find the relevant document — fix chunking, embeddings, or search strategy first", "The user's question is poorly worded"]} answer={1} explanation="When the answer exists but the system says 'I don't know,' retrieval is the bottleneck. The LLM never saw the right document. Always diagnose retrieval before tuning generation." />

  <QuizQuestion question="Should you fine-tune a model on your company's data instead of using RAG?" options={["Yes — fine-tuning is always more accurate than RAG", "Usually no — RAG is cheaper, easier to update, and provides citations; fine-tuning is for style/behavior changes, not knowledge updates", "Only if your dataset is larger than 10GB"]} answer={1} explanation="Fine-tuning changes how a model behaves, not what it knows. For injecting knowledge that changes frequently, RAG is far more practical — just update the document store." />
</Quiz>
