Skip to content

index_generator.py

File: oracle/index_generator.py

#!/usr/bin/env python3
"""
Oracle LIMS Knowledge Base Search Interface Generator
Creates a single searchable CLI interface from all markdown and SQL files
"""

import os
import re
import json
import glob
from pathlib import Path
from typing import Dict, List, Tuple, Any

class ContentParser:
    def __init__(self):
        self.sections = []  # List of searchable items
        self.content_map = {}  # Maps section IDs to content

    def parse_markdown_file(self, file_path: str) -> List[Dict]:
        """Parse markdown file and extract sections"""
        with open(file_path, 'r', encoding='utf-8') as f:
            content = f.read()

        sections = []
        lines = content.split('\n')
        current_section = None
        current_content = []

        for line in lines:
            # Main headers (# or ##)
            if line.strip().startswith('#'):
                # Save previous section
                if current_section:
                    sections.append({
                        'title': current_section,
                        'content': '\n'.join(current_content),
                        'file': Path(file_path).name,
                        'type': 'markdown'
                    })

                # Start new section
                current_section = line.strip().lstrip('#').strip()
                current_content = []
            else:
                current_content.append(line)

        # Save last section
        if current_section:
            sections.append({
                'title': current_section,
                'content': '\n'.join(current_content),
                'file': Path(file_path).name,
                'type': 'markdown'
            })

        return sections

    def parse_sql_file(self, file_path: str) -> List[Dict]:
        """Parse SQL file and extract functions, procedures, and comment sections"""
        with open(file_path, 'r', encoding='utf-8') as f:
            content = f.read()

        sections = []
        lines = content.split('\n')

        # Extract comment sections (-- ===== style)
        current_section = None
        current_content = []

        for i, line in enumerate(lines):
            stripped = line.strip()

            # Section headers in comments (-- ===============)
            if stripped.startswith('-- =') and len(stripped) > 10:
                if current_section:
                    sections.append({
                        'title': current_section,
                        'content': '\n'.join(current_content),
                        'file': Path(file_path).name,
                        'type': 'sql_section'
                    })
                current_section = None
                current_content = []
                continue

            # Section titles in comments (-- TITLE)
            if stripped.startswith('-- ') and not stripped.startswith('-- -') and len(stripped) > 5:
                section_title = stripped[3:].strip()
                if section_title and not any(c.isdigit() for c in section_title[:3]):
                    if current_section:
                        sections.append({
                            'title': current_section,
                            'content': '\n'.join(current_content),
                            'file': Path(file_path).name,
                            'type': 'sql_section'
                        })
                    current_section = section_title
                    current_content = []
                    continue

            # Add content
            if current_section:
                current_content.append(line)

        # Save last section
        if current_section:
            sections.append({
                'title': current_section,
                'content': '\n'.join(current_content),
                'file': Path(file_path).name,
                'type': 'sql_section'
            })

        # Extract functions and procedures
        function_pattern = r'(CREATE\s+(?:OR\s+REPLACE\s+)?(?:FUNCTION|PROCEDURE)\s+(\w+))'
        matches = re.finditer(function_pattern, content, re.IGNORECASE | re.MULTILINE)

        for match in matches:
            func_name = match.group(2)
            start_pos = match.start()

            # Find the end of the function (next CREATE or end of file)
            next_create = re.search(r'\nCREATE\s+', content[start_pos + len(match.group(1)):], re.IGNORECASE)
            if next_create:
                end_pos = start_pos + len(match.group(1)) + next_create.start()
                func_content = content[start_pos:end_pos]
            else:
                func_content = content[start_pos:]

            sections.append({
                'title': f"{func_name}()",
                'content': func_content.strip(),
                'file': Path(file_path).name,
                'type': 'sql_function'
            })

        return sections

    def process_directory(self, directory: str) -> Dict:
        """Process all markdown and SQL files in directory"""
        all_sections = []

        # Process markdown files
        for md_file in glob.glob(os.path.join(directory, "*.md")):
            sections = self.parse_markdown_file(md_file)
            all_sections.extend(sections)

        # Process SQL files
        for sql_file in glob.glob(os.path.join(directory, "*.sql")):
            sections = self.parse_sql_file(sql_file)
            all_sections.extend(sections)

        # Create searchable index
        search_index = {}
        for i, section in enumerate(all_sections):
            section_id = f"section_{i}"
            search_index[section_id] = {
                'title': section['title'],
                'file': section['file'],
                'type': section['type'],
                'content': self._process_content_for_display(section['content'])
            }

        return search_index

    def _process_content_for_display(self, content: str) -> str:
        """Process content for HTML display with syntax highlighting"""
        # Convert markdown code blocks to HTML
        content = re.sub(r'```sql\n(.*?)\n```', r'<pre class="language-sql"><code>\1</code></pre>', content, flags=re.DOTALL)
        content = re.sub(r'```\n(.*?)\n```', r'<pre><code>\1</code></pre>', content, flags=re.DOTALL)

        # Convert markdown headers to HTML
        content = re.sub(r'^### (.*?)$', r'<h3>\1</h3>', content, flags=re.MULTILINE)
        content = re.sub(r'^## (.*?)$', r'<h2>\1</h2>', content, flags=re.MULTILINE)
        content = re.sub(r'^# (.*?)$', r'<h1>\1</h1>', content, flags=re.MULTILINE)

        # Convert markdown lists
        content = re.sub(r'^- (.*?)$', r'<div class="line">• \1</div>', content, flags=re.MULTILINE)

        # Convert bold text
        content = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', content)

        # Wrap plain lines in divs
        lines = content.split('\n')
        processed_lines = []
        for line in lines:
            if line.strip() and not line.startswith('<'):
                processed_lines.append(f'<div class="line">{line}</div>')
            else:
                processed_lines.append(line)

        return '\n'.join(processed_lines)

class HTMLGenerator:
    def __init__(self):
        self.template = self._load_template()
        self.folder_name = None

    def _load_template(self) -> str:
        return '''<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title}</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/themes/prism-tomorrow.min.css" rel="stylesheet"/>
<style>
body {
  margin: 0;
  font-family: 'Fira Code', monospace;
  background: #0d1117;
  color: #c9d1d9;
  min-height: 100vh;
  padding: 2rem 2rem 8rem 2rem;
  overflow-y: auto;
}

.cli-window {
  max-width: 1200px;
  margin: 0 auto;
}

.line {
  margin-bottom: 0.5rem;
}

.prompt {
  color: #58a6ff;
}

pre {
  background: #161b22;
  padding: 1rem;
  border-left: 3px solid #79c0ff;
  overflow-x: auto;
  margin: 0.5rem 0;
}

.cursor {
  display: inline-block;
  width: 8px;
  background-color: #c9d1d9;
  animation: blink 1s step-start infinite;
  margin-left: 2px;
}

@keyframes blink {
  50% { opacity: 0; }
}

.suggestion {
  color: #555;
  pointer-events: none;
  user-select: none;
  white-space: pre;
  font-family: 'Fira Code', monospace;
  margin-left: 2px;
}

.input-wrapper {
  display: flex;
  position: relative;
}

.section-content {
  margin: 1rem 0;
  border-left: 2px solid #58a6ff;
  padding-left: 1rem;
}

.section-header {
  color: #58a6ff;
  font-weight: bold;
  margin-bottom: 0.5rem;
}

.file-origin {
  color: #8b949e;
  font-size: 0.9em;
  font-style: italic;
}

.error-line {
  color: #f85149;
  margin: 0.5rem 0;
}

.info-line {
  color: #58a6ff;
  margin: 0.5rem 0;
}

.match-list {
  margin: 1rem 0;
}

.match-item {
  margin: 0.3rem 0;
  color: #c9d1d9;
}

.match-number {
  color: #79c0ff;
  font-weight: bold;
}

/* Prism SQL highlighting */
.token.keyword {
  color: #ff7b72 !important;
  font-weight: bold;
}

.token.string {
  color: #a5d6ff !important;
}

.token.comment {
  color: #8b949e !important;
  font-style: italic;
}

.token.number {
  color: #79c0ff !important;
}

.token.operator {
  color: #c9d1d9 !important;
}

.token.function {
  color: #d2a8ff !important;
}

.language-sql .token.keyword {
  color: #ff7b72 !important;
}

.language-sql .token.string {
  color: #a5d6ff !important;
}
</style>
</head>
<body>

<div class="cli-window" id="terminal">
  <div class="line"><span class="prompt">{prompt}</span> {section_title}</div>
  <div class="line"><span class="prompt">{prompt}</span> Type 'help' for commands, 'ls' to list all sections</div>
  <div class="line input-wrapper">
    <span class="prompt">{prompt}&nbsp;</span>
    <span id="command-text"></span><span class="cursor"></span><span id="suggestion" class="suggestion"></span>
  </div>
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/prism.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/components/prism-sql.min.js"></script>
<script>
const searchIndex = {search_data};

// Fuzzy search functionality
class FuzzySearch {
  constructor(items) {
    this.items = items;
    this.lastResults = [];
  }

  search(query) {
    if (!query.trim()) return [];

    const results = [];
    const queryLower = query.toLowerCase();

    for (let id in this.items) {
      const item = this.items[id];
      const title = item.title.toLowerCase();
      const content = item.content.toLowerCase();

      // Exact match (highest priority)
      if (title === queryLower) {
        results.push({ id, item, score: 1000 });
        continue;
      }

      // Starts with query (very high priority)
      if (title.startsWith(queryLower)) {
        results.push({ id, item, score: 950 });
        continue;
      }

      // Contains query as whole word in title (high priority)
      if (new RegExp('\\b' + queryLower.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&') + '\\b').test(title)) {
        results.push({ id, item, score: 900 });
        continue;
      }

      // Contains query substring in title (medium-high priority)
      if (title.includes(queryLower)) {
        results.push({ id, item, score: 800 });
        continue;
      }

      // Contains all words in title (medium priority)
      const queryWords = queryLower.split(/\\s+/);
      const titleWords = title.split(/\\s+/);

      if (queryWords.every(qw => titleWords.some(tw => tw.includes(qw)))) {
        results.push({ id, item, score: 700 });
        continue;
      }

      // Contains query in content (lower priority)
      if (content.includes(queryLower)) {
        results.push({ id, item, score: 600 });
        continue;
      }

      // Fuzzy match in title (low priority)
      if (this.fuzzyMatch(title, queryLower)) {
        results.push({ id, item, score: 500 });
      }
    }

    // Sort by score (higher is better), then by title length (shorter is better)
    results.sort((a, b) => {
      if (a.score !== b.score) return b.score - a.score;
      return a.item.title.length - b.item.title.length;
    });

    this.lastResults = results;
    return results;
  }

  fuzzyMatch(text, pattern) {
    let textIndex = 0;
    let patternIndex = 0;

    while (textIndex < text.length && patternIndex < pattern.length) {
      if (text[textIndex] === pattern[patternIndex]) {
        patternIndex++;
      }
      textIndex++;
    }

    return patternIndex === pattern.length;
  }
}

// CLI Terminal functionality
class SearchCLI {
  constructor() {
    this.terminal = document.getElementById('terminal');
    this.commandText = document.getElementById('command-text');
    this.suggestionSpan = document.getElementById('suggestion');
    this.command = '';
    this.history = [];
    this.historyIndex = -1;
    this.fuzzySearch = new FuzzySearch(searchIndex);
    this.currentPage = 1;
    this.pageSize = 25;
    this.currentResults = [];

    this.attachEventListeners();
  }

  attachEventListeners() {
    // Make body focusable and focus it
    document.body.setAttribute('tabindex', '0');
    document.body.focus();

    // Prevent default behavior and ensure focus
    document.body.addEventListener('click', () => {
      document.body.focus();
    });

    document.addEventListener('keydown', (e) => this.handleKeyPress(e));

    // Prevent scrolling with arrow keys when they're used for history
    document.addEventListener('keydown', (e) => {
      if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
        e.preventDefault();
      }
    });
  }

  handleKeyPress(e) {
    switch(e.key) {
      case 'Enter':
        e.preventDefault();
        this.executeCommand();
        break;
      case 'Backspace':
        e.preventDefault();
        this.command = this.command.slice(0, -1);
        this.updateInput();
        break;
      case 'ArrowUp':
        e.preventDefault();
        this.navigateHistory(-1);
        break;
      case 'ArrowDown':
        e.preventDefault();
        this.navigateHistory(1);
        break;
      case 'Tab':
        e.preventDefault();
        this.autoComplete();
        break;
      default:
        if (e.key.length === 1) {
          this.command += e.key;
          this.updateInput();
        }
        break;
    }
  }

  executeCommand() {
    if (this.command.trim() === '') return;

    this.history.push(this.command);
    this.historyIndex = this.history.length;

    const cmdLine = document.createElement('div');
    cmdLine.className = 'line';
    cmdLine.innerHTML = `<span class="prompt">{prompt}</span> ${this.command}`;
    this.terminal.insertBefore(cmdLine, this.terminal.querySelector('.input-wrapper'));

    this.processCommand(this.command.trim());

    this.command = '';
    this.updateInput();
    this.scrollToBottom();
  }

  processCommand(cmd) {
    if (cmd === 'help') {
      this.showHelp();
    } else if (cmd === 'ls') {
      this.listAll();
    } else if (cmd === 'ls -l') {
      this.listDetailed();
    } else if (cmd.startsWith('grep ')) {
      this.grepContent(cmd.substring(5));
    } else if (cmd === 'clear') {
      this.clearTerminal();
    } else if (cmd === 'history') {
      this.showHistory();
    } else if (cmd === 'more' || cmd === 'm') {
      this.showMoreResults();
    } else if (cmd.startsWith('page ')) {
      this.showPage(parseInt(cmd.substring(5)));
    } else if (cmd === 'cd ..') {
      window.location.href = '../index.html';
    } else {
      this.searchContent(cmd);
    }
  }

  searchContent(query) {
    const results = this.fuzzySearch.search(query);
    this.currentResults = results;
    this.currentPage = 1;

    if (results.length === 0) {
      this.showError(`No matches found for: ${query}`);
    } else if (results.length === 1) {
      this.showSection(results[0].id, results[0].item);
    } else {
      this.showMultipleResults(results, 1);
    }
  }

  showSection(id, item) {
    const content = document.createElement('div');
    content.className = 'section-content';
    content.innerHTML = `
      <div class="section-header">${item.title}</div>
      <div class="file-origin">From: ${item.file} (${item.type})</div>
      ${item.content}
    `;

    this.terminal.insertBefore(content, this.terminal.querySelector('.input-wrapper'));

    if (typeof Prism !== 'undefined') {
      Prism.highlightAllUnder(content);
    }
  }

  showMultipleResults(results, page = 1) {
    const totalPages = Math.ceil(results.length / this.pageSize);
    const startIndex = (page - 1) * this.pageSize;
    const endIndex = Math.min(startIndex + this.pageSize, results.length);
    const pageResults = results.slice(startIndex, endIndex);

    let content = `<div class="info-line">Found ${results.length} matches (showing ${startIndex + 1}-${endIndex}, page ${page}/${totalPages}):</div>`;
    content += '<div class="match-list">';

    pageResults.forEach((result, index) => {
      content += `<div class="match-item">• ${result.item.title} <span class="file-origin">(${result.item.file})</span></div>`;
    });

    content += '</div>';

    if (totalPages > 1) {
      content += '<div class="line">Commands: ';
      if (page > 1) content += '<strong>page ' + (page - 1) + '</strong> (previous), ';
      if (page < totalPages) content += '<strong>page ' + (page + 1) + '</strong> (next), ';
      content += '<strong>more</strong> (show more results)</div>';
    }

    content += '<div class="line">Refine your search for better results</div>';

    const div = document.createElement('div');
    div.innerHTML = content;
    this.terminal.insertBefore(div, this.terminal.querySelector('.input-wrapper'));
  }


  listAll() {
    let content = '<div class="info-line">All available sections:</div>';
    content += '<div class="match-list">';

    for (let id in searchIndex) {
      const item = searchIndex[id];
      content += `<div class="match-item">• ${item.title} <span class="file-origin">(${item.file})</span></div>`;
    }

    content += '</div>';
    content += '<div class="line">Type a section name to view</div>';

    const div = document.createElement('div');
    div.innerHTML = content;
    this.terminal.insertBefore(div, this.terminal.querySelector('.input-wrapper'));
  }

  listDetailed() {
    let content = '<div class="info-line">Detailed section listing:</div>';
    content += '<div class="match-list">';

    for (let id in searchIndex) {
      const item = searchIndex[id];
      const typeIcon = item.type === 'sql_function' ? '⚡' : item.type === 'sql_section' ? '🔧' : '📝';
      content += `<div class="match-item">${typeIcon} ${item.title} <span class="file-origin">(${item.file} - ${item.type})</span></div>`;
    }

    content += '</div>';

    const div = document.createElement('div');
    div.innerHTML = content;
    this.terminal.insertBefore(div, this.terminal.querySelector('.input-wrapper'));
  }

  grepContent(query) {
    const results = [];
    for (let id in searchIndex) {
      const item = searchIndex[id];
      if (item.content.toLowerCase().includes(query.toLowerCase())) {
        results.push({ id, item });
      }
    }

    if (results.length === 0) {
      this.showError(`No content matches: ${query}`);
    } else {
      let content = `<div class="info-line">Content matches for "${query}" (${results.length} found):</div>`;
      content += '<div class="match-list">';

      results.forEach((result, index) => {
        content += `<div class="match-item">• ${result.item.title} <span class="file-origin">(${result.item.file})</span></div>`;
      });

      content += '</div>';

      const div = document.createElement('div');
      div.innerHTML = content;
      this.terminal.insertBefore(div, this.terminal.querySelector('.input-wrapper'));

      // Store for pagination
      this.currentResults = results.map(r => ({ id: r.id, item: r.item, score: 100 }));
      this.currentPage = 1;
    }
  }

  showMoreResults() {
    if (this.currentResults.length === 0) {
      this.showError('No current search results to show more of');
      return;
    }

    const nextPage = this.currentPage + 1;
    const totalPages = Math.ceil(this.currentResults.length / this.pageSize);

    if (nextPage <= totalPages) {
      this.currentPage = nextPage;
      this.showMultipleResults(this.currentResults, nextPage);
    } else {
      this.showError('Already showing all results');
    }
  }

  showPage(pageNum) {
    if (this.currentResults.length === 0) {
      this.showError('No current search results to paginate');
      return;
    }

    const totalPages = Math.ceil(this.currentResults.length / this.pageSize);

    if (pageNum < 1 || pageNum > totalPages) {
      this.showError(`Invalid page number. Use 1-${totalPages}`);
      return;
    }

    this.currentPage = pageNum;
    this.showMultipleResults(this.currentResults, pageNum);
  }

  showHelp() {
    const helpContent = `
      <div class="info-line">Available commands:</div>
      <div class="line">• <strong>search</strong> - Type any text to search sections</div>
      <div class="line">• <strong>ls</strong> - List all sections</div>
      <div class="line">• <strong>ls -l</strong> - Detailed listing with file types</div>
      <div class="line">• <strong>grep xyz</strong> - Search within section content</div>
      <div class="line">• <strong>more</strong> or <strong>m</strong> - Show next page of results</div>
      <div class="line">• <strong>page N</strong> - Jump to specific page number</div>
      <div class="line">• <strong>cd ..</strong> - Go back to main knowledge base</div>
      <div class="line">• <strong>clear</strong> - Clear terminal</div>
      <div class="line">• <strong>history</strong> - Show command history</div>
      <div class="line">• <strong>help</strong> - Show this help</div>
      <div class="line"></div>
      <div class="line">Tips: Use fuzzy search, Tab for suggestions, arrows for history</div>
    `;

    const div = document.createElement('div');
    div.innerHTML = helpContent;
    this.terminal.insertBefore(div, this.terminal.querySelector('.input-wrapper'));
  }

  showHistory() {
    const historyContent = this.history.map((cmd, index) =>
      `<div class="line">${index + 1}: ${cmd}</div>`
    ).join('');

    const div = document.createElement('div');
    div.innerHTML = `<div class="info-line">Command history:</div>${historyContent}`;
    this.terminal.insertBefore(div, this.terminal.querySelector('.input-wrapper'));
  }

  showError(message) {
    const div = document.createElement('div');
    div.className = 'error-line';
    div.textContent = message;
    this.terminal.insertBefore(div, this.terminal.querySelector('.input-wrapper'));
  }

  clearTerminal() {
    const lines = this.terminal.querySelectorAll('.line:not(.input-wrapper), .section-content, .error-line, .info-line, .match-list');
    lines.forEach(line => line.remove());
  }

  navigateHistory(direction) {
    if (direction === -1 && this.historyIndex > 0) {
      this.historyIndex--;
      this.command = this.history[this.historyIndex];
    } else if (direction === 1 && this.historyIndex < this.history.length - 1) {
      this.historyIndex++;
      this.command = this.history[this.historyIndex];
    } else if (direction === 1 && this.historyIndex === this.history.length - 1) {
      this.historyIndex = this.history.length;
      this.command = '';
    }
    this.updateInput();
  }

  autoComplete() {
    // Simple autocomplete based on section titles
    const matches = [];
    for (let id in searchIndex) {
      const title = searchIndex[id].title.toLowerCase();
      if (title.startsWith(this.command.toLowerCase())) {
        matches.push(searchIndex[id].title);
      }
    }

    if (matches.length === 1) {
      this.command = matches[0];
      this.updateInput();
    }
  }

  updateInput() {
    this.commandText.textContent = this.command;

    // Show suggestion
    const matches = [];
    for (let id in searchIndex) {
      const title = searchIndex[id].title.toLowerCase();
      if (title.startsWith(this.command.toLowerCase()) && title !== this.command.toLowerCase()) {
        matches.push(searchIndex[id].title);
      }
    }

    this.suggestionSpan.textContent = matches.length > 0 ? matches[0].slice(this.command.length) : '';
  }

  scrollToBottom() {
    // Scroll to ensure the input line is visible with some padding
    setTimeout(() => {
      const inputWrapper = this.terminal.querySelector('.input-wrapper');
      if (inputWrapper) {
        inputWrapper.scrollIntoView({ behavior: 'smooth', block: 'center' });
      }
    }, 50);
  }
}

// Initialize CLI when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
  const cli = new SearchCLI();

  // Ensure initial focus
  setTimeout(() => {
    document.body.focus();
  }, 100);
});
</script>

</body>
</html>'''

    def generate_html(self, search_data: Dict, output_path: str):
        """Generate the final HTML file"""
        search_json = json.dumps(search_data, indent=2)

        # Set context based on folder
        if self.folder_name:
            title = f"{self.folder_name.title()} Knowledge Base Search"
            prompt = f"user@kb_{self.folder_name}:~$"
            section_title = f"{self.folder_name.title()} Knowledge Base Search"
        else:
            title = "Knowledge Base Search"
            prompt = "user@kb:~$"
            section_title = "Knowledge Base Search"

        html_content = self.template.replace('{search_data}', search_json)
        html_content = html_content.replace('{title}', title)
        html_content = html_content.replace('{prompt}', prompt)
        html_content = html_content.replace('{section_title}', section_title)

        with open(output_path, 'w', encoding='utf-8') as f:
            f.write(html_content)

class NavigationGenerator:
    def __init__(self):
        self.template = self._load_template()

    def scan_subfolders(self, kb_path: str) -> list:
        """Scan kb directory for subdirectories"""
        subfolders = []
        for item in os.listdir(kb_path):
            item_path = os.path.join(kb_path, item)
            if os.path.isdir(item_path) and not item.startswith('.'):
                subfolders.append(item)
        return sorted(subfolders)

    def _load_template(self) -> str:
        return '''<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Knowledge Base Navigation</title>
<style>
body {
  margin: 0;
  font-family: 'Fira Code', monospace;
  background: #0d1117;
  color: #c9d1d9;
  min-height: 100vh;
  padding: 2rem 2rem 8rem 2rem;
  overflow-y: auto;
}

.cli-window {
  max-width: 1200px;
  margin: 0 auto;
}

.line {
  margin-bottom: 0.5rem;
}

.prompt {
  color: #58a6ff;
}

.cursor {
  display: inline-block;
  width: 8px;
  background-color: #c9d1d9;
  animation: blink 1s step-start infinite;
  margin-left: 2px;
}

@keyframes blink {
  50% { opacity: 0; }
}

.input-wrapper {
  display: flex;
  position: relative;
}

.info-line {
  color: #58a6ff;
  margin: 0.5rem 0;
}

.error-line {
  color: #f85149;
  margin: 0.5rem 0;
}

.folder-item {
  margin: 0.3rem 0;
  color: #c9d1d9;
}

.folder-icon {
  color: #79c0ff;
  margin-right: 0.5rem;
}
</style>
</head>
<body>

<div class="cli-window" id="terminal">
  <div class="line"><span class="prompt">user@kb:~$</span> Knowledge Base Navigation</div>
  <div class="line"><span class="prompt">user@kb:~$</span> Type 'help' for commands, 'ls' to list folders</div>
  <div class="line input-wrapper">
    <span class="prompt">user@kb:~$&nbsp;</span>
    <span id="command-text"></span><span class="cursor"></span>
  </div>
</div>

<script>
const folders = {folder_data};

class NavigationCLI {
  constructor() {
    this.terminal = document.getElementById('terminal');
    this.commandText = document.getElementById('command-text');
    this.command = '';
    this.history = [];
    this.historyIndex = -1;

    this.attachEventListeners();
  }

  attachEventListeners() {
    document.body.setAttribute('tabindex', '0');
    document.body.focus();

    document.body.addEventListener('click', () => {
      document.body.focus();
    });

    document.addEventListener('keydown', (e) => this.handleKeyPress(e));

    document.addEventListener('keydown', (e) => {
      if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
        e.preventDefault();
      }
    });
  }

  handleKeyPress(e) {
    switch(e.key) {
      case 'Enter':
        e.preventDefault();
        this.executeCommand();
        break;
      case 'Backspace':
        e.preventDefault();
        this.command = this.command.slice(0, -1);
        this.updateInput();
        break;
      case 'ArrowUp':
        e.preventDefault();
        this.navigateHistory(-1);
        break;
      case 'ArrowDown':
        e.preventDefault();
        this.navigateHistory(1);
        break;
      default:
        if (e.key.length === 1) {
          this.command += e.key;
          this.updateInput();
        }
        break;
    }
  }

  executeCommand() {
    if (this.command.trim() === '') return;

    this.history.push(this.command);
    this.historyIndex = this.history.length;

    const cmdLine = document.createElement('div');
    cmdLine.className = 'line';
    cmdLine.innerHTML = `<span class="prompt">user@kb:~$</span> ${this.command}`;
    this.terminal.insertBefore(cmdLine, this.terminal.querySelector('.input-wrapper'));

    this.processCommand(this.command.trim());

    this.command = '';
    this.updateInput();
    this.scrollToBottom();
  }

  processCommand(cmd) {
    if (cmd === 'help') {
      this.showHelp();
    } else if (cmd === 'ls') {
      this.listFolders();
    } else if (cmd.startsWith('cd ')) {
      this.changeDirectory(cmd.substring(3));
    } else if (cmd === 'clear') {
      this.clearTerminal();
    } else if (cmd === 'history') {
      this.showHistory();
    } else {
      this.showError(`Command not found: ${cmd}. Type 'help' for available commands.`);
    }
  }

  listFolders() {
    let content = '<div class="info-line">Available knowledge base folders:</div>';

    folders.forEach(folder => {
      content += `<div class="folder-item"><span class="folder-icon">📁</span>${folder}</div>`;
    });

    content += '<div class="line">Use "cd foldername" to navigate to a folder</div>';

    const div = document.createElement('div');
    div.innerHTML = content;
    this.terminal.insertBefore(div, this.terminal.querySelector('.input-wrapper'));
  }

  changeDirectory(folderName) {
    if (folders.includes(folderName)) {
      window.location.href = `${folderName}/index.html`;
    } else {
      this.showError(`Folder not found: ${folderName}. Use 'ls' to see available folders.`);
    }
  }

  showHelp() {
    const helpContent = `
      <div class="info-line">Available commands:</div>
      <div class="line">• <strong>ls</strong> - List all knowledge base folders</div>
      <div class="line">• <strong>cd foldername</strong> - Navigate to a specific folder</div>
      <div class="line">• <strong>clear</strong> - Clear terminal</div>
      <div class="line">• <strong>history</strong> - Show command history</div>
      <div class="line">• <strong>help</strong> - Show this help</div>
      <div class="line"></div>
      <div class="line">Example: cd oracle (goes to oracle knowledge base)</div>
    `;

    const div = document.createElement('div');
    div.innerHTML = helpContent;
    this.terminal.insertBefore(div, this.terminal.querySelector('.input-wrapper'));
  }

  showHistory() {
    const historyContent = this.history.map((cmd, index) =>
      `<div class="line">${index + 1}: ${cmd}</div>`
    ).join('');

    const div = document.createElement('div');
    div.innerHTML = `<div class="info-line">Command history:</div>${historyContent}`;
    this.terminal.insertBefore(div, this.terminal.querySelector('.input-wrapper'));
  }

  showError(message) {
    const div = document.createElement('div');
    div.className = 'error-line';
    div.textContent = message;
    this.terminal.insertBefore(div, this.terminal.querySelector('.input-wrapper'));
  }

  clearTerminal() {
    const lines = this.terminal.querySelectorAll('.line:not(.input-wrapper), .info-line, .error-line');
    lines.forEach(line => line.remove());
  }

  navigateHistory(direction) {
    if (direction === -1 && this.historyIndex > 0) {
      this.historyIndex--;
      this.command = this.history[this.historyIndex];
    } else if (direction === 1 && this.historyIndex < this.history.length - 1) {
      this.historyIndex++;
      this.command = this.history[this.historyIndex];
    } else if (direction === 1 && this.historyIndex === this.history.length - 1) {
      this.historyIndex = this.history.length;
      this.command = '';
    }
    this.updateInput();
  }

  updateInput() {
    this.commandText.textContent = this.command;
  }

  scrollToBottom() {
    setTimeout(() => {
      const inputWrapper = this.terminal.querySelector('.input-wrapper');
      if (inputWrapper) {
        inputWrapper.scrollIntoView({ behavior: 'smooth', block: 'center' });
      }
    }, 50);
  }
}

document.addEventListener('DOMContentLoaded', function() {
  const cli = new NavigationCLI();

  setTimeout(() => {
    document.body.focus();
  }, 100);
});
</script>

</body>
</html>'''

    def generate_html(self, folders: list, output_path: str):
        """Generate the main navigation HTML file"""
        folder_json = json.dumps(folders, indent=2)
        html_content = self.template.replace('{folder_data}', folder_json)

        with open(output_path, 'w', encoding='utf-8') as f:
            f.write(html_content)

def is_main_kb_folder(path: str) -> bool:
    """Check if current directory is the main kb folder by looking for multiple subfolders"""
    subfolders = [item for item in os.listdir(path)
                  if os.path.isdir(os.path.join(path, item)) and not item.startswith('.')]
    return len(subfolders) > 1

def main():
    """Main function"""
    current_dir = os.getcwd()

    if is_main_kb_folder(current_dir):
        # Generate navigation index for main kb folder
        nav_generator = NavigationGenerator()
        folders = nav_generator.scan_subfolders(current_dir)
        output_path = os.path.join(current_dir, 'index.html')
        nav_generator.generate_html(folders, output_path)
        print(f"Generated main navigation index: {output_path}")
        print(f"Found {len(folders)} folders: {', '.join(folders)}")
    else:
        # Generate search index for subfolder
        parser = ContentParser()
        search_data = parser.process_directory(current_dir)

        # Get folder name for context
        folder_name = os.path.basename(current_dir)

        # Generate HTML with back navigation
        generator = HTMLGenerator()
        generator.folder_name = folder_name  # Add folder context
        output_path = os.path.join(current_dir, 'index.html')
        generator.generate_html(search_data, output_path)

        print(f"Generated searchable index: {output_path}")
        print(f"Found {len(search_data)} searchable sections")

if __name__ == "__main__":
    main()