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

# LLM Fundamentals

> Understand what's actually happening when you call a model

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 are probabilistic text generators, not knowledge databases. Understanding how they actually work — tokenization, temperature, context windows — is the foundation for everything else in this tutorial.

## Hello World

This isn't your typical "Hello World" — we're diving straight into what makes LLMs powerful. After configuring your OpenAI API key, press Enter to see the magic happen. The code is self-explanatory, so go ahead and modify it to experiment.

<CodeEditor file="src/prompting/hello_world.ts" functionName="main" lines="20-29" title="Hello World" />

<Note>
  **Prefer to follow along locally?** You can checkout the examples repository and run the code on your machine:

  ```bash theme={null}
  git clone https://github.com/ai-tutorial/typescript-examples
  ```

  Then navigate to the specific example file and run it with your configured OpenAI API key.
</Note>

## How LLMs Process Your Input

Before we write our first prompt, let's understand what's actually happening under the hood.

**The Context Window: Your Working Memory**

Think of an LLM's context window like RAM on your computer. Everything you send - your instructions, conversation history, documents - gets loaded into this window. The model can only "see" what fits inside.

**Current Context Windows** (as of January 2025; verify latest on vendor pages below):

* GPT-4: 128K tokens (\~96K words)
* Claude Sonnet 4.5: 200K tokens (\~150K words)
* Gemini 1.5 Pro: 2M tokens (\~1.5M words)

See vendor references: [OpenAI models](https://platform.openai.com/docs/models), [Anthropic pricing/models](https://www.anthropic.com/pricing), [Google Gemini models](https://ai.google.dev/gemini-api/docs/models).

**Why This Matters:** A customer support agent handling a complex case might need:

* 2K tokens: System instructions
* 5K tokens: Company knowledge base excerpts
* 10K tokens: Conversation history
* 3K tokens: Customer account details

That's 20K tokens before the LLM generates a single word. Multiply by thousands of requests, and you see why context management matters.

**Token Economics:**

$$
input\,cost = 150K\,tokens * (\$10\,/\,1M\,tokens) = \$1.50 \newline output\,cost = 3K\,tokens * (\$30\,/\,1M\,tokens) = \$0.09 \newline total\,per\,request = \$1.59
$$

## LLM Limitations You Must Know

**1. Hallucinations: Making Stuff Up**

LLMs are trained to predict the next plausible token. They're not fact-checking databases.

**Famous Failure:** Air Canada's chatbot hallucinated a bereavement discount policy that didn't exist. The airline had to honor it in court. Cost: Unknown, but significant legal precedent. ([BBC, 2024](https://www.bbc.com/travel/article/20240222-air-canada-chatbot-misinformation-what-travellers-should-know))

**Why It Happens:**

* Missing information → fills gaps with plausible-sounding text
* Conflicting instructions → makes judgment calls
* Outdated training data → invents current information

**What Works in Production:**

* Constrain to provided context: "Only use information from these documents"
* Validate outputs: Check facts against source data
* Ground the answer in knowledge (throughout the tutorial).
* Add human review: For high-stakes decisions

**2. Non-Determinism: Different Answers Every Time**

Run the same prompt twice, get different answers. That's by design (temperature > 0). Try running the same prompt multiple times with different temperature settings to see how the model generates different responses:

<LLMPlayground
  title="Playground: Temperature & Non-Determinism"
  defaultInput="Summarize this customer complaint in 10 words:


I ordered a premium wireless headset last week with express shipping. The package arrived three days late, and the headset was damaged with scratches. The left speaker produces only static noise. I demand a full refund."
  defaultTemperature={2.0}
  height="500px"
  keepInput={true}
/>

<Tip>Press the resend button to re-run the same prompt and see how the response changes each time.</Tip>

**Production Strategy:**

Control the creativity of the model with the Temperature parameter.

* Temperature=0 for minimal creativity tasks (classification, extraction)
* Temperature=0.3-0.7 for creative tasks (writing, brainstorming)
* Temperature=2.0 is he maximum limit. This forces the model to take extreme risks, often resulting in highly creative but frequently nonsensical or unstable output.
* Run multiple tixmes and vote (self-consistency, covered in 1.5)

<Note>
  GPT-5 models do **not** support the temperature parameter, and using it will raise an error. This breaks backward compatibility with earlier OpenAI models.

  Instead, GPT-5 introduces a new way to control output variability: reasoning depth, via:To achieve similar results with reasoning effort set higher, or with another GPT-5 family model, try these alternative parameters:

  ```python theme={null}
  Reasoning depth: reasoning: { effort: "none" | "low" | "medium" | "high" }
  ```
</Note>

**3. Recency Bias: Recent Context Matters More**

LLMs pay more attention to text at the beginning and end of the prompt. Middle content gets "lost."

**Heuristic:** Prompt structure and placement can affect results. Many teams place the specific query toward the end; validate with your use case.

**Best Practice for Gemini:** See [Gemini prompting strategies](https://ai.google.dev/gemini-api/docs/prompting-strategies).

```
Critical instructions at START
[Large document content in MIDDLE]
Specific query at END
```

## Mental Model: LLMs as Completion Engines

**Wrong Mental Model:** "The AI understands my intent"\
**Right Mental Model:** "The AI completes patterns it's seen in training"

**Example:**

```python theme={null}
prompt = "The capital of France is"
result = "Paris"
# Not because it "knows" geography, but because it's seen this pattern millions of times

prompt = "The capital of Atlantis is"
result = "unknown" or makes something up
# It hasn't seen this pattern → hallucination risk
```

### Practical Implication

When you want structured output, give the LLM a pattern to complete. Compare these two approaches:

#### ❌ Antipattern

First, try the vague prompt and see how the LLM responds in prose. Then try the pattern-based approach to get structured output:

<LLMPlayground
  title="Playground: Vague Prompt (Antipattern)"
  defaultInput={`//  Vague: "Extract the customer's name and email"
// LLM might respond in prose: "The customer's name is John and his email is..."

Extract the customer's name and email from: "Hi, I'm John Smith and you can reach me at john.smith@email.com for the order inquiry."`}
  defaultTemperature={2.0}
  height="500px"
  keepInput={true}
/>

#### ✅ Best Practice

<LLMPlayground
  title="Playground: Pattern-Based Prompt"
  defaultInput={`// Pattern to complete:
// LLM completes the pattern: "Name: John Smith\nEmail: john@email.com"

Extract customer information:

Name: [the customer's name]
Email: [the customer's email address]

From: "Hi, I'm John Smith and you can reach me at john.smith@email.com for the order inquiry."`}
  defaultTemperature={2.0}
  height="500px"
  keepInput={true}
/>

<Quiz>
  <QuizQuestion question="You set temperature to 0 but the model still occasionally gives slightly different outputs. Why?" options={["Temperature 0 doesn't guarantee determinism — sampling ties, batching, and floating-point differences can still cause variation", "The model is broken — temperature 0 should always be deterministic", "The API is caching a previous response"]} answer={0} explanation="Even at temperature 0, implementation details like floating-point precision and parallel processing can cause minor variations. True determinism often requires setting a seed parameter too." />

  <QuizQuestion question="A user asks 'Is this contract legally binding?' and the model gives a confident, detailed legal analysis. What's the core risk?" options={["The response will be too long and expensive", "The model is pattern-matching legal language, not reasoning about law — it may sound authoritative while being wrong", "The model needs a legal fine-tune to answer correctly"]} answer={1} explanation="LLMs optimize for plausibility, not correctness. Legal-sounding output doesn't mean legal accuracy. This is hallucination at its most dangerous — confident and wrong." />

  <QuizQuestion question="You're building a system that extracts dates from documents. Should you rely on the LLM's knowledge of date formats?" options={["Yes — LLMs understand dates well from training data", "No — provide explicit format examples in the prompt because the model's 'understanding' is just pattern matching", "Only if you use GPT-4 or better"]} answer={1} explanation="LLMs don't truly understand dates — they match patterns. Without explicit format guidance, the model might output '03/04/2024' ambiguously (March 4 or April 3?)." />
</Quiz>
