/**
 * ConversationPage.tsx - Conversational Heritage Institution Explorer
 * 
 * A full-featured conversation page that integrates:
 * - DSPy RAG pipeline for intelligent responses
 * - Multi-database retrieval (Qdrant, Oxigraph, TypeDB)
 * - Interactive D3 geo visualizations
 * - Dynamic visualization components (maps, timelines, charts)
 * - Bilingual support (NL/EN)
 * 
 * Based on the NDEStatsPage and QueryBuilderPage patterns.
 * 
 * © 2025 Netwerk Digitaal Erfgoed & TextPast. All rights reserved.
 */

import React, { useState, useRef, useEffect, useCallback, useMemo, type RefObject } from 'react';
import { useInfiniteScroll } from '../hooks/useInfiniteScroll';
import { 
  Send, 
  Loader2, 
  Sparkles, 
  AlertCircle, 
  Copy, 
  Check, 
  ChevronDown,
  History,
  Download,
  Upload,
  Trash2,
  X,
  Map,
  BarChart3,
  Network,
  Clock,
  Table2,
  LayoutGrid,
  Maximize2,
  Minimize2,
  MapPin,
  Building2,
  Info,
  ExternalLink,
  Layers,
  Database,
  Zap,
  RefreshCw,
  Plus,
  Search,
} from 'lucide-react';
import { useLanguage } from '../contexts/LanguageContext';
import { useUIState } from '../contexts/UIStateContext';
import { useMultiDatabaseRAG, type RAGResponse, type ConversationMessage, type VisualizationType, type InstitutionData, type PaginationState, type RetrievalProgress, type RetrievalStage } from '../hooks/useMultiDatabaseRAG';
import type { CacheStats } from '../lib/storage/semantic-cache';
import { useQdrant } from '../hooks/useQdrant';
import { ConversationMapLibre, ConversationBarChart, ConversationTimeline, ConversationNetworkGraph, ConversationSocialNetworkGraph, ConversationEmbeddingPanel, ConversationKnowledgeGraphPanel } from '../components/conversation';
import type { RetrievedResult, QueryType } from '../hooks/useMultiDatabaseRAG';
import type { EmbeddingPoint } from '../components/database/EmbeddingProjector';
import type { GraphData, GraphBackend, GraphNode, GraphEdge } from '../components/database/KnowledgeGraphProjector';
import { useNavigate } from 'react-router-dom';
import './ConversationPage.css';

// ============================================================================
// Constants
// ============================================================================


// LLM Providers for dropdown selection
const LLM_PROVIDERS = [
  { id: 'zai' as const, name: 'Z.AI GLM-4.6', shortName: 'Z.AI', description: 'Free, multilingual', color: '#10b981', badge: '🆓' },
  { id: 'anthropic' as const, name: 'Claude', shortName: 'Claude', description: 'Highest quality', color: '#7c3aed', badge: '💎' },
  { id: 'openai' as const, name: 'OpenAI', shortName: 'GPT', description: 'GPT models', color: '#0ea5e9', badge: '💎' },
];

// LocalStorage keys
const STORAGE_KEYS = {
  HISTORY: 'glam-conversation-history',
  CONVERSATIONS: 'glam-conversation-saved',
  SELECTED_SOURCES: 'glam-selected-sources',
};

// Available data sources - matches backend DataSource enum
type DataSource = 'qdrant' | 'sparql' | 'typedb';

interface DataSourceConfig {
  id: DataSource;
  name: { nl: string; en: string };
  description: { nl: string; en: string };
  icon: React.ElementType;
  color: string;
}

const DATA_SOURCES: DataSourceConfig[] = [
  { 
    id: 'qdrant', 
    name: { nl: 'Vector', en: 'Vector' },
    description: { nl: 'Semantische zoekopdracht (Qdrant)', en: 'Semantic search (Qdrant)' },
    icon: Search,
    color: '#6366f1'
  },
  { 
    id: 'sparql', 
    name: { nl: 'Graaf', en: 'Graph' },
    description: { nl: 'Kennisgraaf (Oxigraph)', en: 'Knowledge graph (Oxigraph)' },
    icon: Network,
    color: '#10b981'
  },
  { 
    id: 'typedb', 
    name: { nl: 'TypeDB', en: 'TypeDB' },
    description: { nl: 'Relatie-gebaseerd', en: 'Relationship-based' },
    icon: Database,
    color: '#f59e0b'
  },
];

// Bilingual text content
const TEXT = {
  pageTitle: { nl: 'Gesprek', en: 'Conversation' },
  pageSubtitle: { 
    nl: 'Verken erfgoedinstellingen in een gesprek', 
    en: 'Explore heritage institutions through natural conversation' 
  },
  placeholder: { 
    nl: 'Stel een vraag over erfgoedinstellingen...', 
    en: 'Ask a question about heritage institutions...' 
  },
  send: { nl: 'Verstuur', en: 'Send' },
  thinking: { nl: 'Bezig met nadenken...', en: 'Thinking...' },
  searching: { nl: 'Zoeken in databases...', en: 'Searching databases...' },
  generatedQuery: { nl: 'Gegenereerde Query', en: 'Generated Query' },
  sparqlQuery: { nl: 'SPARQL Query', en: 'SPARQL Query' },
  typeqlQuery: { nl: 'TypeQL Query', en: 'TypeQL Query' },
  copied: { nl: 'Gekopieerd!', en: 'Copied!' },
  copyQuery: { nl: 'Kopieer', en: 'Copy' },
  errorTitle: { nl: 'Fout', en: 'Error' },
  errorConnection: { 
    nl: 'Kan geen verbinding maken met de databases. Controleer of de services draaien.', 
    en: 'Cannot connect to databases. Check if services are running.' 
  },
  welcomeTitle: { nl: 'Erfgoed Gesprek', en: 'Heritage Conversation' },
  welcomeDescription: { 
    nl: 'Stel vragen over erfgoedinstellingen in Nederland en wereldwijd. Ik doorzoek meerdere databases en toon resultaten met interactieve visualisaties.', 
    en: 'Ask questions about heritage institutions in the Netherlands and worldwide. I search multiple databases and show results with interactive visualizations.' 
  },
  exampleQuestions: { nl: 'Probeer:', en: 'Try:' },
  poweredBy: { nl: 'DSPy + Qdrant + Oxigraph + TypeDB', en: 'DSPy + Qdrant + Oxigraph + TypeDB' },
  selectModel: { nl: 'Model', en: 'Model' },
  history: { nl: 'Geschiedenis', en: 'History' },
  clearHistory: { nl: 'Wis', en: 'Clear' },
  noHistory: { nl: 'Geen recente vragen', en: 'No recent questions' },
  exportConversation: { nl: 'Exporteren', en: 'Export' },
  importConversation: { nl: 'Importeren', en: 'Import' },
  clearConversation: { nl: 'Wissen', en: 'Clear' },
  export: { nl: 'Export', en: 'Export' },
  import: { nl: 'Import', en: 'Import' },
  clear: { nl: 'Wis', en: 'Clear' },
  new: { nl: 'Nieuw', en: 'New' },
  newConversation: { nl: 'Nieuw gesprek starten', en: 'Start new conversation' },
  embeddings: { nl: 'Embeddings', en: 'Embeddings' },
  advanced: { nl: 'Geavanceerd', en: 'Advanced' },
  simple: { nl: 'Eenvoudig', en: 'Simple' },
  conversationCleared: { nl: 'Conversatie gewist', en: 'Conversation cleared' },
  exportSuccess: { nl: 'Conversatie geëxporteerd', en: 'Conversation exported' },
  importSuccess: { nl: 'Conversatie geïmporteerd', en: 'Conversation imported' },
  importError: { nl: 'Ongeldig bestand', en: 'Invalid file' },
  resultsFound: { nl: 'resultaten gevonden', en: 'results found' },
  sources: { nl: 'Bronnen', en: 'Sources' },
  showMap: { nl: 'Toon op kaart', en: 'Show on map' },
  showChart: { nl: 'Toon grafiek', en: 'Show chart' },
  showTimeline: { nl: 'Toon tijdlijn', en: 'Show timeline' },
  showNetwork: { nl: 'Toon netwerk', en: 'Show network' },
  showTable: { nl: 'Toon tabel', en: 'Show table' },
  showCard: { nl: 'Toon kaarten', en: 'Show cards' },
  showGallery: { nl: 'Toon galerij', en: 'Show gallery' },
  expandVisualization: { nl: 'Vergroot', en: 'Expand' },
  collapseVisualization: { nl: 'Verklein', en: 'Collapse' },
  refreshVisualization: { nl: 'Ververs', en: 'Refresh' },
  resizePanel: { nl: 'Sleep om formaat te wijzigen', en: 'Drag to resize' },
  noVisualization: { nl: 'Geen visualisatie beschikbaar', en: 'No visualization available' },
  institutions: { nl: 'Instellingen', en: 'Institutions' },
  viewDetails: { nl: 'Details bekijken', en: 'View details' },
  openWebsite: { nl: 'Website openen', en: 'Open website' },
  confidence: { nl: 'Betrouwbaarheid', en: 'Confidence' },
  showEmbeddings: { nl: 'Embeddings', en: 'Embeddings' },
  hideEmbeddings: { nl: 'Verberg', en: 'Hide' },
  simpleMode: { nl: 'Eenvoudig', en: 'Simple' },
  advancedMode: { nl: 'Geavanceerd', en: 'Advanced' },
  embeddingProjector: { nl: 'Embedding Projector', en: 'Embedding Projector' },
  loadingEmbeddings: { nl: 'Embeddings laden...', en: 'Loading embeddings...' },
  sourcesUsed: { nl: 'Gebruikte bronnen', en: 'Sources used' },
  addToContext: { nl: 'Toevoegen aan context', en: 'Add to context' },
  pointAddedToContext: { nl: 'Punt toegevoegd aan context', en: 'Point added to context' },
  // Cache-related labels
  cache: { nl: 'Cache', en: 'Cache' },
  cacheSettings: { nl: 'Cache Instellingen', en: 'Cache Settings' },
  cacheEnabled: { nl: 'Cache Ingeschakeld', en: 'Cache Enabled' },
  cacheDisabled: { nl: 'Cache Uitgeschakeld', en: 'Cache Disabled' },
  cacheHit: { nl: 'Cache Hit', en: 'Cache Hit' },
  cacheMiss: { nl: 'Cache Miss', en: 'Cache Miss' },
  cacheSimilarity: { nl: 'Gelijkenis', en: 'Similarity' },
  cacheThreshold: { nl: 'Drempelwaarde', en: 'Threshold' },
  cacheThresholdDescription: { nl: 'Hoger = exactere overeenkomst vereist', en: 'Higher = more exact match required' },
  cacheHits: { nl: 'Hits', en: 'Hits' },
  cacheMisses: { nl: 'Misses', en: 'Misses' },
  cacheHitRate: { nl: 'Hitrate', en: 'Hit Rate' },
  cacheEntries: { nl: 'Items', en: 'Entries' },
  cacheStorageUsed: { nl: 'Opslag Gebruikt', en: 'Storage Used' },
  clearCache: { nl: 'Cache Wissen', en: 'Clear Cache' },
  cacheCleared: { nl: 'Cache gewist', en: 'Cache cleared' },
  cacheClearedLocal: { nl: 'Lokale cache gewist (gedeelde cache niet beschikbaar)', en: 'Local cache cleared (shared cache unavailable)' },
  cacheClearFailed: { nl: 'Cache wissen mislukt', en: 'Failed to clear cache' },
  bypassCache: { nl: 'Cache Overslaan', en: 'Bypass Cache' },
  bypassCacheEnabled: { nl: 'Cache wordt overgeslagen voor volgende vraag', en: 'Cache will be bypassed for next query' },
  bypassCacheDisabled: { nl: 'Cache normaal gebruikt', en: 'Cache used normally' },
  refreshResponse: { nl: 'Vernieuwen', en: 'Refresh' },
  refreshingResponse: { nl: 'Vernieuwen...', en: 'Refreshing...' },
  responseRefreshed: { nl: 'Antwoord vernieuwd', en: 'Response refreshed' },
  retryQuery: { nl: 'Opnieuw proberen', en: 'Retry' },
  retryingQuery: { nl: 'Opnieuw proberen...', en: 'Retrying...' },
  clearCacheAndRetry: { nl: 'Cache wissen en opnieuw', en: 'Clear cache & retry' },
  enableCache: { nl: 'Cache inschakelen', en: 'Enable caching' },
  enableCacheDescription: { nl: 'Sla antwoorden op voor vergelijkbare vragen', en: 'Save responses for similar questions' },
  fromCache: { nl: 'Uit cache', en: 'From cache' },
  // Embedding projector mode labels
  globalMode: { nl: 'Globaal', en: 'Global' },
  contextMode: { nl: 'Context', en: 'Context' },
  globalModeDescription: { nl: 'Alle 500 embeddings', en: 'All 500 embeddings' },
  contextModeDescription: { nl: 'Alleen RAG resultaten', en: 'RAG results only' },
  noContextResults: { nl: 'Geen context resultaten - stel eerst een vraag', en: 'No context results - ask a question first' },
  // Pagination labels
  loadMore: { nl: 'Meer laden', en: 'Load More' },
  loadingMore: { nl: 'Laden...', en: 'Loading...' },
  showingResults: { nl: 'Toont', en: 'Showing' },
  ofResults: { nl: 'van', en: 'of' },
  allResultsLoaded: { nl: 'Alle resultaten geladen', en: 'All results loaded' },
  // Infinite scroll
  infiniteScroll: { nl: 'Oneindig scrollen', en: 'Infinite scroll' },
  infiniteScrollEnabled: { nl: 'Automatisch meer laden bij scrollen', en: 'Auto-load more when scrolling' },
  loadingMoreResults: { nl: 'Meer resultaten laden...', en: 'Loading more results...' },
  // Data source selection labels
  dataSources: { nl: 'Bronnen', en: 'Sources' },
  noSourcesSelected: { nl: 'Selecteer minimaal één databron', en: 'Select at least one data source' },
  // Knowledge Graph labels
  graph: { nl: 'Graaf', en: 'Graph' },
  showGraph: { nl: 'Toon Kennisgraaf', en: 'Show Knowledge Graph' },
  hideGraph: { nl: 'Verberg Kennisgraaf', en: 'Hide Knowledge Graph' },
};

// Example questions
const EXAMPLE_QUESTIONS = {
  nl: [
    'Toon alle musea in Amsterdam op de kaart',
    'Welke archieven zijn er in Noord-Holland?',
    'Hoeveel bibliotheken zijn er per provincie?',
    'Wat zijn de best beoordeelde musea?',
    'Toon de tijdlijn van opgerichte instellingen',
  ],
  en: [
    'Show all museums in Amsterdam on the map',
    'What archives are in North Holland?',
    'How many libraries are there per province?',
    'What are the best rated museums?',
    'Show the timeline of founded institutions',
  ],
};

// Visualization type icons
const VIZ_ICONS: Record<VisualizationType, React.ReactNode> = {
  none: null,
  map: <Map size={16} />,
  chart: <BarChart3 size={16} />,
  timeline: <Clock size={16} />,
  network: <Network size={16} />,
  table: <Table2 size={16} />,
  card: <LayoutGrid size={16} />,
  gallery: <Building2 size={16} />,
};

// Visualization type short labels for buttons
const VIZ_LABELS: Record<VisualizationType, { nl: string; en: string }> = {
  none: { nl: '', en: '' },
  map: { nl: 'Kaart', en: 'Map' },
  chart: { nl: 'Grafiek', en: 'Chart' },
  timeline: { nl: 'Tijdlijn', en: 'Timeline' },
  network: { nl: 'Netwerk', en: 'Network' },
  table: { nl: 'Tabel', en: 'Table' },
  card: { nl: 'Kaarten', en: 'Cards' },
  gallery: { nl: 'Galerij', en: 'Gallery' },
};

// Helper function to detect error messages by content (for cached errors that don't have error flag)
const ERROR_MESSAGE_PATTERNS = [
  'Er is een fout opgetreden',
  'Probeer het later opnieuw',
  'Error processing',
  'Serverfout',
  'verbinding maken',
  'Failed to',
  'Unable to',
  'Something went wrong',
  'Kon geen',
  'Geen resultaten gevonden',
];

const isErrorMessage = (message: ConversationMessage): boolean => {
  // If error flag is set, it's definitely an error
  if (message.error) return true;
  // Only check assistant messages
  if (message.role !== 'assistant') return false;
  // Check if content matches known error patterns
  if (!message.content) return false;
  return ERROR_MESSAGE_PATTERNS.some(pattern => 
    message.content.toLowerCase().includes(pattern.toLowerCase())
  );
};

// ============================================================================
// Progressive Loading Stages Component
// ============================================================================

// Stage labels for progressive loading indicator
const STAGE_LABELS: Record<RetrievalStage, { nl: string; en: string; icon: string }> = {
  idle: { nl: 'Wachten...', en: 'Waiting...', icon: '⏳' },
  embedding: { nl: 'Query analyseren...', en: 'Analyzing query...', icon: '🔤' },
  cache_check: { nl: 'Cache controleren...', en: 'Checking cache...', icon: '💾' },
  geo_detect: { nl: 'Locatie detecteren...', en: 'Detecting location...', icon: '📍' },
  qdrant: { nl: 'Vector zoeken...', en: 'Vector search...', icon: '🔍' },
  sparql: { nl: 'Kennisgraaf doorzoeken...', en: 'Querying knowledge graph...', icon: '🕸️' },
  typedb: { nl: 'TypeDB doorzoeken...', en: 'Querying TypeDB...', icon: '🗃️' },
  llm: { nl: 'Antwoord genereren...', en: 'Generating response...', icon: '🤖' },
  complete: { nl: 'Voltooid', en: 'Complete', icon: '✅' },
};

interface LoadingStagesProps {
  progress: RetrievalProgress;
  language: 'nl' | 'en';
}

const LoadingStages: React.FC<LoadingStagesProps> = ({ progress, language }) => {
  const { currentStage, stages, detectedPlace, cacheHit, cacheSimilarity, totalElapsed } = progress;
  
  // Filter to show only stages that have started (not pending)
  const visibleStages = stages.filter(s => s.status !== 'pending');
  
  // If no stages have started yet, show a simple loading message
  if (visibleStages.length === 0 && currentStage === 'idle') {
    return (
      <div className="loading-stages">
        <div className="loading-stage loading-stage--active">
          <span className="loading-stage__icon">⏳</span>
          <span className="loading-stage__label">
            {language === 'nl' ? 'Initialiseren...' : 'Initializing...'}
          </span>
          <Loader2 className="loading-stage__spinner" size={14} />
        </div>
      </div>
    );
  }
  
  return (
    <div className="loading-stages">
      {visibleStages.map(stageStatus => {
        const label = STAGE_LABELS[stageStatus.stage];
        const isActive = stageStatus.status === 'active';
        const isComplete = stageStatus.status === 'complete';
        const isError = stageStatus.status === 'error';
        const isSkipped = stageStatus.status === 'skipped';
        
        return (
          <div 
            key={stageStatus.stage}
            className={`loading-stage loading-stage--${stageStatus.status}`}
          >
            <span className="loading-stage__icon">{label.icon}</span>
            <span className="loading-stage__label">
              {language === 'nl' ? label.nl : label.en}
            </span>
            {isActive && <Loader2 className="loading-stage__spinner" size={14} />}
            {isComplete && stageStatus.count !== undefined && stageStatus.count > 0 && (
              <span className="loading-stage__count">{stageStatus.count}</span>
            )}
            {isComplete && stageStatus.duration !== undefined && (
              <span className="loading-stage__duration">{stageStatus.duration}ms</span>
            )}
            {isSkipped && (
              <span className="loading-stage__skipped">
                ({language === 'nl' ? 'overgeslagen' : 'skipped'})
              </span>
            )}
            {isError && <span className="loading-stage__error">⚠️</span>}
          </div>
        );
      })}
      
      {/* Show detected place if available */}
      {detectedPlace && (
        <div className="loading-stage__place">
          📍 {language === 'nl' ? 'Locatie' : 'Location'}: {detectedPlace}
        </div>
      )}
      
      {/* Show cache hit notification */}
      {cacheHit && (
        <div className="loading-stage__cache-hit">
          <Zap size={14} />
          {language === 'nl' ? 'Uit cache geladen!' : 'Loaded from cache!'}
          {cacheSimilarity && (
            <span className="loading-stage__similarity">
              ({Math.round(cacheSimilarity * 100)}%)
            </span>
          )}
        </div>
      )}
      
      {/* Show elapsed time for complete state */}
      {totalElapsed !== undefined && totalElapsed > 0 && currentStage === 'complete' && (
        <div className="loading-stage__elapsed">
          {language === 'nl' ? 'Totaal' : 'Total'}: {(totalElapsed / 1000).toFixed(1)}s
        </div>
      )}
    </div>
  );
};

// ============================================================================
// Sub-Components
// ============================================================================

interface InstitutionCardProps {
  institution: InstitutionData;
  language: 'nl' | 'en';
}

const InstitutionCard: React.FC<InstitutionCardProps> = ({ institution, language }) => {
  const t = (key: keyof typeof TEXT) => TEXT[key][language];
  const navigate = useNavigate();
  
  return (
    <div className="conversation-institution-card">
      <div className="conversation-institution-card__header">
        <Building2 size={20} className="conversation-institution-card__icon" />
        <h4 className="conversation-institution-card__name">{institution.name}</h4>
        {institution.type && (
          <span className="conversation-institution-card__type">{institution.type}</span>
        )}
      </div>
      
      {institution.description && (
        <p className="conversation-institution-card__description">
          {institution.description.slice(0, 150)}
          {institution.description.length > 150 ? '...' : ''}
        </p>
      )}
      
      <div className="conversation-institution-card__details">
        {institution.city && (
          <div className="conversation-institution-card__detail">
            <MapPin size={14} />
            <span>{institution.city}{institution.province ? `, ${institution.province}` : ''}</span>
          </div>
        )}
        
        {(institution.rating ?? 0) > 0 && (
          <div className="conversation-institution-card__detail conversation-institution-card__detail--rating">
            <span className="conversation-institution-card__stars">
              {'★'.repeat(Math.round(institution.rating ?? 0))}
              {'☆'.repeat(5 - Math.round(institution.rating ?? 0))}
            </span>
            <span>{(institution.rating ?? 0).toFixed(1)}</span>
            {(institution.reviews ?? 0) > 0 && (
              <span className="conversation-institution-card__reviews">
                ({institution.reviews} reviews)
              </span>
            )}
          </div>
        )}
      </div>
      
      <div className="conversation-institution-card__actions">
        {institution.website && (
          <a 
            href={institution.website} 
            target="_blank" 
            rel="noopener noreferrer"
            className="conversation-institution-card__action"
          >
            <ExternalLink size={14} />
            {t('openWebsite')}
          </a>
        )}
        {(institution.latitude && institution.longitude) && (
          <button 
            className="conversation-institution-card__action"
            onClick={() => {
              const params = new URLSearchParams({
                lat: String(institution.latitude),
                lon: String(institution.longitude),
                zoom: '14',
                highlight: institution.name || '',
              });
              navigate(`/map?${params.toString()}`);
            }}
          >
            <Map size={14} />
            {t('showMap')}
          </button>
        )}
      </div>
      
      {(institution.isil || institution.wikidata) && (
        <div className="conversation-institution-card__ids">
          {institution.isil && (
            <span className="conversation-institution-card__id">ISIL: {institution.isil}</span>
          )}
          {institution.wikidata && (
            <a 
              href={`https://www.wikidata.org/wiki/${institution.wikidata}`}
              target="_blank"
              rel="noopener noreferrer"
              className="conversation-institution-card__id conversation-institution-card__id--link"
            >
              Wikidata: {institution.wikidata}
            </a>
          )}
        </div>
      )}
    </div>
  );
};

interface VisualizationPanelProps {
  type: VisualizationType;
  data: RAGResponse['visualizationData'];
  language: 'nl' | 'en';
  isExpanded: boolean;
  onToggleExpand: () => void;
  onChangeType: (type: VisualizationType) => void;
  /** Retrieved results from RAG for social network visualization */
  retrievedResults?: RetrievedResult[] | null;
  /** Query type from RAG - 'person' or 'institution' */
  queryType?: QueryType | null;
  /** Pagination state for loading more results */
  paginationState?: PaginationState | null;
  /** Whether more results are currently loading */
  isLoadingMore?: boolean;
  /** Callback to load more results */
  onLoadMore?: () => void;
  /** Whether infinite scroll is enabled (vs button mode) */
  infiniteScrollEnabled?: boolean;
  /** Callback to toggle infinite scroll mode */
  onToggleInfiniteScroll?: () => void;
}

const VisualizationPanel: React.FC<VisualizationPanelProps> = ({
  type,
  data,
  language,
  isExpanded,
  onToggleExpand,
  onChangeType,
  retrievedResults,
  queryType,
  paginationState,
  isLoadingMore,
  onLoadMore,
  infiniteScrollEnabled = false,
  onToggleInfiniteScroll,
}) => {
  const t = (key: keyof typeof TEXT) => TEXT[key][language];
  const [selectedInstitutionId, setSelectedInstitutionId] = useState<string | null>(null);
  
  // Infinite scroll for cards and table containers
  const handleInfiniteLoad = useCallback(async () => {
    if (onLoadMore) {
      await onLoadMore();
    }
  }, [onLoadMore]);
  
  const { scrollContainerRef: cardsScrollRef } = useInfiniteScroll({
    onLoadMore: handleInfiniteLoad,
    hasMore: paginationState?.hasMore ?? false,
    isLoading: isLoadingMore ?? false,
    enabled: infiniteScrollEnabled && type === 'card',
    threshold: 300,
  });
  
  const { scrollContainerRef: tableScrollRef } = useInfiniteScroll({
    onLoadMore: handleInfiniteLoad,
    hasMore: paginationState?.hasMore ?? false,
    isLoading: isLoadingMore ?? false,
    enabled: infiniteScrollEnabled && type === 'table',
    threshold: 200,
  });
  
  // Calculate dimensions based on expanded state
  const vizWidth = isExpanded ? Math.min(window.innerWidth - 100, 1200) : 550;
  const vizHeight = isExpanded ? 500 : 350;
  
  // Convert institutions to GeoCoordinates for map
  const mapCoordinates = useMemo(() => {
    if (!data?.coordinates) {
      // Fallback: convert institutions with lat/lng to coordinates
      return (data?.institutions || [])
        .filter(inst => inst.latitude && inst.longitude)
        .map(inst => ({
          lat: inst.latitude!,
          lng: inst.longitude!,
          label: inst.name,
          type: inst.type,
          data: inst,
        }));
    }
    return data.coordinates;
  }, [data?.coordinates, data?.institutions]);
  
  // Generate chart data from institutions if not provided
  const chartData = useMemo(() => {
    if (data?.chartData) return data.chartData;
    
    // Group by type or province
    const institutions = data?.institutions || [];
    const byType: Record<string, number> = {};
    const byProvince: Record<string, number> = {};
    
    institutions.forEach(inst => {
      if (inst.type) {
        byType[inst.type] = (byType[inst.type] || 0) + 1;
      }
      if (inst.province) {
        byProvince[inst.province] = (byProvince[inst.province] || 0) + 1;
      }
    });
    
    // Use province if more variety, else type
    const dataMap = Object.keys(byProvince).length >= 3 ? byProvince : byType;
    const labels = Object.keys(dataMap).slice(0, 10);
    const values = labels.map(l => dataMap[l]);
    
    return {
      labels,
      datasets: [{
        label: language === 'nl' ? 'Aantal' : 'Count',
        data: values,
      }],
    };
  }, [data?.chartData, data?.institutions, language]);
  
  // Generate timeline events from institutions if not provided
  const timelineEvents = useMemo(() => {
    if (data?.timeline) return data.timeline;
    
    // Could extract founding dates from institutions if available
    return [];
  }, [data?.timeline]);
  
  // Handle marker/node clicks
  const handleInstitutionClick = useCallback((inst: InstitutionData) => {
    setSelectedInstitutionId(inst.id);
    console.log('Selected institution:', inst);
  }, []);
  
  if (!data || type === 'none') {
    return (
      <div className="conversation-viz conversation-viz--empty">
        <Info size={24} />
        <p>{t('noVisualization')}</p>
      </div>
    );
  }
  
  const renderVisualization = () => {
    switch (type) {
      case 'map':
        return (
          <div className="conversation-viz__map">
            {mapCoordinates.length > 0 ? (
              <>
                <ConversationMapLibre
                  coordinates={mapCoordinates}
                  width={vizWidth}
                  height={vizHeight}
                  language={language}
                  selectedId={selectedInstitutionId}
                  onMarkerClick={handleInstitutionClick}
                  showClustering={mapCoordinates.length > 50}
                />
                {/* Map pagination status bar */}
                {paginationState && (
                  <div className="conversation-viz__map-pagination">
                    <span className="conversation-viz__pagination-info">
                      {mapCoordinates.length} {language === 'nl' ? 'op kaart' : 'on map'} · {paginationState.totalLoaded} {t('ofResults')} {paginationState.estimatedTotal ?? '?'}
                    </span>
                    {paginationState.hasMore && (
                      <button 
                        className="conversation-viz__load-more-btn conversation-viz__load-more-btn--compact"
                        onClick={onLoadMore}
                        disabled={isLoadingMore}
                      >
                        {isLoadingMore ? (
                          <Loader2 size={14} className="conversation-viz__load-more-spinner" />
                        ) : (
                          <span>+ {t('loadMore')}</span>
                        )}
                      </button>
                    )}
                  </div>
                )}
              </>
            ) : (
              <div className="conversation-viz__map-placeholder">
                <Map size={48} />
                <p>{data.institutions?.length || 0} {t('institutions')}</p>
                <p className="conversation-viz__map-hint">
                  {language === 'nl' 
                    ? 'Geen locatiegegevens beschikbaar' 
                    : 'No location data available'}
                </p>
              </div>
            )}
          </div>
        );
        
      case 'card':
        return (
          <div className="conversation-viz__cards-container">
            <div 
              className="conversation-viz__cards"
              ref={cardsScrollRef as RefObject<HTMLDivElement>}
            >
              {data.institutions?.map((inst, idx) => (
                <InstitutionCard 
                  key={inst.id || idx} 
                  institution={inst} 
                  language={language}
                />
              ))}
              {/* Infinite scroll loading indicator */}
              {infiniteScrollEnabled && isLoadingMore && (
                <div className="conversation-viz__infinite-loader">
                  <Loader2 size={20} className="conversation-viz__load-more-spinner" />
                  <span>{t('loadingMoreResults')}</span>
                </div>
              )}
            </div>
            {/* Pagination controls - only show button when infinite scroll is disabled */}
            {paginationState && (
              <div className="conversation-viz__pagination">
                <span className="conversation-viz__pagination-info">
                  {t('showingResults')} {paginationState.totalLoaded} {t('ofResults')} {paginationState.estimatedTotal ?? '?'}
                </span>
                {!infiniteScrollEnabled && paginationState.hasMore ? (
                  <button 
                    className="conversation-viz__load-more-btn"
                    onClick={onLoadMore}
                    disabled={isLoadingMore}
                  >
                    {isLoadingMore ? (
                      <>
                        <Loader2 size={16} className="conversation-viz__load-more-spinner" />
                        {t('loadingMore')}
                      </>
                    ) : (
                      t('loadMore')
                    )}
                  </button>
                ) : !paginationState.hasMore ? (
                  <span className="conversation-viz__all-loaded">
                    <Check size={14} />
                    {t('allResultsLoaded')}
                  </span>
                ) : null}
              </div>
            )}
          </div>
        );
        
      case 'table':
        return (
          <div className="conversation-viz__table-wrapper">
            <div 
              className="conversation-viz__table-container"
              ref={tableScrollRef as RefObject<HTMLDivElement>}
            >
              <table className="conversation-viz__table">
                <thead>
                  <tr>
                    <th>{language === 'nl' ? 'Naam' : 'Name'}</th>
                    <th>{language === 'nl' ? 'Type' : 'Type'}</th>
                    <th>{language === 'nl' ? 'Plaats' : 'City'}</th>
                    <th>{language === 'nl' ? 'Provincie' : 'Province'}</th>
                  </tr>
                </thead>
                <tbody>
                  {data.institutions?.map((inst, idx) => (
                    <tr key={inst.id || idx}>
                      <td>{inst.name}</td>
                      <td>{inst.type || '-'}</td>
                      <td>{inst.city || '-'}</td>
                      <td>{inst.province || '-'}</td>
                    </tr>
                  ))}
                  {/* Infinite scroll loading indicator */}
                  {infiniteScrollEnabled && isLoadingMore && (
                    <tr className="conversation-viz__table-loader-row">
                      <td colSpan={4}>
                        <div className="conversation-viz__infinite-loader">
                          <Loader2 size={16} className="conversation-viz__load-more-spinner" />
                          <span>{t('loadingMoreResults')}</span>
                        </div>
                      </td>
                    </tr>
                  )}
                </tbody>
              </table>
            </div>
            {/* Pagination controls - only show button when infinite scroll is disabled */}
            {paginationState && (
              <div className="conversation-viz__pagination">
                <span className="conversation-viz__pagination-info">
                  {t('showingResults')} {paginationState.totalLoaded} {t('ofResults')} {paginationState.estimatedTotal ?? '?'}
                </span>
                {!infiniteScrollEnabled && paginationState.hasMore ? (
                  <button 
                    className="conversation-viz__load-more-btn"
                    onClick={onLoadMore}
                    disabled={isLoadingMore}
                  >
                    {isLoadingMore ? (
                      <>
                        <Loader2 size={16} className="conversation-viz__load-more-spinner" />
                        {t('loadingMore')}
                      </>
                    ) : (
                      t('loadMore')
                    )}
                  </button>
                ) : !paginationState.hasMore ? (
                  <span className="conversation-viz__all-loaded">
                    <Check size={14} />
                    {t('allResultsLoaded')}
                  </span>
                ) : null}
              </div>
            )}
          </div>
        );
        
      case 'chart':
        return (
          <div className="conversation-viz__chart">
            {chartData.labels.length > 0 ? (
              <ConversationBarChart
                data={chartData}
                width={vizWidth}
                height={vizHeight}
                language={language}
                orientation="vertical"
                showValues={true}
                animate={true}
                title={language === 'nl' ? 'Verdeling' : 'Distribution'}
              />
            ) : (
              <div className="conversation-viz__chart-placeholder">
                <BarChart3 size={48} />
                <p>{language === 'nl' ? 'Geen grafiekgegevens beschikbaar' : 'No chart data available'}</p>
              </div>
            )}
          </div>
        );
        
      case 'timeline':
        return (
          <div className="conversation-viz__timeline">
            {timelineEvents.length > 0 ? (
              <ConversationTimeline
                events={timelineEvents}
                width={vizWidth}
                height={Math.min(vizHeight, 250)}
                language={language}
                showLabels={true}
              />
            ) : (
              <div className="conversation-viz__timeline-placeholder">
                <Clock size={48} />
                <p>{language === 'nl' ? 'Geen tijdlijngegevens beschikbaar' : 'No timeline data available'}</p>
              </div>
            )}
          </div>
        );
        
      case 'network':
        // Use social network graph for person queries with retrieved results
        if (queryType === 'person' && retrievedResults && retrievedResults.length > 0) {
          return (
            <div className="conversation-viz__network conversation-viz__social-network">
              <ConversationSocialNetworkGraph
                retrievedResults={retrievedResults}
                width={vizWidth}
                height={vizHeight}
                language={language}
              />
            </div>
          );
        }
        // Fall back to standard network graph for institution relationships
        return (
          <div className="conversation-viz__network">
            {data.graphData && data.graphData.nodes.length > 0 ? (
              <ConversationNetworkGraph
                data={data.graphData}
                width={vizWidth}
                height={vizHeight}
                language={language}
                showLabels={true}
                showEdgeLabels={data.graphData.edges.length < 20}
              />
            ) : (
              <div className="conversation-viz__network-placeholder">
                <Network size={48} />
                <p>{language === 'nl' ? 'Geen netwerkgegevens beschikbaar' : 'No network data available'}</p>
              </div>
            )}
          </div>
        );
        
      default:
        return null;
    }
  };
  
  return (
    <div className={`conversation-viz ${isExpanded ? 'conversation-viz--expanded' : ''}`}>
      <div className="conversation-viz__toolbar">
        <div className="conversation-viz__type-selector">
          {(['map', 'card', 'table', 'chart', 'timeline', 'network'] as VisualizationType[]).map(vizType => (
            <button
              key={vizType}
              className={`conversation-viz__type-btn ${type === vizType ? 'conversation-viz__type-btn--active' : ''}`}
              onClick={() => onChangeType(vizType)}
              title={t(`show${vizType.charAt(0).toUpperCase() + vizType.slice(1)}` as keyof typeof TEXT)}
            >
              {VIZ_ICONS[vizType]}
              <span>{VIZ_LABELS[vizType][language]}</span>
            </button>
          ))}
        </div>
        
        <div className="conversation-viz__actions">
          {/* Infinite scroll toggle - only for card/table views */}
          {(type === 'card' || type === 'table') && onToggleInfiniteScroll && (
            <button
              className={`conversation-viz__action-btn ${infiniteScrollEnabled ? 'conversation-viz__action-btn--active' : ''}`}
              onClick={onToggleInfiniteScroll}
              title={infiniteScrollEnabled ? t('infiniteScrollEnabled') : t('infiniteScroll')}
            >
              <span style={{ fontSize: '18px', fontWeight: 'bold' }}>∞</span>
            </button>
          )}
          <button
            className="conversation-viz__action-btn"
            onClick={onToggleExpand}
            title={isExpanded ? t('collapseVisualization') : t('expandVisualization')}
          >
            {isExpanded ? <Minimize2 size={16} /> : <Maximize2 size={16} />}
            <span>{isExpanded ? t('collapseVisualization') : t('expandVisualization')}</span>
          </button>
        </div>
      </div>
      
      <div className="conversation-viz__content">
        {renderVisualization()}
      </div>
      
      {data.institutions && data.institutions.length > 0 && (
        <div className="conversation-viz__footer">
          <span>{data.institutions.length} {t('resultsFound')}</span>
        </div>
      )}
    </div>
  );
};

// ============================================================================
// Main Page Component
// ============================================================================

const ConversationPage: React.FC = () => {
  const { language } = useLanguage();
  const t = (key: keyof typeof TEXT) => TEXT[key][language];
  
  // RAG hook with cache support
  const { 
    queryRAG, 
    isLoading: ragLoading, 
    clearContext, 
    lastContext,
    cacheEnabled,
    lastCacheLookup,
    setCacheEnabled,
    getCacheStats,
    clearCache,
    setCacheSimilarityThreshold,
    // Pagination support
    paginationState,
    isLoadingMore,
    loadMoreResults,
    // Progressive loading stages
    retrievalProgress,
  } = useMultiDatabaseRAG();
  
  // UI State for LLM provider selection
  const { state: uiState, setLLMProvider } = useUIState();
  
  // Qdrant hook for embedding projector
  const { scrollPoints, status: qdrantStatus } = useQdrant();
  
  // State
  const [messages, setMessages] = useState<ConversationMessage[]>([]);
  const [input, setInput] = useState('');
  const [showModelDropdown, setShowModelDropdown] = useState(false);
  const [showHistoryDropdown, setShowHistoryDropdown] = useState(false);
  const [history, setHistory] = useState<Array<{ question: string; timestamp: Date }>>([]);
  const [notification, setNotification] = useState<string | null>(null);
  const [copiedId, setCopiedId] = useState<string | null>(null);
  const [vizExpanded, setVizExpanded] = useState(false);
  const [activeVizType, setActiveVizType] = useState<VisualizationType>('card');
  const [activeVizData, setActiveVizData] = useState<RAGResponse['visualizationData'] | null>(null);
  const [activeRetrievedResults, setActiveRetrievedResults] = useState<RetrievedResult[] | null>(null);
  const [activeQueryType, setActiveQueryType] = useState<QueryType | null>(null);
  
  // Embedding Projector state
  const [showProjector, setShowProjector] = useState(false);
  const [projectorSimpleMode, setProjectorSimpleMode] = useState(false);
  const [projectorPinned, setProjectorPinned] = useState(false);
  const [embeddingPoints, setEmbeddingPoints] = useState<EmbeddingPoint[]>([]);
  const [embeddingsLoading, setEmbeddingsLoading] = useState(false);
  const [highlightedSourceIndices, setHighlightedSourceIndices] = useState<number[]>([]);
  const [additionalContextPoints, setAdditionalContextPoints] = useState<EmbeddingPoint[]>([]);
  // Projector mode: 'global' shows all 500 points, 'context' shows only RAG results with vectors
  const [projectorMode, setProjectorMode] = useState<'global' | 'context'>('global');
  const [contextEmbeddingPoints, setContextEmbeddingPoints] = useState<EmbeddingPoint[]>([]);
  // Load All state for embedding projector
  const [isLoadingAllEmbeddings, setIsLoadingAllEmbeddings] = useState(false);
  const [totalEmbeddingPoints, setTotalEmbeddingPoints] = useState<number | undefined>(undefined);
  
  // Knowledge Graph Projector state
  const [showGraphProjector, setShowGraphProjector] = useState(false);
  const [graphProjectorMode, setGraphProjectorMode] = useState<'global' | 'context'>('global');
  const [graphProjectorPinned, setGraphProjectorPinned] = useState(false);
  const [graphBackend, setGraphBackend] = useState<GraphBackend>('oxigraph');
  const [contextGraphData, setContextGraphData] = useState<GraphData | null>(null);
  // Track if user has manually selected a mode (to avoid overriding their choice)
  const userSelectedModeRef = useRef(false);
  
  // Resizable visualization panel state
  const [vizPanelWidth, setVizPanelWidth] = useState(400);
  const [isResizing, setIsResizing] = useState(false);
  
  // Cache management state
  const [showCachePanel, setShowCachePanel] = useState(false);
  const [cacheStats, setCacheStats] = useState<CacheStats | null>(null);
  const [cacheSimilarityThreshold, setCacheSimilarityThresholdLocal] = useState(0.92);
  const [bypassCache, setBypassCache] = useState(false);
  const [refreshingMessageId, setRefreshingMessageId] = useState<string | null>(null);
  
  // Data source selection state - default to qdrant + sparql (fastest combination)
  const [selectedSources, setSelectedSources] = useState<DataSource[]>(['qdrant', 'sparql']);
  
  // Infinite scroll toggle (alternative to Load More button for cards/table)
  const [infiniteScrollEnabled, setInfiniteScrollEnabled] = useState(false);
  
  // Refs
  const messagesEndRef = useRef<HTMLDivElement>(null);
  const messagesContainerRef = useRef<HTMLDivElement>(null);
  const inputRef = useRef<HTMLTextAreaElement>(null);
  const modelDropdownRef = useRef<HTMLDivElement>(null);
  const historyDropdownRef = useRef<HTMLDivElement>(null);
  const fileInputRef = useRef<HTMLInputElement>(null);
  
  // ============================================================================
  // Effects
  // ============================================================================
  
  // Load history from localStorage
  useEffect(() => {
    try {
      const savedHistory = localStorage.getItem(STORAGE_KEYS.HISTORY);
      if (savedHistory) {
        const parsed = JSON.parse(savedHistory);
        setHistory(parsed.map((h: { question: string; timestamp: string }) => ({ 
          ...h, 
          timestamp: new Date(h.timestamp) 
        })));
      }
    } catch (e) {
      console.error('Failed to load history:', e);
    }
  }, []);
  
  // Load selected sources from localStorage
  useEffect(() => {
    try {
      const savedSources = localStorage.getItem(STORAGE_KEYS.SELECTED_SOURCES);
      if (savedSources) {
        const parsed = JSON.parse(savedSources);
        if (Array.isArray(parsed) && parsed.length > 0) {
          // Validate that all sources are valid
          const validSources = parsed.filter((s: string) => 
            DATA_SOURCES.some(ds => ds.id === s)
          ) as DataSource[];
          if (validSources.length > 0) {
            setSelectedSources(validSources);
          }
        }
      }
    } catch (e) {
      console.error('Failed to load selected sources:', e);
    }
  }, []);
  
  // Save selected sources to localStorage
  useEffect(() => {
    try {
      localStorage.setItem(STORAGE_KEYS.SELECTED_SOURCES, JSON.stringify(selectedSources));
    } catch (e) {
      console.error('Failed to save selected sources:', e);
    }
  }, [selectedSources]);
  
  // Auto-scroll to bottom
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages]);
  
  // Auto-resize textarea
  useEffect(() => {
    if (inputRef.current) {
      inputRef.current.style.height = 'auto';
      inputRef.current.style.height = `${Math.min(inputRef.current.scrollHeight, 150)}px`;
    }
  }, [input]);
  
  // Close dropdowns on outside click
  useEffect(() => {
    const handleClickOutside = (e: MouseEvent) => {
      if (modelDropdownRef.current && !modelDropdownRef.current.contains(e.target as Node)) {
        setShowModelDropdown(false);
      }
      if (historyDropdownRef.current && !historyDropdownRef.current.contains(e.target as Node)) {
        setShowHistoryDropdown(false);
      }
    };
    document.addEventListener('mousedown', handleClickOutside);
    return () => document.removeEventListener('mousedown', handleClickOutside);
  }, []);
  
  // Load embeddings when projector is shown
  useEffect(() => {
    if (!showProjector || !qdrantStatus.isConnected) return;
    if (embeddingPoints.length > 0) return; // Already loaded
    
    const loadEmbeddings = async () => {
      setEmbeddingsLoading(true);
      try {
        // Load embeddings from the heritage_custodians collection
        const collectionName = 'heritage_custodians';
        const { points } = await scrollPoints(collectionName, 500, null);
        
        const embeddings: EmbeddingPoint[] = points.map(p => ({
          id: p.id,
          vector: p.vector,
          payload: p.payload,
        }));
        
        setEmbeddingPoints(embeddings);
      } catch (err) {
        console.error('Failed to load embeddings:', err);
      } finally {
        setEmbeddingsLoading(false);
      }
    };
    
    loadEmbeddings();
  }, [showProjector, qdrantStatus.isConnected, embeddingPoints.length, scrollPoints]);
  
  // Track RAG source indices when context updates
  useEffect(() => {
    if (!lastContext?.qdrantResults || embeddingPoints.length === 0) {
      setHighlightedSourceIndices([]);
      return;
    }
    
    // Find indices of points that match the RAG results
    const sourceIds = new Set(lastContext.qdrantResults.map(r => String(r.id)));
    const indices: number[] = [];
    
    embeddingPoints.forEach((point, index) => {
      if (sourceIds.has(String(point.id))) {
        indices.push(index);
      }
    });
    
    setHighlightedSourceIndices(indices);
  }, [lastContext?.qdrantResults, embeddingPoints]);
  
  // Populate context embedding points from RAG results (with vectors)
  useEffect(() => {
    if (!lastContext?.qdrantResults) {
      setContextEmbeddingPoints([]);
      return;
    }
    
    // Filter for results that have vectors (from with_vector: true)
    const pointsWithVectors: EmbeddingPoint[] = lastContext.qdrantResults
      .filter(r => r.vector && Array.isArray(r.vector) && r.vector.length > 0)
      .map(r => ({
        id: r.id,
        vector: r.vector as number[],
        payload: r.payload,
      }));
    
    console.log(`[EmbeddingProjector] Context mode: ${pointsWithVectors.length} points with vectors from ${lastContext.qdrantResults.length} RAG results`);
    setContextEmbeddingPoints(pointsWithVectors);
  }, [lastContext?.qdrantResults]);
  

  // Populate context graph data from RAG results for Knowledge Graph panel "Context" mode
  // Processes Qdrant, SPARQL, and TypeDB results into a unified graph visualization
  useEffect(() => {
    console.log('[KnowledgeGraph DEBUG] useEffect triggered, lastContext:', {
      hasLastContext: !!lastContext,
      qdrantCount: lastContext?.qdrantResults?.length ?? 0,
      sparqlCount: lastContext?.sparqlResults?.length ?? 0,
      typedbCount: lastContext?.typedbResults?.length ?? 0,
    });
    
    const hasQdrantResults = lastContext?.qdrantResults && lastContext.qdrantResults.length > 0;
    const hasSparqlResults = lastContext?.sparqlResults && lastContext.sparqlResults.length > 0;
    const hasTypedbResults = lastContext?.typedbResults && lastContext.typedbResults.length > 0;
    
    if (!hasQdrantResults && !hasSparqlResults && !hasTypedbResults) {
      console.log('[KnowledgeGraph DEBUG] No results, setting contextGraphData to null');
      setContextGraphData(null);
      return;
    }
    
    // Transform all database results into GraphData format
    // Each result becomes a node, relationships between entities become edges
    const nodes: GraphNode[] = [];
    const edges: GraphEdge[] = [];
    const nodeIds = new Set<string>();
    
    // Helper to extract entity type from RDF type URI
    const extractTypeFromUri = (uri: string): string => {
      if (!uri) return 'Entity';
      // Extract last segment from URI (e.g., "http://schema.org/Museum" -> "Museum")
      const lastSlash = uri.lastIndexOf('/');
      const lastHash = uri.lastIndexOf('#');
      const lastSeparator = Math.max(lastSlash, lastHash);
      if (lastSeparator >= 0 && lastSeparator < uri.length - 1) {
        return uri.substring(lastSeparator + 1);
      }
      return 'Entity';
    };
    
    // Helper to extract value from SPARQL binding (handles { value: '...', type: 'uri'|'literal' } format)
    const getSparqlValue = (binding: unknown): string => {
      if (!binding) return '';
      if (typeof binding === 'string') return binding;
      if (typeof binding === 'object' && binding !== null) {
        const obj = binding as Record<string, unknown>;
        if ('value' in obj) return String(obj.value);
      }
      return String(binding);
    };
    
    // =========================================================================
    // Process Qdrant results (vector similarity search)
    // =========================================================================
    if (hasQdrantResults) {
      // Debug: Log first 3 Qdrant result payloads to see if ghcid is present
      console.log('[KnowledgeGraph DEBUG] First 3 Qdrant payloads:', 
        lastContext!.qdrantResults.slice(0, 3).map(r => ({
          id: r.id,
          payloadKeys: Object.keys(r.payload || {}),
          ghcid: r.payload?.ghcid,
          name: r.payload?.name,
        }))
      );
      
      lastContext!.qdrantResults.forEach((result, index) => {
        const payload = result.payload || {};
        const nodeId = `qdrant:${String(result.id)}`;
        
        // Skip if we've already processed this node
        if (nodeIds.has(nodeId)) return;
        nodeIds.add(nodeId);
        
        // Determine node type from payload
        const institutionType = String(payload.institution_type || payload.type || 'unknown').toLowerCase();
        const entityType = mapInstitutionTypeToEntityType(institutionType);
        
        // Create node with available attributes
        const node: GraphNode = {
          id: nodeId,
          label: String(payload.name || payload.custodian_name || `Qdrant Result ${index + 1}`),
          type: 'custodian',
          entityType: entityType,
          attributes: {
            ...payload,
            score: result.score,
            source: 'qdrant',
            sourceColor: '#6366f1', // Indigo for Qdrant
          },
        };
        
        nodes.push(node);
        
        // Create edges from relationships in payload
        // Link to city/location if available
        if (payload.city) {
          const cityId = `city:${String(payload.city).toLowerCase().replace(/\s+/g, '_')}`;
          if (!nodeIds.has(cityId)) {
            nodeIds.add(cityId);
            nodes.push({
              id: cityId,
              label: String(payload.city),
              type: 'location',
              entityType: 'Place',
              attributes: { 
                country: payload.country,
                source: 'qdrant_context',
              },
            });
          }
          edges.push({
            id: `${nodeId}-located_in-${cityId}`,
            source: nodeId,
            target: cityId,
            label: 'located_in',
            relationType: 'spatial',
          });
        }
        
        // Link to country if available
        if (payload.country) {
          const countryId = `country:${String(payload.country).toLowerCase()}`;
          if (!nodeIds.has(countryId)) {
            nodeIds.add(countryId);
            nodes.push({
              id: countryId,
              label: String(payload.country),
              type: 'location',
              entityType: 'Country',
              attributes: { source: 'qdrant_context' },
            });
          }
          // Link city to country if city exists, otherwise link custodian to country
          const cityId = payload.city ? `city:${String(payload.city).toLowerCase().replace(/\s+/g, '_')}` : null;
          if (cityId && nodeIds.has(cityId)) {
            const edgeId = `${cityId}-in_country-${countryId}`;
            if (!edges.some(e => e.id === edgeId)) {
              edges.push({
                id: edgeId,
                source: cityId,
                target: countryId,
                label: 'in_country',
                relationType: 'spatial',
              });
            }
          } else {
            edges.push({
              id: `${nodeId}-in_country-${countryId}`,
              source: nodeId,
              target: countryId,
              label: 'in_country',
              relationType: 'spatial',
            });
          }
        }
      });
    }
    
    // =========================================================================
    // Process SPARQL results (Oxigraph knowledge graph)
    // Format: Array of bindings with ?s (URI), ?label, ?type
    // =========================================================================
    if (hasSparqlResults) {
      lastContext!.sparqlResults.forEach((binding, index) => {
        // Extract values from SPARQL binding format
        const subjectUri = getSparqlValue(binding.s);
        const label = getSparqlValue(binding.label) || getSparqlValue(binding.name) || getSparqlValue(binding.prefLabel);
        const typeUri = getSparqlValue(binding.type) || getSparqlValue(binding.class);
        
        // Skip if no meaningful identifier
        if (!subjectUri && !label) return;
        
        // Create a unique node ID - prefer URI, fall back to generated ID
        const nodeId = subjectUri ? `sparql:${subjectUri}` : `sparql:result_${index}`;
        
        // Check for duplicates by URI or by label (for cross-source deduplication)
        if (nodeIds.has(nodeId)) return;
        
        // Also check if we have a Qdrant node with the same label (cross-source dedup)
        const normalizedLabel = label.toLowerCase().trim();
        const existingNode = nodes.find(n => 
          n.label.toLowerCase().trim() === normalizedLabel && n.attributes?.source !== 'sparql'
        );
        
        if (existingNode) {
          // Merge: add SPARQL source info to existing node
          existingNode.attributes = {
            ...existingNode.attributes,
            sparqlUri: subjectUri,
            sparqlType: typeUri,
            sources: ['qdrant', 'sparql'], // Multi-source attribution
          };
          return;
        }
        
        nodeIds.add(nodeId);
        
        // Determine entity type from RDF type URI
        const entityType = typeUri ? extractTypeFromUri(typeUri) : 'Entity';
        
        // Create node
        const node: GraphNode = {
          id: nodeId,
          label: label || `SPARQL Result ${index + 1}`,
          type: 'custodian', // Most SPARQL results are heritage custodians
          entityType: mapInstitutionTypeToEntityType(entityType.toLowerCase()) || entityType,
          attributes: {
            uri: subjectUri,
            rdfType: typeUri,
            source: 'sparql',
            sourceColor: '#10b981', // Emerald for SPARQL
            ...binding, // Include all binding variables
          },
        };
        
        nodes.push(node);
      });
    }
    
    // =========================================================================
    // Process TypeDB results (TypeQL knowledge graph)
    // Format: Array of records with entity attributes
    // =========================================================================
    if (hasTypedbResults) {
      lastContext!.typedbResults.forEach((record, index) => {
        // TypeDB returns entities with various attribute patterns
        // Common pattern: { x: {...entity...}, n: { value: 'name' } }
        const entity = record.x as Record<string, unknown> | undefined;
        const nameAttr = record.n as { value?: string } | undefined;
        const name = nameAttr?.value || (entity as Record<string, unknown>)?.name as string || '';
        
        // Extract entity identifier if available
        const entityId = entity ? String((entity as Record<string, unknown>).iid || index) : String(index);
        const nodeId = `typedb:${entityId}`;
        
        // Skip if no meaningful data or already processed
        if (!name && !entity) return;
        if (nodeIds.has(nodeId)) return;
        
        // Check for cross-source duplicates by label
        const normalizedLabel = name.toLowerCase().trim();
        if (normalizedLabel) {
          const existingNode = nodes.find(n => 
            n.label.toLowerCase().trim() === normalizedLabel && n.attributes?.source !== 'typedb'
          );
          
          if (existingNode) {
            // Merge: add TypeDB source info to existing node
            const existingSources = existingNode.attributes?.sources as string[] | undefined;
            existingNode.attributes = {
              ...existingNode.attributes,
              typedbId: entityId,
              sources: existingSources ? [...existingSources, 'typedb'] : [existingNode.attributes?.source as string || 'unknown', 'typedb'],
            };
            return;
          }
        }
        
        nodeIds.add(nodeId);
        
        // Determine entity type from TypeDB entity type if available
        const typedbType = String((entity as Record<string, unknown>)?.type || 'heritage_custodian');
        
        // Create node
        const node: GraphNode = {
          id: nodeId,
          label: name || `TypeDB Result ${index + 1}`,
          type: 'custodian',
          entityType: mapInstitutionTypeToEntityType(typedbType.toLowerCase()) || 'Organization',
          attributes: {
            ...record,
            source: 'typedb',
            sourceColor: '#f59e0b', // Amber for TypeDB
          },
        };
        
        nodes.push(node);
      });
    }
    
    const graphData: GraphData = { nodes, edges };
    
    // Log summary with breakdown by source
    const qdrantCount = nodes.filter(n => n.attributes?.source === 'qdrant').length;
    const sparqlCount = nodes.filter(n => n.attributes?.source === 'sparql').length;
    const typedbCount = nodes.filter(n => n.attributes?.source === 'typedb').length;
    const multiSourceCount = nodes.filter(n => Array.isArray(n.attributes?.sources)).length;
    
    console.log(
      `[KnowledgeGraph] Context mode: ${nodes.length} nodes (Qdrant: ${qdrantCount}, SPARQL: ${sparqlCount}, TypeDB: ${typedbCount}, Multi-source: ${multiSourceCount}), ` +
      `${edges.length} edges`
    );
    
    console.log('[KnowledgeGraph DEBUG] Setting contextGraphData with:', {
      nodeCount: graphData.nodes.length,
      edgeCount: graphData.edges.length,
      firstNodeLabel: graphData.nodes[0]?.label,
      // Debug: Check GHCID presence in first 5 Qdrant nodes
      first5QdrantNodesGhcid: graphData.nodes
        .filter(n => n.attributes?.source === 'qdrant')
        .slice(0, 5)
        .map(n => ({
          label: n.label,
          ghcid: n.attributes?.ghcid,
          ghcid_current: n.attributes?.ghcid_current,
          hasGhcidField: 'ghcid' in (n.attributes || {}),
          attrKeys: Object.keys(n.attributes || {}),
        })),
    });
    
    setContextGraphData(graphData);
  }, [lastContext?.qdrantResults, lastContext?.sparqlResults, lastContext?.typedbResults]);

  // Auto-switch to Context mode when RAG results become available
  // This improves UX by showing retrieved entities immediately when user opens the KG panel
  useEffect(() => {
    // Only auto-switch if:
    // 1. Context data is available (has nodes)
    // 2. We're currently in global mode (avoid switching back if user manually selected global)
    // 3. The panel is visible (showGraphProjector)
    // 4. User hasn't manually selected a mode in this session
    if (
      contextGraphData && 
      contextGraphData.nodes.length > 0 && 
      graphProjectorMode === 'global' && 
      showGraphProjector &&
      !userSelectedModeRef.current
    ) {
      console.log('[KnowledgeGraph] Auto-switching to context mode (context data available with', contextGraphData.nodes.length, 'nodes)');
      setGraphProjectorMode('context');
    }
  }, [contextGraphData, graphProjectorMode, showGraphProjector]);

  // Reset the manual mode selection flag when panel is closed
  useEffect(() => {
    if (!showGraphProjector) {
      userSelectedModeRef.current = false;
    }
  }, [showGraphProjector]);

  // Wrapper for user-initiated mode changes (marks manual selection)
  const handleGraphProjectorModeChange = useCallback((mode: 'global' | 'context') => {
    userSelectedModeRef.current = true;
    setGraphProjectorMode(mode);
  }, []);

  // Helper function to map institution types to entity types for graph coloring
  function mapInstitutionTypeToEntityType(type: string): string {
    const typeMap: Record<string, string> = {
      'museum': 'Museum',
      'library': 'Library',
      'archive': 'Archive',
      'gallery': 'Gallery',
      'collection': 'Collection',
      'm': 'Museum',
      'l': 'Library',
      'a': 'Archive',
      'g': 'Gallery',
      'r': 'Research',
      'o': 'Organization',
      'c': 'Corporation',
      'u': 'Unknown',
      'b': 'Botanical',
      'e': 'Education',
      's': 'Society',
      'f': 'Feature',
      'i': 'Intangible',
      'x': 'Mixed',
      'p': 'Personal',
      'h': 'HolySite',
      'd': 'Digital',
      'n': 'NGO',
      't': 'TasteSmell',
    };
    return typeMap[type.toLowerCase()] || 'Organization';
  }

  // Load ALL embeddings from Qdrant (for comprehensive search)
  const loadAllEmbeddings = useCallback(async () => {
    if (!qdrantStatus.isConnected || isLoadingAllEmbeddings) return;
    
    setIsLoadingAllEmbeddings(true);
    try {
      const collectionName = 'heritage_custodians';
      const allPoints: EmbeddingPoint[] = [];
      let offset: string | number | null = null;
      
      // Scroll through all points in batches
      while (true) {
        const { points, nextOffset } = await scrollPoints(collectionName, 500, offset);
        
        const batch: EmbeddingPoint[] = points.map(p => ({
          id: p.id,
          vector: p.vector,
          payload: p.payload,
        }));
        
        allPoints.push(...batch);
        
        if (!nextOffset || points.length === 0) break;
        offset = nextOffset;
      }
      
      setEmbeddingPoints(allPoints);
      setTotalEmbeddingPoints(allPoints.length);
      console.log(`[EmbeddingProjector] Loaded all ${allPoints.length} points`);
    } catch (err) {
      console.error('Failed to load all embeddings:', err);
    } finally {
      setIsLoadingAllEmbeddings(false);
    }
  }, [qdrantStatus.isConnected, isLoadingAllEmbeddings, scrollPoints]);
  
  // Handle panel resize with mouse
  useEffect(() => {
    if (!isResizing) return;
    
    const handleMouseMove = (e: MouseEvent) => {
      const newWidth = window.innerWidth - e.clientX;
      // Constrain width between 280px and 800px
      setVizPanelWidth(Math.min(800, Math.max(280, newWidth)));
    };
    
    const handleMouseUp = () => {
      setIsResizing(false);
    };
    
    document.addEventListener('mousemove', handleMouseMove);
    document.addEventListener('mouseup', handleMouseUp);
    
    return () => {
      document.removeEventListener('mousemove', handleMouseMove);
      document.removeEventListener('mouseup', handleMouseUp);
    };
  }, [isResizing]);
  
  // Load cache stats when panel is opened
  useEffect(() => {
    if (showCachePanel) {
      getCacheStats().then(setCacheStats).catch(console.error);
    }
  }, [showCachePanel, getCacheStats]);
  
  // ============================================================================
  // Handlers
  // ============================================================================
  
  const showNotification = useCallback((message: string) => {
    setNotification(message);
    setTimeout(() => setNotification(null), 3000);
  }, []);
  
  // Cache handlers
  const handleToggleCache = useCallback(() => {
    setCacheEnabled(!cacheEnabled);
    showNotification(cacheEnabled ? t('cacheDisabled') : t('cacheEnabled'));
  }, [cacheEnabled, setCacheEnabled, showNotification, t]);
  
  const handleClearCache = useCallback(async () => {
    try {
      const result = await clearCache();
      const stats = await getCacheStats();
      setCacheStats(stats);
      
      // Show appropriate notification based on result
      if (result.localCleared && result.sharedCleared) {
        showNotification(t('cacheCleared'));
      } else if (result.localCleared && !result.sharedCleared) {
        showNotification(t('cacheClearedLocal'));
      } else {
        showNotification(t('cacheClearFailed'));
      }
    } catch (error) {
      console.error('Failed to clear cache:', error);
      showNotification(t('cacheClearFailed'));
    }
  }, [clearCache, getCacheStats, showNotification, t]);
  
  const handleThresholdChange = useCallback((value: number) => {
    setCacheSimilarityThresholdLocal(value);
    setCacheSimilarityThreshold(value);
  }, [setCacheSimilarityThreshold]);
  
  // Toggle data source selection
  const toggleSource = useCallback((source: DataSource) => {
    setSelectedSources(prev => {
      if (prev.includes(source)) {
        // Don't allow deselecting the last source
        if (prev.length === 1) {
          showNotification(t('noSourcesSelected'));
          return prev;
        }
        return prev.filter(s => s !== source);
      } else {
        return [...prev, source];
      }
    });
  }, [showNotification, t]);
  
  const saveHistory = useCallback((items: Array<{ question: string; timestamp: Date }>) => {
    try {
      const trimmed = items.slice(-20);
      localStorage.setItem(STORAGE_KEYS.HISTORY, JSON.stringify(trimmed));
      setHistory(trimmed);
    } catch (e) {
      console.error('Failed to save history:', e);
    }
  }, []);
  
  const handleSend = async () => {
    const question = input.trim();
    if (!question || ragLoading) return;
    
    const userMessage: ConversationMessage = {
      id: `msg-${Date.now()}`,
      role: 'user',
      content: question,
      timestamp: new Date(),
    };
    
    const loadingMessage: ConversationMessage = {
      id: `msg-${Date.now()}-loading`,
      role: 'assistant',
      content: t('searching'),
      timestamp: new Date(),
      isLoading: true,
    };
    
    setMessages(prev => [...prev, userMessage, loadingMessage]);
    setInput('');
    
    try {
      const response = await queryRAG(question, {
        llmProvider: uiState.llmProvider,
        language,
        conversationHistory: messages,
        bypassCache: bypassCache,
        // Data source selection - qdrant is always included, toggle sparql/typedb
        includeSparql: selectedSources.includes('sparql'),
        includeTypeDB: selectedSources.includes('typedb'),
      });
      
      // Reset bypass cache after query (it's a one-shot toggle)
      if (bypassCache) {
        setBypassCache(false);
      }
      
      // Save to history
      saveHistory([...history, { question, timestamp: new Date() }]);
      
      // Update visualization
      if (response.visualizationType && response.visualizationType !== 'none') {
        setActiveVizType(response.visualizationType);
        setActiveVizData(response.visualizationData || null);
      }
      
      // Store retrieved results for social network visualization
      console.log('[ConversationPage] response.retrievedResults:', response.retrievedResults);
      console.log('[ConversationPage] response.queryType:', response.queryType);
      if (response.retrievedResults && response.retrievedResults.length > 0) {
        console.log('[ConversationPage] Setting activeRetrievedResults with', response.retrievedResults.length, 'items');
        setActiveRetrievedResults(response.retrievedResults);
        setActiveQueryType(response.queryType || null);
        // Auto-switch to network view for person queries
        if (response.queryType === 'person') {
          console.log('[ConversationPage] Auto-switching to network view (person query)');
          setActiveVizType('network');
        }
      } else {
        console.log('[ConversationPage] No retrievedResults or empty array');
      }
      
      // Capture cache lookup result (updated by queryRAG)
      const wasCacheHit = lastCacheLookup?.found || false;
      const cacheSimilarity = lastCacheLookup?.similarity;
      
      // Replace loading message with response
      setMessages(prev => prev.map(msg =>
        msg.id === loadingMessage.id
          ? {
              ...msg,
              content: response.answer,
              response,
              isLoading: false,
              fromCache: wasCacheHit,
              cacheSimilarity: cacheSimilarity,
            }
          : msg
      ));
      
    } catch (error) {
      setMessages(prev => prev.map(msg =>
        msg.id === loadingMessage.id
          ? {
              ...msg,
              content: t('errorConnection'),
              isLoading: false,
              error: error instanceof Error ? error.message : 'Unknown error',
            }
          : msg
      ));
    }
  };
  
  // Refresh a specific assistant message by re-running the query (bypassing cache)
  const handleRefreshMessage = async (assistantMessageId: string) => {
    // Find the assistant message and the user message that preceded it
    const msgIndex = messages.findIndex(m => m.id === assistantMessageId);
    if (msgIndex < 1) return; // Need at least one message before
    
    // Find the user message before this assistant message
    let userMessageIndex = msgIndex - 1;
    while (userMessageIndex >= 0 && messages[userMessageIndex].role !== 'user') {
      userMessageIndex--;
    }
    if (userMessageIndex < 0) return;
    
    const originalQuestion = messages[userMessageIndex].content;
    if (!originalQuestion || ragLoading) return;
    
    // Mark this message as refreshing
    setRefreshingMessageId(assistantMessageId);
    
    // Build conversation history up to (but not including) the current exchange
    const historyForContext = messages.slice(0, userMessageIndex);
    
    try {
      const response = await queryRAG(originalQuestion, {
        llmProvider: uiState.llmProvider,
        language,
        conversationHistory: historyForContext,
        bypassCache: true, // Always bypass cache for refresh
        // Data source selection - use current settings
        includeSparql: selectedSources.includes('sparql'),
        includeTypeDB: selectedSources.includes('typedb'),
      });
      
      // Update visualization
      if (response.visualizationType && response.visualizationType !== 'none') {
        setActiveVizType(response.visualizationType);
        setActiveVizData(response.visualizationData || null);
      }
      
      // Store retrieved results for social network visualization
      if (response.retrievedResults && response.retrievedResults.length > 0) {
        setActiveRetrievedResults(response.retrievedResults);
        setActiveQueryType(response.queryType || null);
        if (response.queryType === 'person') {
          setActiveVizType('network');
        }
      }
      
      // Replace the assistant message with fresh response
      setMessages(prev => prev.map(msg =>
        msg.id === assistantMessageId
          ? {
              ...msg,
              content: response.answer,
              response,
              isLoading: false,
              fromCache: false, // Fresh response
              cacheSimilarity: undefined,
            }
          : msg
      ));
      
      showNotification(t('responseRefreshed'));
      
    } catch (error) {
      setMessages(prev => prev.map(msg =>
        msg.id === assistantMessageId
          ? {
              ...msg,
              content: t('errorConnection'),
              isLoading: false,
              error: error instanceof Error ? error.message : 'Unknown error',
            }
          : msg
      ));
    } finally {
      setRefreshingMessageId(null);
    }
  };
  
  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault();
      handleSend();
    }
  };
  
  const handleExampleClick = (question: string) => {
    setInput(question);
    inputRef.current?.focus();
  };
  
  const handleCopyQuery = async (id: string, query: string) => {
    await navigator.clipboard.writeText(query);
    setCopiedId(id);
    setTimeout(() => setCopiedId(null), 2000);
  };
  
  const handleHistoryClick = (item: { question: string }) => {
    setInput(item.question);
    setShowHistoryDropdown(false);
    inputRef.current?.focus();
  };
  
  const handleClearHistory = () => {
    localStorage.removeItem(STORAGE_KEYS.HISTORY);
    setHistory([]);
    setShowHistoryDropdown(false);
  };
  
  const handleExportConversation = () => {
    if (messages.length === 0) return;
    
    const exportData = {
      version: 1,
      exportedAt: new Date().toISOString(),
      llmProvider: uiState.llmProvider,
      messages: messages.map(m => ({
        role: m.role,
        content: m.content,
        timestamp: m.timestamp,
        response: m.response,
      })),
    };
    
    const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `conversation-${new Date().toISOString().slice(0, 10)}.json`;
    a.click();
    URL.revokeObjectURL(url);
    showNotification(t('exportSuccess'));
  };
  
  const handleImportConversation = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;
    
    const reader = new FileReader();
    reader.onload = (event) => {
      try {
        const data = JSON.parse(event.target?.result as string);
        if (data.version && data.messages && Array.isArray(data.messages)) {
          const importedMessages: ConversationMessage[] = data.messages.map(
            (m: ConversationMessage, i: number) => ({
              id: `imported-${Date.now()}-${i}`,
              role: m.role,
              content: m.content,
              timestamp: new Date(m.timestamp),
              response: m.response,
            })
          );
          setMessages(importedMessages);
          if (data.llmProvider) {
            setLLMProvider(data.llmProvider);
          }
          showNotification(t('importSuccess'));
        } else {
          showNotification(t('importError'));
        }
      } catch {
        showNotification(t('importError'));
      }
    };
    reader.readAsText(file);
    e.target.value = '';
  };
  
  const handleClearConversation = () => {
    setMessages([]);
    clearContext();
    setActiveVizData(null);
    setActiveRetrievedResults(null);
    setActiveQueryType(null);
    setHighlightedSourceIndices([]);
    setAdditionalContextPoints([]);
    showNotification(t('conversationCleared'));
  };
  
  // Handler for loading more results and updating visualization data
  const handleLoadMoreResults = useCallback(async () => {
    const newResults = await loadMoreResults();
    
    if (newResults.length > 0) {
      // For person queries, update activeRetrievedResults for social network graph
      if (activeQueryType === 'person') {
        const newRetrievedResults: RetrievedResult[] = newResults.map((result) => ({
          type: 'person' as const,
          person_id: String(result.id),
          name: (result.payload?.name as string) || 'Unknown',
          headline: result.payload?.headline as string | undefined,
          custodian_name: result.payload?.custodian_name as string | undefined,
          custodian_slug: result.payload?.custodian_slug as string | undefined,
          heritage_relevant: result.payload?.heritage_relevant as boolean | undefined,
          heritage_type: result.payload?.heritage_type as string | undefined,
          linkedin_url: result.payload?.linkedin_url as string | null | undefined,
          score: result.score,
        }));
        
        // Combine with existing results (avoiding duplicates by person_id)
        setActiveRetrievedResults(prev => {
          if (!prev) return newRetrievedResults;
          const existingIds = new Set(prev.map(r => r.person_id));
          const uniqueNew = newRetrievedResults.filter(r => !existingIds.has(r.person_id));
          console.log(`[ConversationPage] Adding ${uniqueNew.length} new person results (${newRetrievedResults.length - uniqueNew.length} duplicates filtered)`);
          return [...prev, ...uniqueNew];
        });
      }
      
      // For institution queries (or any query), update activeVizData for map/table/cards
      if (activeVizData) {
        // Convert Qdrant results to InstitutionData format
        const newInstitutions: InstitutionData[] = newResults.map((result) => ({
          id: String(result.id),
          name: (result.payload?.name as string) || (result.payload?.title as string) || 'Unknown',
          type: (result.payload?.institution_type as string) || (result.payload?.type as string),
          city: (result.payload?.city as string) || (result.payload?.municipality as string),
          province: (result.payload?.province as string) || (result.payload?.admin1 as string),
          country: (result.payload?.country as string),
          latitude: result.payload?.latitude as number,
          longitude: result.payload?.longitude as number,
          website: (result.payload?.website as string),
          description: (result.payload?.description as string),
          rating: result.payload?.rating as number,
          ghcid: (result.payload?.ghcid as string),
        }));
        
        // Combine with existing institutions (avoiding duplicates by id)
        const existingIds = new Set(activeVizData.institutions?.map(i => i.id) || []);
        const uniqueNewInstitutions = newInstitutions.filter(i => !existingIds.has(i.id));
        
        setActiveVizData(prevData => {
          if (!prevData) return prevData;
          return {
            ...prevData,
            type: prevData.type, // Ensure type is explicitly set (required field)
            institutions: [...(prevData.institutions || []), ...uniqueNewInstitutions],
          };
        });
      }
    }
  }, [loadMoreResults, activeVizData, activeQueryType]);
  
  // Handler for adding point to context from Embedding Projector
  const handleContextSelect = useCallback((point: EmbeddingPoint) => {
    // Check if already in additional context
    const alreadyAdded = additionalContextPoints.some(p => String(p.id) === String(point.id));
    if (alreadyAdded) return;
    
    // Add to additional context
    setAdditionalContextPoints(prev => [...prev, point]);
    
    // Also add to highlighted indices if not already there
    const pointIndex = embeddingPoints.findIndex(p => String(p.id) === String(point.id));
    if (pointIndex !== -1 && !highlightedSourceIndices.includes(pointIndex)) {
      setHighlightedSourceIndices(prev => [...prev, pointIndex]);
    }
    
    // Show notification
    const pointName = (point.payload?.name as string) || (point.payload?.title as string) || `Point ${point.id}`;
    showNotification(`${t('pointAddedToContext')}: ${pointName}`);
  }, [additionalContextPoints, embeddingPoints, highlightedSourceIndices, showNotification, t]);
  
  // ============================================================================
  // Render
  // ============================================================================
  
  return (
    <div className="conversation-page">
      {/* Notification Toast */}
      {notification && (
        <div className="conversation-notification">
          <span>{notification}</span>
          <button onClick={() => setNotification(null)} className="conversation-notification__close">
            <X size={14} />
          </button>
        </div>
      )}
      
      {/* Hidden file input */}
      <input
        type="file"
        ref={fileInputRef}
        accept=".json"
        onChange={handleImportConversation}
        style={{ display: 'none' }}
      />
      
      {/* Main Layout */}
      <div className={`conversation-layout ${activeVizData ? 'conversation-layout--with-viz' : ''}`}>
        {/* Chat Panel */}
        <div className="conversation-chat">
          {/* Header */}
          <div className="conversation-chat__header">
            <div className="conversation-chat__title">
              <Sparkles size={24} className="conversation-chat__icon" />
              <div>
                <h1>{t('pageTitle')}</h1>
                <p>{t('pageSubtitle')}</p>
              </div>
            </div>
            <button
              className="conversation-chat__new-btn"
              onClick={handleClearConversation}
              title={t('new')}
              type="button"
            >
              <Plus size={18} />
              <span>{t('new')}</span>
            </button>
          </div>
          
          {/* Input Area - Top */}
          <div className="conversation-chat__input-area">
            <div className="conversation-chat__input-container">
              <textarea
                ref={inputRef}
                className="conversation-chat__input"
                placeholder={t('placeholder')}
                value={input}
                onChange={e => setInput(e.target.value)}
                onKeyDown={handleKeyDown}
                disabled={ragLoading}
                rows={1}
              />
              <button
                className="conversation-chat__send-btn"
                onClick={handleSend}
                disabled={!input.trim() || ragLoading}
                title={t('send')}
              >
                {ragLoading ? (
                  <>
                    <Loader2 className="conversation-chat__send-icon conversation-chat__send-icon--loading" size={18} />
                    <span>{t('thinking')}</span>
                  </>
                ) : (
                  <>
                    <Send className="conversation-chat__send-icon" size={18} />
                    <span>{t('send')}</span>
                  </>
                )}
              </button>
            </div>
            
            <div className="conversation-chat__toolbar">
              {/* LLM Provider Selector */}
              <div className="conversation-chat__model-selector" ref={modelDropdownRef}>
                <button
                  className="conversation-chat__model-btn"
                  onClick={() => setShowModelDropdown(!showModelDropdown)}
                  type="button"
                >
                  <span 
                    className="conversation-chat__provider-indicator"
                    style={{ backgroundColor: LLM_PROVIDERS.find(p => p.id === uiState.llmProvider)?.color }}
                  />
                  <span className="conversation-chat__model-name">
                    {LLM_PROVIDERS.find(p => p.id === uiState.llmProvider)?.shortName || 'Z.AI'}
                  </span>
                  <span className="conversation-chat__provider-badge">
                    {LLM_PROVIDERS.find(p => p.id === uiState.llmProvider)?.badge}
                  </span>
                  <ChevronDown 
                    size={14} 
                    className={`conversation-chat__model-chevron ${showModelDropdown ? 'conversation-chat__model-chevron--open' : ''}`} 
                  />
                </button>
                {showModelDropdown && (
                  <div className="conversation-chat__model-dropdown">
                    {LLM_PROVIDERS.map(provider => (
                      <button
                        key={provider.id}
                        className={`conversation-chat__model-option ${uiState.llmProvider === provider.id ? 'conversation-chat__model-option--selected' : ''}`}
                        onClick={() => {
                          setLLMProvider(provider.id);
                          setShowModelDropdown(false);
                        }}
                      >
                        <span 
                          className="conversation-chat__provider-dot"
                          style={{ backgroundColor: provider.color }}
                        />
                        <span className="conversation-chat__model-option-name">
                          {provider.name} {provider.badge}
                        </span>
                        <span className="conversation-chat__model-option-desc">{provider.description}</span>
                      </button>
                    ))}
                  </div>
                )}
              </div>
              
              {/* Data Source Toggles */}
              <div className="conversation-chat__sources">
                <span className="conversation-chat__sources-label">{t('dataSources')}:</span>
                <div className="conversation-chat__sources-toggles">
                  {DATA_SOURCES.map(source => {
                    const Icon = source.icon;
                    const isSelected = selectedSources.includes(source.id);
                    return (
                      <button
                        key={source.id}
                        className={`conversation-chat__source-toggle ${isSelected ? 'conversation-chat__source-toggle--active' : ''}`}
                        onClick={() => toggleSource(source.id)}
                        title={source.description[language]}
                        style={{
                          '--source-color': source.color,
                          borderColor: isSelected ? source.color : undefined,
                          backgroundColor: isSelected ? `${source.color}15` : undefined,
                        } as React.CSSProperties}
                        type="button"
                      >
                        <Icon size={14} style={{ color: isSelected ? source.color : undefined }} />
                        <span>{source.name[language]}</span>
                      </button>
                    );
                  })}
                </div>
              </div>
              
              <div className="conversation-chat__actions">
                {/* History */}
                <div className="conversation-chat__history-selector" ref={historyDropdownRef}>
                  <button
                    className="conversation-chat__action-btn"
                    onClick={() => setShowHistoryDropdown(!showHistoryDropdown)}
                    title={t('history')}
                    type="button"
                  >
                    <History size={16} />
                    <span>{t('history')}</span>
                  </button>
                  {showHistoryDropdown && (
                    <div className="conversation-chat__history-dropdown">
                      <div className="conversation-chat__history-header">
                        <span>{t('history')}</span>
                        {history.length > 0 && (
                          <button
                            className="conversation-chat__history-clear"
                            onClick={handleClearHistory}
                          >
                            {t('clearHistory')}
                          </button>
                        )}
                      </div>
                      {history.length === 0 ? (
                        <div className="conversation-chat__history-empty">{t('noHistory')}</div>
                      ) : (
                        <div className="conversation-chat__history-list">
                          {history.slice().reverse().map((item, i) => (
                            <button
                              key={i}
                              className="conversation-chat__history-item"
                              onClick={() => handleHistoryClick(item)}
                            >
                              {item.question}
                            </button>
                          ))}
                        </div>
                      )}
                    </div>
                  )}
                </div>
                
                {/* Export */}
                <button
                  className="conversation-chat__action-btn"
                  onClick={handleExportConversation}
                  title={t('exportConversation')}
                  disabled={messages.length === 0}
                  type="button"
                >
                  <Download size={16} />
                  <span>{t('export')}</span>
                </button>
                
                {/* Import */}
                <button
                  className="conversation-chat__action-btn"
                  onClick={() => fileInputRef.current?.click()}
                  title={t('importConversation')}
                  type="button"
                >
                  <Upload size={16} />
                  <span>{t('import')}</span>
                </button>
                
                {/* Clear */}
                {messages.length > 0 && (
                  <button
                    className="conversation-chat__action-btn conversation-chat__action-btn--danger"
                    onClick={handleClearConversation}
                    title={t('clearConversation')}
                    type="button"
                  >
                    <Trash2 size={16} />
                    <span>{t('clear')}</span>
                  </button>
                )}
                
                {/* Reset Cache - Warning Style */}
                <button
                  className="conversation-chat__action-btn conversation-chat__action-btn--warning"
                  onClick={handleClearCache}
                  title={t('clearCache')}
                  type="button"
                >
                  <RefreshCw size={16} />
                  <span>{t('clearCache')}</span>
                </button>
                
                {/* Bypass Cache Toggle - One-shot fresh query */}
                <button
                  className={`conversation-chat__action-btn ${bypassCache ? 'conversation-chat__action-btn--bypass-active' : ''}`}
                  onClick={() => {
                    setBypassCache(!bypassCache);
                    showNotification(t(bypassCache ? 'bypassCacheDisabled' : 'bypassCacheEnabled'));
                  }}
                  title={t('bypassCache')}
                  type="button"
                >
                  <Zap size={16} />
                  <span>{t('bypassCache')}</span>
                </button>
                
                {/* Cache Status Indicator */}
                {lastCacheLookup && (
                  <div className={`conversation-cache-status ${lastCacheLookup.found ? 'conversation-cache-status--hit' : 'conversation-cache-status--miss'}`}>
                    <Zap size={14} className="conversation-cache-status__icon" />
                    <span className="conversation-cache-status__text">
                      {lastCacheLookup.found ? t('cacheHit') : t('cacheMiss')}
                    </span>
                    {lastCacheLookup.found && lastCacheLookup.similarity && (
                      <span className="conversation-cache-status__similarity">
                        {Math.round(lastCacheLookup.similarity * 100)}%
                      </span>
                    )}
                  </div>
                )}
                
                {/* Cache Settings Button */}
                <button
                  className={`conversation-chat__action-btn ${cacheEnabled ? 'conversation-chat__action-btn--active' : ''}`}
                  onClick={() => setShowCachePanel(true)}
                  title={t('cacheSettings')}
                  type="button"
                >
                  <Database size={16} />
                  <span>{t('cache')}</span>
                </button>
                
                {/* Embedding Projector Toggle */}
                <button
                  className={`conversation-chat__action-btn ${showProjector ? 'conversation-chat__action-btn--active' : ''}`}
                  onClick={() => setShowProjector(!showProjector)}
                  title={showProjector ? t('hideEmbeddings') : t('showEmbeddings')}
                  type="button"
                >
                  <Layers size={16} />
                  <span>{t('embeddings')}</span>
                </button>
                
                {/* Knowledge Graph Projector Toggle */}
                <button
                  className={`conversation-chat__action-btn ${showGraphProjector ? 'conversation-chat__action-btn--active' : ''}`}
                  onClick={() => setShowGraphProjector(!showGraphProjector)}
                  title={showGraphProjector ? t('hideGraph') : t('showGraph')}
                  type="button"
                >
                  <Network size={16} />
                  <span>{t('graph')}</span>
                </button>
              </div>
              
              <div className="conversation-chat__powered-by">
                <Sparkles size={12} />
                <span>DSPy + {selectedSources.map(s => 
                  DATA_SOURCES.find(d => d.id === s)?.name[language]
                ).join(' + ')}</span>
              </div>
            </div>
          </div>
          
          {/* Messages */}
          <div className="conversation-chat__messages" ref={messagesContainerRef}>
            {messages.length === 0 ? (
              <div className="conversation-chat__welcome">
                <div className="conversation-chat__welcome-header">
                  <Sparkles size={32} className="conversation-chat__welcome-icon" />
                  <h2>{t('welcomeTitle')}</h2>
                </div>
                <p className="conversation-chat__welcome-description">{t('welcomeDescription')}</p>
                
                <div className="conversation-chat__examples">
                  <span className="conversation-chat__examples-title">{t('exampleQuestions')}</span>
                  <div className="conversation-chat__examples-list">
                    {EXAMPLE_QUESTIONS[language].map((question, idx) => (
                      <button
                        key={idx}
                        className="conversation-chat__example-btn"
                        onClick={() => handleExampleClick(question)}
                      >
                        {question}
                      </button>
                    ))}
                  </div>
                </div>
              </div>
            ) : (
              <>
                {messages.map(message => (
                  <div
                    key={message.id}
                    className={`conversation-message conversation-message--${message.role}`}
                  >
                    <div className="conversation-message__content">
                      {message.isLoading ? (
                        <LoadingStages progress={retrievalProgress} language={language} />
                      ) : isErrorMessage(message) ? (
                        <div className="conversation-message__error">
                          <div className="conversation-message__error-content">
                            <AlertCircle size={16} />
                            <span>{message.content}</span>
                          </div>
                          {/* Retry button for error messages - clears cache and retries */}
                          {message.role === 'assistant' && (
                            <button
                              className="conversation-message__retry-btn"
                              onClick={() => handleRefreshMessage(message.id)}
                              disabled={ragLoading || refreshingMessageId === message.id}
                              title={t('clearCacheAndRetry')}
                            >
                              <RefreshCw size={14} className={refreshingMessageId === message.id ? 'conversation-message__refresh-icon--spinning' : ''} />
                              {refreshingMessageId === message.id ? t('retryingQuery') : t('retryQuery')}
                            </button>
                          )}
                        </div>
                      ) : (
                        <>
                          <p className="conversation-message__text">{message.content}</p>
                          
                          {/* Query boxes */}
                          {message.response?.sparqlQuery && (
                            <div className="conversation-message__query-box">
                              <div className="conversation-message__query-header">
                                <span>{t('sparqlQuery')}</span>
                                <button
                                  className="conversation-message__query-copy"
                                  onClick={() => handleCopyQuery(`sparql-${message.id}`, message.response!.sparqlQuery!)}
                                >
                                  {copiedId === `sparql-${message.id}` ? (
                                    <><Check size={14} /> {t('copied')}</>
                                  ) : (
                                    <><Copy size={14} /> {t('copyQuery')}</>
                                  )}
                                </button>
                              </div>
                              <pre className="conversation-message__query-code">
                                <code>{message.response.sparqlQuery}</code>
                              </pre>
                            </div>
                          )}
                          
                          {/* Confidence indicator */}
                          {message.response && (
                            <div className="conversation-message__meta">
                              {(message as ConversationMessage & { fromCache?: boolean; cacheSimilarity?: number }).fromCache && (
                                <span className="conversation-message__cache-badge">
                                  <Zap size={12} />
                                  {t('fromCache')}
                                  {(message as ConversationMessage & { cacheSimilarity?: number }).cacheSimilarity && (
                                    <span> ({Math.round(((message as ConversationMessage & { cacheSimilarity?: number }).cacheSimilarity || 0) * 100)}%)</span>
                                  )}
                                </span>
                              )}
                              
                              {/* Refresh button - re-run query bypassing cache */}
                              <button
                                className={`conversation-message__refresh-btn ${(message as ConversationMessage & { fromCache?: boolean }).fromCache ? 'conversation-message__refresh-btn--cached' : ''}`}
                                onClick={() => handleRefreshMessage(message.id)}
                                disabled={ragLoading || refreshingMessageId === message.id}
                                title={t('refreshResponse')}
                              >
                                <RefreshCw size={12} className={refreshingMessageId === message.id ? 'conversation-message__refresh-icon--spinning' : ''} />
                                {refreshingMessageId === message.id ? t('refreshingResponse') : t('refreshResponse')}
                              </button>
                              
                              <span className="conversation-message__confidence">
                                {t('confidence')}: {Math.round(message.response.confidence * 100)}%
                              </span>
                              {message.response.context && (
                                <span className="conversation-message__results">
                                  {message.response.context.totalRetrieved} {t('resultsFound')}
                                </span>
                              )}
                            </div>
                          )}
                        </>
                      )}
                    </div>
                  </div>
                ))}
                <div ref={messagesEndRef} />
              </>
            )}
          </div>
        </div>
        
        {/* Visualization Panel - Only show when we have data */}
        {activeVizData && (
          <div 
            className={`conversation-viz-panel conversation-viz-panel--animated ${vizExpanded ? 'conversation-viz-panel--expanded' : ''} ${isResizing ? 'conversation-viz-panel--resizing' : ''}`}
            style={{ width: vizExpanded ? '100%' : `${vizPanelWidth}px` }}
          >
            {/* Resize handle */}
            <div 
              className="conversation-viz-panel__resize-handle"
              onMouseDown={() => setIsResizing(true)}
              title={t('resizePanel') || 'Resize panel'}
            />
            <VisualizationPanel
              type={activeVizType}
              data={activeVizData ?? undefined}
              language={language}
              isExpanded={vizExpanded}
              onToggleExpand={() => setVizExpanded(!vizExpanded)}
              onChangeType={setActiveVizType}
              retrievedResults={activeRetrievedResults}
              queryType={activeQueryType}
              paginationState={paginationState}
              isLoadingMore={isLoadingMore}
              onLoadMore={handleLoadMoreResults}
              infiniteScrollEnabled={infiniteScrollEnabled}
              onToggleInfiniteScroll={() => setInfiniteScrollEnabled(prev => !prev)}
            />
          </div>
        )}
        
        {/* Embedding Projector Panel - Draggable */}
        {showProjector && (
          <ConversationEmbeddingPanel
            points={projectorMode === 'context' ? contextEmbeddingPoints : embeddingPoints}
            isLoading={embeddingsLoading}
            highlightedIndices={highlightedSourceIndices}
            onClose={() => setShowProjector(false)}
            title={t('embeddingProjector')}
            t={t as (key: string) => string}
            language={language}
            simpleMode={projectorSimpleMode}
            isPinned={projectorPinned}
            onTogglePin={() => setProjectorPinned(prev => !prev)}
            onToggleSimpleMode={() => setProjectorSimpleMode(prev => !prev)}
            onContextSelect={handleContextSelect}
            mode={projectorMode}
            onModeChange={setProjectorMode}
            contextPointsCount={contextEmbeddingPoints.length}
            totalPointsCount={totalEmbeddingPoints}
            onLoadAll={loadAllEmbeddings}
            isLoadingAll={isLoadingAllEmbeddings}
          />
        )}
        
        {/* Knowledge Graph Panel - Draggable */}
        {showGraphProjector && (
          <ConversationKnowledgeGraphPanel
            onClose={() => setShowGraphProjector(false)}
            t={t as (key: string) => string}
            language={language}
            isPinned={graphProjectorPinned}
            onTogglePin={() => setGraphProjectorPinned(prev => !prev)}
            mode={graphProjectorMode}
            onModeChange={handleGraphProjectorModeChange}
            contextNodesCount={contextGraphData?.nodes.length || 0}
            backend={graphBackend}
            onBackendChange={setGraphBackend}
            contextData={contextGraphData}
          />
        )}
      </div>
      
      {/* Cache Settings Panel Modal */}
      {showCachePanel && (
        <>
          <div 
            className="conversation-cache-panel__backdrop" 
            onClick={() => setShowCachePanel(false)}
          />
          <div className="conversation-cache-panel">
            <div className="conversation-cache-panel__header">
              <h3 className="conversation-cache-panel__title">
                <Database size={18} />
                {t('cacheSettings')}
              </h3>
              <button
                className="conversation-cache-panel__close-btn"
                onClick={() => setShowCachePanel(false)}
                title="Close"
              >
                <X size={16} />
              </button>
            </div>
            
            <div className="conversation-cache-panel__content">
              {/* Cache Statistics */}
              {cacheStats && (
                <div className="conversation-cache-panel__stats">
                  <div className="conversation-cache-panel__stat">
                    <span className="conversation-cache-panel__stat-label">{t('cacheHits')}</span>
                    <span className="conversation-cache-panel__stat-value conversation-cache-panel__stat-value--success">
                      {cacheStats.totalHits}
                    </span>
                  </div>
                  <div className="conversation-cache-panel__stat">
                    <span className="conversation-cache-panel__stat-label">{t('cacheMisses')}</span>
                    <span className="conversation-cache-panel__stat-value">
                      {cacheStats.totalMisses}
                    </span>
                  </div>
                  <div className="conversation-cache-panel__stat">
                    <span className="conversation-cache-panel__stat-label">{t('cacheHitRate')}</span>
                    <span className={`conversation-cache-panel__stat-value ${
                      cacheStats.hitRate >= 0.5 ? 'conversation-cache-panel__stat-value--success' : 
                      cacheStats.hitRate >= 0.2 ? 'conversation-cache-panel__stat-value--warning' : ''
                    }`}>
                      {Math.round(cacheStats.hitRate * 100)}%
                    </span>
                  </div>
                  <div className="conversation-cache-panel__stat">
                    <span className="conversation-cache-panel__stat-label">{t('cacheEntries')}</span>
                    <span className="conversation-cache-panel__stat-value">
                      {cacheStats.totalEntries}
                    </span>
                    <span className="conversation-cache-panel__stat-subtext">
                      / 500 max
                    </span>
                  </div>
                  <div className="conversation-cache-panel__stat">
                    <span className="conversation-cache-panel__stat-label">{t('cacheStorageUsed')}</span>
                    <span className="conversation-cache-panel__stat-value">
                      {(cacheStats.storageUsedBytes / (1024 * 1024)).toFixed(2)} MB
                    </span>
                  </div>
                </div>
              )}
              
              {/* Settings */}
              <div className="conversation-cache-panel__settings">
                {/* Enable/Disable Toggle */}
                <div className="conversation-cache-panel__setting">
                  <div className="conversation-cache-panel__setting-info">
                    <span className="conversation-cache-panel__setting-label">{t('enableCache')}</span>
                    <span className="conversation-cache-panel__setting-description">{t('enableCacheDescription')}</span>
                  </div>
                  <button
                    className={`conversation-cache-panel__toggle ${cacheEnabled ? 'conversation-cache-panel__toggle--active' : ''}`}
                    onClick={handleToggleCache}
                    type="button"
                  >
                    <span className="conversation-cache-panel__toggle-knob" />
                  </button>
                </div>
                
                {/* Similarity Threshold Slider */}
                <div className="conversation-cache-panel__setting">
                  <div className="conversation-cache-panel__setting-info">
                    <span className="conversation-cache-panel__setting-label">{t('cacheThreshold')}</span>
                    <span className="conversation-cache-panel__setting-description">{t('cacheThresholdDescription')}</span>
                  </div>
                  <div className="conversation-cache-panel__slider-container">
                    <input
                      type="range"
                      className="conversation-cache-panel__slider"
                      min="0.7"
                      max="0.99"
                      step="0.01"
                      value={cacheSimilarityThreshold}
                      onChange={(e) => handleThresholdChange(parseFloat(e.target.value))}
                    />
                    <span className="conversation-cache-panel__slider-value">
                      {Math.round(cacheSimilarityThreshold * 100)}%
                    </span>
                  </div>
                </div>
              </div>
              
              {/* Actions */}
              <div className="conversation-cache-panel__actions">
                <button
                  className="conversation-cache-panel__action-btn conversation-cache-panel__action-btn--danger"
                  onClick={handleClearCache}
                  disabled={!cacheStats || cacheStats.totalEntries === 0}
                  type="button"
                >
                  <Trash2 size={14} />
                  {t('clearCache')}
                </button>
                <button
                  className="conversation-cache-panel__action-btn conversation-cache-panel__action-btn--primary"
                  onClick={() => setShowCachePanel(false)}
                  type="button"
                >
                  <Check size={14} />
                  OK
                </button>
              </div>
            </div>
          </div>
        </>
      )}
    </div>
  );
};

export default ConversationPage;
