diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index ab818ef5..00000000 --- a/Dockerfile +++ /dev/null @@ -1,107 +0,0 @@ -# syntax=docker/dockerfile:1-labs - -# Build argument for custom certificates directory -ARG CUSTOM_CERT_DIR="certs" - -FROM node:20-alpine3.22 AS node_base - -FROM node_base AS node_deps -WORKDIR /app -COPY package.json package-lock.json ./ -RUN npm ci --legacy-peer-deps - -FROM node_base AS node_builder -WORKDIR /app -COPY --from=node_deps /app/node_modules ./node_modules -# Copy only necessary files for Next.js build -COPY package.json package-lock.json next.config.ts tsconfig.json tailwind.config.js postcss.config.mjs ./ -COPY src/ ./src/ -COPY public/ ./public/ -# Increase Node.js memory limit for build and disable telemetry -ENV NODE_OPTIONS="--max-old-space-size=4096" -ENV NEXT_TELEMETRY_DISABLED=1 -RUN NODE_ENV=production npm run build - -FROM python:3.11-slim AS py_deps -WORKDIR /app -RUN python -m venv /opt/venv -ENV PATH="/opt/venv/bin:$PATH" -COPY api/requirements.txt ./api/ -RUN pip install --no-cache -r api/requirements.txt - -# Use Python 3.11 as final image -FROM python:3.11-slim - -# Set working directory -WORKDIR /app - -# Install Node.js and npm -RUN apt-get update && apt-get install -y \ - curl \ - gnupg \ - git \ - ca-certificates \ - && mkdir -p /etc/apt/keyrings \ - && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \ - && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list \ - && apt-get update \ - && apt-get install -y nodejs \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -# Update certificates if custom ones were provided and copied successfully -RUN if [ -n "${CUSTOM_CERT_DIR}" ]; then \ - mkdir -p /usr/local/share/ca-certificates && \ - if [ -d "${CUSTOM_CERT_DIR}" ]; then \ - cp -r ${CUSTOM_CERT_DIR}/* /usr/local/share/ca-certificates/ 2>/dev/null || true; \ - update-ca-certificates; \ - echo "Custom certificates installed successfully."; \ - else \ - echo "Warning: ${CUSTOM_CERT_DIR} not found. Skipping certificate installation."; \ - fi \ - fi - -ENV PATH="/opt/venv/bin:$PATH" - -# Copy Python dependencies -COPY --from=py_deps /opt/venv /opt/venv -COPY api/ ./api/ - -# Copy Node app -COPY --from=node_builder /app/public ./public -COPY --from=node_builder /app/.next/standalone ./ -COPY --from=node_builder /app/.next/static ./.next/static - -# Expose the port the app runs on -EXPOSE ${PORT:-8001} 3000 - -# Create a script to run both backend and frontend -RUN echo '#!/bin/bash\n\ -# Load environment variables from .env file if it exists\n\ -if [ -f .env ]; then\n\ - export $(grep -v "^#" .env | xargs -r)\n\ -fi\n\ -\n\ -# Check for required environment variables\n\ -if [ -z "$OPENAI_API_KEY" ] || [ -z "$GOOGLE_API_KEY" ]; then\n\ - echo "Warning: OPENAI_API_KEY and/or GOOGLE_API_KEY environment variables are not set."\n\ - echo "These are required for DeepWiki to function properly."\n\ - echo "You can provide them via a mounted .env file or as environment variables when running the container."\n\ -fi\n\ -\n\ -# Start the API server in the background with the configured port\n\ -python -m api.main --port ${PORT:-8001} &\n\ -PORT=3000 HOSTNAME=0.0.0.0 node server.js &\n\ -wait -n\n\ -exit $?' > /app/start.sh && chmod +x /app/start.sh - -# Set environment variables -ENV PORT=8001 -ENV NODE_ENV=production -ENV SERVER_BASE_URL=http://localhost:${PORT:-8001} - -# Create empty .env file (will be overridden if one exists at runtime) -RUN touch .env - -# Command to run the application -CMD ["/app/start.sh"] diff --git a/README.md b/README.md index 538fa647..e3b61a77 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ [![Twitter/X](https://img.shields.io/badge/Twitter-1DA1F2?style=for-the-badge&logo=twitter&logoColor=white)](https://x.com/sashimikun_void) [![Discord](https://img.shields.io/badge/Discord-7289DA?style=for-the-badge&logo=discord&logoColor=white)](https://discord.com/invite/VQMBGR8u5v) -[English](./README.md) | [简体中文](./README.zh.md) | [繁體中文](./README.zh-tw.md) | [日本語](./README.ja.md) | [Español](./README.es.md) | [한국어](./README.kr.md) | [Tiếng Việt](./README.vi.md) | [Português Brasileiro](./README.pt-br.md) | [Français](./README.fr.md) | [Русский](./README.ru.md) +[English](./docs/README.md) | [简体中文](./docs/README.zh.md) | [繁體中文](./docs/README.zh-tw.md) | [日本語](./docs/README.ja.md) | [Español](./docs/README.es.md) | [한국어](./docs/README.kr.md) | [Tiếng Việt](./docs/README.vi.md) | [Português Brasileiro](./docs/README.pt-br.md) | [Français](./docs/README.fr.md) | [Русский](./docs/README.ru.md) ## ✨ Features diff --git a/.python-version b/backend/.python-version similarity index 100% rename from .python-version rename to backend/.python-version diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 00000000..7c5743f6 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,18 @@ +# backend/Dockerfile +FROM python:3.11-slim +WORKDIR /app + +# Install Python dependencies +COPY backend/api/requirements.txt ./api/ +RUN python -m venv /opt/venv && \ + /opt/venv/bin/pip install --no-cache -r api/requirements.txt + +# Copy source +COPY backend/api/ ./api/ + +ENV PATH="/opt/venv/bin:$PATH" +ENV PORT=8001 + +# A health-check endpoint should be implemented in FastAPI +EXPOSE 8001 +CMD ["sh", "-c", "uvicorn api.api:app --host 0.0.0.0 --port ${PORT}"] \ No newline at end of file diff --git a/api/README.md b/backend/api/README.md similarity index 100% rename from api/README.md rename to backend/api/README.md diff --git a/api/__init__.py b/backend/api/__init__.py similarity index 100% rename from api/__init__.py rename to backend/api/__init__.py diff --git a/api/api.py b/backend/api/api.py similarity index 100% rename from api/api.py rename to backend/api/api.py diff --git a/api/azureai_client.py b/backend/api/azureai_client.py similarity index 100% rename from api/azureai_client.py rename to backend/api/azureai_client.py diff --git a/api/bedrock_client.py b/backend/api/bedrock_client.py similarity index 100% rename from api/bedrock_client.py rename to backend/api/bedrock_client.py diff --git a/api/config.py b/backend/api/config.py similarity index 100% rename from api/config.py rename to backend/api/config.py diff --git a/api/config/embedder.json b/backend/api/config/embedder.json similarity index 100% rename from api/config/embedder.json rename to backend/api/config/embedder.json diff --git a/api/config/embedder.json.bak b/backend/api/config/embedder.json.bak similarity index 100% rename from api/config/embedder.json.bak rename to backend/api/config/embedder.json.bak diff --git a/api/config/embedder.ollama.json.bak b/backend/api/config/embedder.ollama.json.bak similarity index 100% rename from api/config/embedder.ollama.json.bak rename to backend/api/config/embedder.ollama.json.bak diff --git a/api/config/embedder.openai_compatible.json.bak b/backend/api/config/embedder.openai_compatible.json.bak similarity index 100% rename from api/config/embedder.openai_compatible.json.bak rename to backend/api/config/embedder.openai_compatible.json.bak diff --git a/api/config/generator.json b/backend/api/config/generator.json similarity index 100% rename from api/config/generator.json rename to backend/api/config/generator.json diff --git a/api/config/lang.json b/backend/api/config/lang.json similarity index 100% rename from api/config/lang.json rename to backend/api/config/lang.json diff --git a/api/config/repo.json b/backend/api/config/repo.json similarity index 100% rename from api/config/repo.json rename to backend/api/config/repo.json diff --git a/api/dashscope_client.py b/backend/api/dashscope_client.py similarity index 100% rename from api/dashscope_client.py rename to backend/api/dashscope_client.py diff --git a/api/data_pipeline.py b/backend/api/data_pipeline.py similarity index 100% rename from api/data_pipeline.py rename to backend/api/data_pipeline.py diff --git a/api/logging_config.py b/backend/api/logging_config.py similarity index 100% rename from api/logging_config.py rename to backend/api/logging_config.py diff --git a/api/main.py b/backend/api/main.py similarity index 100% rename from api/main.py rename to backend/api/main.py diff --git a/api/ollama_patch.py b/backend/api/ollama_patch.py similarity index 100% rename from api/ollama_patch.py rename to backend/api/ollama_patch.py diff --git a/api/openai_client.py b/backend/api/openai_client.py similarity index 100% rename from api/openai_client.py rename to backend/api/openai_client.py diff --git a/api/openrouter_client.py b/backend/api/openrouter_client.py similarity index 100% rename from api/openrouter_client.py rename to backend/api/openrouter_client.py diff --git a/api/prompts.py b/backend/api/prompts.py similarity index 100% rename from api/prompts.py rename to backend/api/prompts.py diff --git a/api/rag.py b/backend/api/rag.py similarity index 100% rename from api/rag.py rename to backend/api/rag.py diff --git a/api/requirements.txt b/backend/api/requirements.txt similarity index 100% rename from api/requirements.txt rename to backend/api/requirements.txt diff --git a/api/simple_chat.py b/backend/api/simple_chat.py similarity index 100% rename from api/simple_chat.py rename to backend/api/simple_chat.py diff --git a/api/test_api.py b/backend/api/test_api.py similarity index 100% rename from api/test_api.py rename to backend/api/test_api.py diff --git a/api/tools/embedder.py b/backend/api/tools/embedder.py similarity index 100% rename from api/tools/embedder.py rename to backend/api/tools/embedder.py diff --git a/api/websocket_wiki.py b/backend/api/websocket_wiki.py similarity index 100% rename from api/websocket_wiki.py rename to backend/api/websocket_wiki.py diff --git a/pyproject.toml b/backend/pyproject.toml similarity index 100% rename from pyproject.toml rename to backend/pyproject.toml diff --git a/pytest.ini b/backend/pytest.ini similarity index 100% rename from pytest.ini rename to backend/pytest.ini diff --git a/run.sh b/backend/run.sh similarity index 100% rename from run.sh rename to backend/run.sh diff --git a/test/__init__.py b/backend/test/__init__.py similarity index 100% rename from test/__init__.py rename to backend/test/__init__.py diff --git a/test/test_extract_repo_name.py b/backend/test/test_extract_repo_name.py similarity index 100% rename from test/test_extract_repo_name.py rename to backend/test/test_extract_repo_name.py diff --git a/uv.lock b/backend/uv.lock similarity index 100% rename from uv.lock rename to backend/uv.lock diff --git a/docker-compose.yml b/docker-compose.yml index 08953808..7395e779 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,31 +1,37 @@ version: '3.8' services: - deepwiki: + backend: build: context: . - dockerfile: Dockerfile + dockerfile: backend/Dockerfile + container_name: deepwiki-backend ports: - - "${PORT:-8001}:${PORT:-8001}" # API port - - "3000:3000" # Next.js port + - "8001:8001" env_file: - .env - environment: - - PORT=${PORT:-8001} - - NODE_ENV=production - - SERVER_BASE_URL=http://localhost:${PORT:-8001} - - LOG_LEVEL=${LOG_LEVEL:-INFO} - - LOG_FILE_PATH=${LOG_FILE_PATH:-api/logs/application.log} volumes: - ~/.adalflow:/root/.adalflow # Persist repository and embedding data - - ./api/logs:/app/api/logs # Persist log files across container restarts - # Resource limits for docker-compose up (not Swarm mode) - mem_limit: 6g - mem_reservation: 2g - # Health check configuration + - ./backend/api/logs:/app/api/logs # Persist log files across container restarts healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:${PORT:-8001}/health"] - interval: 60s - timeout: 10s + test: ["CMD-SHELL", "curl -f http://localhost:8001/health || exit 1"] + interval: 30s + timeout: 5s retries: 3 - start_period: 30s + + frontend: + build: + context: . + dockerfile: frontend/Dockerfile + container_name: deepwiki-frontend + ports: + - "3000:3000" + environment: + - NODE_ENV=production + depends_on: + backend: + condition: service_healthy + +networks: + default: + driver: bridge diff --git a/README.es.md b/docs/README.es.md similarity index 100% rename from README.es.md rename to docs/README.es.md diff --git a/README.fr.md b/docs/README.fr.md similarity index 100% rename from README.fr.md rename to docs/README.fr.md diff --git a/README.ja.md b/docs/README.ja.md similarity index 100% rename from README.ja.md rename to docs/README.ja.md diff --git a/README.kr.md b/docs/README.kr.md similarity index 100% rename from README.kr.md rename to docs/README.kr.md diff --git a/README.pt-br.md b/docs/README.pt-br.md similarity index 100% rename from README.pt-br.md rename to docs/README.pt-br.md diff --git a/README.ru.md b/docs/README.ru.md similarity index 100% rename from README.ru.md rename to docs/README.ru.md diff --git a/README.vi.md b/docs/README.vi.md similarity index 100% rename from README.vi.md rename to docs/README.vi.md diff --git a/README.zh-tw.md b/docs/README.zh-tw.md similarity index 100% rename from README.zh-tw.md rename to docs/README.zh-tw.md diff --git a/README.zh.md b/docs/README.zh.md similarity index 100% rename from README.zh.md rename to docs/README.zh.md diff --git a/docs/repomix-output.md b/docs/repomix-output.md new file mode 100644 index 00000000..94cc00ea --- /dev/null +++ b/docs/repomix-output.md @@ -0,0 +1,7077 @@ +This file is a merged representation of the entire codebase, combining all repository files into a single document. +Generated by Repomix on: 2025-08-08 10:14:33 + +# File Summary + +## Purpose: + +This file contains a packed representation of the entire repository's contents. +It is designed to be easily consumable by AI systems for analysis, code review, +or other automated processes. + +## File Format: + +The content is organized as follows: +1. This summary section +2. Repository information +3. Repository structure +4. Multiple file entries, each consisting of: + a. A header with the file path (## File: path/to/file) + b. The full contents of the file in a code block + +## Usage Guidelines: + +- This file should be treated as read-only. Any changes should be made to the + original repository files, not this packed version. +- When processing this file, use the file path to distinguish + between different files in the repository. +- Be aware that this file may contain sensitive information. Handle it with + the same level of security as you would the original repository. + +## Notes: + +- Some files may have been excluded based on .gitignore rules and Repomix's + configuration. +- Binary files are not included in this packed representation. Please refer to + the Repository Structure section for a complete list of file paths, including + binary files. + +## Additional Information: + +For more information about Repomix, visit: https://github.com/andersonby/python-repomix + + +# Repository Structure + +``` +src + contexts + LanguageContext.tsx + components + WikiTypeSelector.tsx + WikiTreeView.tsx + ProcessedProjects.tsx + Markdown.tsx + Mermaid.tsx + Ask.tsx + UserSelector.tsx + theme-toggle.tsx + hooks + useProcessedProjects.ts + app + wiki + projects + page.tsx + globals.css + layout.tsx + api + wiki + projects + route.ts + chat + stream + route.ts + models + config + route.ts + auth + status + route.ts + validate + route.ts + i18n.ts + types + wiki + wikistructure.tsx + wikipage.tsx + repoinfo.tsx + utils + urlDecoder.tsx + getRepoUrl.tsx + websocketClient.ts +eslint.config.mjs +LICENSE +package.json +tsconfig.json +test + test_extract_repo_name.py + __init__.py +pytest.ini +postcss.config.mjs +.github + workflows + docker-build-push.yml +Ollama-instruction.md +next.config.ts +pyproject.toml +docker-compose.yml +run.sh +.python-version +tailwind.config.js +.gitignore +.dockerignore +api + test_api.py + ollama_patch.py + api.py + rag.py + tools + embedder.py + config + generator.json + embedder.json + repo.json + lang.json + README.md + prompts.py + __init__.py + requirements.txt + logging_config.py +``` + +# Repository Files + + +## src/contexts/LanguageContext.tsx + +```text +/* eslint-disable @typescript-eslint/no-explicit-any */ +'use client'; + +import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'; +import { locales } from '@/i18n'; + +type Messages = Record; +type LanguageContextType = { + language: string; + setLanguage: (lang: string) => void; + messages: Messages; + supportedLanguages: Record; +}; + +const LanguageContext = createContext(undefined); + +export function LanguageProvider({ children }: { children: ReactNode }) { + // Initialize with 'en' or get from localStorage if available + const [language, setLanguageState] = useState('en'); + const [messages, setMessages] = useState({}); + const [isLoading, setIsLoading] = useState(true); + const [supportedLanguages, setSupportedLanguages] = useState({}) + const [defaultLanguage, setDefaultLanguage] = useState('en') + + // Helper function to detect browser language + const detectBrowserLanguage = (): string => { + try { + if (typeof window === 'undefined' || typeof navigator === 'undefined') { + return 'en'; // Default to English on server-side + } + + // Get browser language (navigator.language returns full locale like 'en-US') + const browserLang = navigator.language || (navigator as any).userLanguage || ''; + console.log('Detected browser language:', browserLang); + + if (!browserLang) { + return 'en'; // Default to English if browser language is not available + } + + // Extract the language code (first 2 characters) + const langCode = browserLang.split('-')[0].toLowerCase(); + console.log('Extracted language code:', langCode); + + // Check if the detected language is supported + if (locales.includes(langCode as any)) { + console.log('Language supported, using:', langCode); + return langCode; + } + + // Special case for Chinese variants + if (langCode === 'zh') { + console.log('Chinese language detected'); + // Check for traditional Chinese variants + if (browserLang.includes('TW') || browserLang.includes('HK')) { + console.log('Traditional Chinese variant detected'); + return 'zh'; // Use Mandarin for traditional Chinese + } + return 'zh'; // Use Mandarin for simplified Chinese + } + + console.log('Language not supported, defaulting to English'); + return 'en'; // Default to English if not supported + } catch (error) { + console.error('Error detecting browser language:', error); + return 'en'; // Default to English on error + } + }; + + useEffect(() => { + const getSupportedLanguages = async () => { + try { + const response = await fetch('/api/lang/config'); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + setSupportedLanguages(data.supported_languages); + setDefaultLanguage(data.default); + } catch (err) { + console.error("Failed to fetch auth status:", err); + // Assuming auth is required if fetch fails to avoid blocking UI for safety + const defaultSupportedLanguages = { + "en": "English", + "ja": "Japanese (日本語)", + "zh": "Mandarin Chinese (中文)", + "zh-tw": "Traditional Chinese (繁體中文)", + "es": "Spanish (Español)", + "kr": "Korean (한국어)", + "vi": "Vietnamese (Tiếng Việt)", + "pt-br": "Brazilian Portuguese (Português Brasileiro)", + "fr": "Français (French)", + "ru": "Русский (Russian)" + }; + setSupportedLanguages(defaultSupportedLanguages); + setDefaultLanguage("en"); + } + } + getSupportedLanguages(); + }, []); + + useEffect(() => { + if (Object.keys(supportedLanguages).length > 0) { + const loadLanguage = async () => { + try { + // Only access localStorage in the browser + let storedLanguage; + if (typeof window !== 'undefined') { + storedLanguage = localStorage.getItem('language'); + + // If no language is stored, detect browser language + if (!storedLanguage) { + console.log('No language in localStorage, detecting browser language'); + storedLanguage = detectBrowserLanguage(); + + // Store the detected language + localStorage.setItem('language', storedLanguage); + } + } else { + console.log('Running on server-side, using default language'); + storedLanguage = 'en'; + } + + console.log('Supported languages loaded, validating language:', storedLanguage); + const validLanguage = Object.keys(supportedLanguages).includes(storedLanguage as any) ? storedLanguage : defaultLanguage; + console.log('Valid language determined:', validLanguage); + + // Load messages for the language + const langMessages = (await import(`../messages/${validLanguage}.json`)).default; + + setLanguageState(validLanguage); + setMessages(langMessages); + + // Update HTML lang attribute (only in browser) + if (typeof document !== 'undefined') { + document.documentElement.lang = validLanguage; + } + } catch (error) { + console.error('Failed to load language:', error); + // Fallback to English + console.log('Falling back to English due to error'); + const enMessages = (await import('../messages/en.json')).default; + setMessages(enMessages); + } finally { + setIsLoading(false); + } + }; + + loadLanguage(); + } + }, [supportedLanguages, defaultLanguage]); + + // Update language and load new messages + const setLanguage = async (lang: string) => { + try { + console.log('Setting language to:', lang); + const validLanguage = Object.keys(supportedLanguages).includes(lang as any) ? lang : defaultLanguage; + + // Load messages for the new language + const langMessages = (await import(`../messages/${validLanguage}.json`)).default; + + setLanguageState(validLanguage); + setMessages(langMessages); + + // Store in localStorage (only in browser) + if (typeof window !== 'undefined') { + localStorage.setItem('language', validLanguage); + } + + // Update HTML lang attribute (only in browser) + if (typeof document !== 'undefined') { + document.documentElement.lang = validLanguage; + } + } catch (error) { + console.error('Failed to set language:', error); + } + }; + + if (isLoading) { + return ( +
+
+
+

Loading...

+
+
+ ); + } + + return ( + + {children} + + ); +} + +export function useLanguage() { + const context = useContext(LanguageContext); + if (context === undefined) { + throw new Error('useLanguage must be used within a LanguageProvider'); + } + return context; +} +``` + +## src/components/WikiTypeSelector.tsx + +```text +'use client'; + +import React from 'react'; +import { useLanguage } from '@/contexts/LanguageContext'; +import { FaBookOpen, FaList } from 'react-icons/fa'; + +interface WikiTypeSelectorProps { + isComprehensiveView: boolean; + setIsComprehensiveView: (value: boolean) => void; +} + +const WikiTypeSelector: React.FC = ({ + isComprehensiveView, + setIsComprehensiveView, +}) => { + const { messages: t } = useLanguage(); + + return ( +
+ +
+ + + +
+
+ ); +}; + +export default WikiTypeSelector; +``` + +## src/components/WikiTreeView.tsx + +```text +'use client'; + +import React, { useState } from 'react'; +import { FaChevronRight, FaChevronDown } from 'react-icons/fa'; + +// Import interfaces from the page component +interface WikiPage { + id: string; + title: string; + content: string; + filePaths: string[]; + importance: 'high' | 'medium' | 'low'; + relatedPages: string[]; + parentId?: string; + isSection?: boolean; + children?: string[]; +} + +interface WikiSection { + id: string; + title: string; + pages: string[]; + subsections?: string[]; +} + +interface WikiStructure { + id: string; + title: string; + description: string; + pages: WikiPage[]; + sections: WikiSection[]; + rootSections: string[]; +} + +interface WikiTreeViewProps { + wikiStructure: WikiStructure; + currentPageId: string | undefined; + onPageSelect: (pageId: string) => void; + messages?: { + pages?: string; + [key: string]: string | undefined; + }; +} + +const WikiTreeView: React.FC = ({ + wikiStructure, + currentPageId, + onPageSelect, +}) => { + const [expandedSections, setExpandedSections] = useState>( + new Set(wikiStructure.rootSections) + ); + + const toggleSection = (sectionId: string, event: React.MouseEvent) => { + event.stopPropagation(); + setExpandedSections(prev => { + const newSet = new Set(prev); + if (newSet.has(sectionId)) { + newSet.delete(sectionId); + } else { + newSet.add(sectionId); + } + return newSet; + }); + }; + + const renderSection = (sectionId: string, level = 0) => { + const section = wikiStructure.sections.find(s => s.id === sectionId); + if (!section) return null; + + const isExpanded = expandedSections.has(sectionId); + + return ( +
+ + + {isExpanded && ( +
0 ? 'pl-2 border-l border-[var(--border-color)]/30' : ''}`}> + {/* Render pages in this section */} + {section.pages.map(pageId => { + const page = wikiStructure.pages.find(p => p.id === pageId); + if (!page) return null; + + return ( + + ); + })} + + {/* Render subsections recursively */} + {section.subsections?.map(subsectionId => + renderSection(subsectionId, level + 1) + )} +
+ )} +
+ ); + }; + + // If there are no sections defined yet, or if sections/rootSections are empty arrays, fall back to the flat list view + if (!wikiStructure.sections || wikiStructure.sections.length === 0 || !wikiStructure.rootSections || wikiStructure.rootSections.length === 0) { + console.log("WikiTreeView: Falling back to flat list view due to missing or empty sections/rootSections"); + return ( +
    + {wikiStructure.pages.map(page => ( +
  • + +
  • + ))} +
+ ); + } + + // Log information about the sections for debugging + console.log("WikiTreeView: Rendering tree view with sections:", wikiStructure.sections); + console.log("WikiTreeView: Root sections:", wikiStructure.rootSections); + + return ( +
+ {wikiStructure.rootSections.map(sectionId => { + const section = wikiStructure.sections.find(s => s.id === sectionId); + if (!section) { + console.warn(`WikiTreeView: Could not find section with id ${sectionId}`); + return null; + } + return renderSection(sectionId); + })} +
+ ); +}; + +export default WikiTreeView; +``` + +## src/components/ProcessedProjects.tsx + +```text +'use client'; + +import React, { useState, useEffect, useMemo } from 'react'; +import Link from 'next/link'; +import { FaTimes, FaTh, FaList } from 'react-icons/fa'; + +// Interface should match the structure from the API +interface ProcessedProject { + id: string; + owner: string; + repo: string; + name: string; + repo_type: string; + submittedAt: number; + language: string; +} + +interface ProcessedProjectsProps { + showHeader?: boolean; + maxItems?: number; + className?: string; + messages?: Record>; // Translation messages with proper typing +} + +export default function ProcessedProjects({ + showHeader = true, + maxItems, + className = "", + messages +}: ProcessedProjectsProps) { + const [projects, setProjects] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [searchQuery, setSearchQuery] = useState(''); + const [viewMode, setViewMode] = useState<'card' | 'list'>('card'); + + // Default messages fallback + const defaultMessages = { + title: 'Processed Wiki Projects', + searchPlaceholder: 'Search projects by name, owner, or repository...', + noProjects: 'No projects found in the server cache. The cache might be empty or the server encountered an issue.', + noSearchResults: 'No projects match your search criteria.', + processedOn: 'Processed on:', + loadingProjects: 'Loading projects...', + errorLoading: 'Error loading projects:', + backToHome: 'Back to Home' + }; + + const t = (key: string) => { + if (messages?.projects?.[key]) { + return messages.projects[key]; + } + return defaultMessages[key as keyof typeof defaultMessages] || key; + }; + + useEffect(() => { + const fetchProjects = async () => { + setIsLoading(true); + setError(null); + try { + const response = await fetch('/api/wiki/projects'); + if (!response.ok) { + throw new Error(`Failed to fetch projects: ${response.statusText}`); + } + const data = await response.json(); + if (data.error) { + throw new Error(data.error); + } + setProjects(data as ProcessedProject[]); + } catch (e: unknown) { + console.error("Failed to load projects from API:", e); + const message = e instanceof Error ? e.message : "An unknown error occurred."; + setError(message); + setProjects([]); + } finally { + setIsLoading(false); + } + }; + + fetchProjects(); + }, []); + + // Filter projects based on search query + const filteredProjects = useMemo(() => { + if (!searchQuery.trim()) { + return maxItems ? projects.slice(0, maxItems) : projects; + } + + const query = searchQuery.toLowerCase(); + const filtered = projects.filter(project => + project.name.toLowerCase().includes(query) || + project.owner.toLowerCase().includes(query) || + project.repo.toLowerCase().includes(query) || + project.repo_type.toLowerCase().includes(query) + ); + + return maxItems ? filtered.slice(0, maxItems) : filtered; + }, [projects, searchQuery, maxItems]); + + const clearSearch = () => { + setSearchQuery(''); + }; + + const handleDelete = async (project: ProcessedProject) => { + if (!confirm(`Are you sure you want to delete project ${project.name}?`)) { + return; + } + try { + const response = await fetch('/api/wiki/projects', { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + owner: project.owner, + repo: project.repo, + repo_type: project.repo_type, + language: project.language, + }), + }); + if (!response.ok) { + const errorBody = await response.json().catch(() => ({ error: response.statusText })); + throw new Error(errorBody.error || response.statusText); + } + setProjects(prev => prev.filter(p => p.id !== project.id)); + } catch (e: unknown) { + console.error('Failed to delete project:', e); + alert(`Failed to delete project: ${e instanceof Error ? e.message : 'Unknown error'}`); + } + }; + + return ( +
+ {showHeader && ( +
+
+

{t('title')}

+ + {t('backToHome')} + +
+
+ )} + + {/* Search Bar and View Toggle */} +
+ {/* Search Bar */} +
+ setSearchQuery(e.target.value)} + placeholder={t('searchPlaceholder')} + className="input-japanese block w-full pl-4 pr-12 py-2.5 border border-[var(--border-color)] rounded-lg bg-[var(--background)] text-[var(--foreground)] placeholder:text-[var(--muted)] focus:outline-none focus:border-[var(--accent-primary)] focus:ring-1 focus:ring-[var(--accent-primary)]" + /> + {searchQuery && ( + + )} +
+ + {/* View Toggle */} +
+ + +
+
+ + {isLoading &&

{t('loadingProjects')}

} + {error &&

{t('errorLoading')} {error}

} + + {!isLoading && !error && filteredProjects.length > 0 && ( +
+ {filteredProjects.map((project) => ( + viewMode === 'card' ? ( +
+ + +

+ {project.name} +

+
+ + {project.repo_type} + + + {project.language} + +
+

+ {t('processedOn')} {new Date(project.submittedAt).toLocaleDateString()} +

+ +
+ ) : ( +
+ + +
+

+ {project.name} +

+

+ {t('processedOn')} {new Date(project.submittedAt).toLocaleDateString()} • {project.repo_type} • {project.language} +

+
+
+ + {project.repo_type} + +
+ +
+ ) + ))} +
+ )} + + {!isLoading && !error && projects.length > 0 && filteredProjects.length === 0 && searchQuery && ( +

{t('noSearchResults')}

+ )} + + {!isLoading && !error && projects.length === 0 && ( +

{t('noProjects')}

+ )} +
+ ); +} +``` + +## src/components/Markdown.tsx + +```text +import React from 'react'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import rehypeRaw from 'rehype-raw'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { tomorrow } from 'react-syntax-highlighter/dist/cjs/styles/prism'; +import Mermaid from './Mermaid'; + +interface MarkdownProps { + content: string; +} + +const Markdown: React.FC = ({ content }) => { + // Define markdown components + const MarkdownComponents: React.ComponentProps['components'] = { + p({ children, ...props }: { children?: React.ReactNode }) { + return

{children}

; + }, + h1({ children, ...props }: { children?: React.ReactNode }) { + return

{children}

; + }, + h2({ children, ...props }: { children?: React.ReactNode }) { + // Special styling for ReAct headings + if (children && typeof children === 'string') { + const text = children.toString(); + if (text.includes('Thought') || text.includes('Action') || text.includes('Observation') || text.includes('Answer')) { + return ( +

+ {children} +

+ ); + } + } + return

{children}

; + }, + h3({ children, ...props }: { children?: React.ReactNode }) { + return

{children}

; + }, + h4({ children, ...props }: { children?: React.ReactNode }) { + return

{children}

; + }, + ul({ children, ...props }: { children?: React.ReactNode }) { + return
    {children}
; + }, + ol({ children, ...props }: { children?: React.ReactNode }) { + return
    {children}
; + }, + li({ children, ...props }: { children?: React.ReactNode }) { + return
  • {children}
  • ; + }, + a({ children, href, ...props }: { children?: React.ReactNode; href?: string }) { + return ( + + {children} + + ); + }, + blockquote({ children, ...props }: { children?: React.ReactNode }) { + return ( +
    + {children} +
    + ); + }, + table({ children, ...props }: { children?: React.ReactNode }) { + return ( +
    + + {children} +
    +
    + ); + }, + thead({ children, ...props }: { children?: React.ReactNode }) { + return {children}; + }, + tbody({ children, ...props }: { children?: React.ReactNode }) { + return {children}; + }, + tr({ children, ...props }: { children?: React.ReactNode }) { + return {children}; + }, + th({ children, ...props }: { children?: React.ReactNode }) { + return ( + + {children} + + ); + }, + td({ children, ...props }: { children?: React.ReactNode }) { + return {children}; + }, + code(props: { + inline?: boolean; + className?: string; + children?: React.ReactNode; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; // Using any here as it's required for ReactMarkdown components + }) { + const { inline, className, children, ...otherProps } = props; + const match = /language-(\w+)/.exec(className || ''); + const codeContent = children ? String(children).replace(/\n$/, '') : ''; + + // Handle Mermaid diagrams + if (!inline && match && match[1] === 'mermaid') { + return ( +
    + +
    + ); + } + + // Handle code blocks + if (!inline && match) { + return ( +
    +
    + {match[1]} + +
    + + {codeContent} + +
    + ); + } + + // Handle inline code + return ( + + {children} + + ); + }, + }; + + return ( +
    + + {content} + +
    + ); +}; + +export default Markdown; +``` + +## src/components/Mermaid.tsx + +```text +import React, { useEffect, useRef, useState } from 'react'; +import mermaid from 'mermaid'; +// We'll use dynamic import for svg-pan-zoom + +// Initialize mermaid with defaults - Japanese aesthetic +mermaid.initialize({ + startOnLoad: true, + theme: 'neutral', + securityLevel: 'loose', + suppressErrorRendering: true, + logLevel: 'error', + maxTextSize: 100000, // Increase text size limit + htmlLabels: true, + flowchart: { + htmlLabels: true, + curve: 'basis', + nodeSpacing: 60, + rankSpacing: 60, + padding: 20, + }, + themeCSS: ` + /* Japanese aesthetic styles for all diagrams */ + .node rect, .node circle, .node ellipse, .node polygon, .node path { + fill: #f8f4e6; + stroke: #d7c4bb; + stroke-width: 1px; + } + .edgePath .path { + stroke: #9b7cb9; + stroke-width: 1.5px; + } + .edgeLabel { + background-color: transparent; + color: #333333; + p { + background-color: transparent !important; + } + } + .label { + color: #333333; + } + .cluster rect { + fill: #f8f4e6; + stroke: #d7c4bb; + stroke-width: 1px; + } + + /* Sequence diagram specific styles */ + .actor { + fill: #f8f4e6; + stroke: #d7c4bb; + stroke-width: 1px; + } + text.actor { + fill: #333333; + stroke: none; + } + .messageText { + fill: #333333; + stroke: none; + } + .messageLine0, .messageLine1 { + stroke: #9b7cb9; + } + .noteText { + fill: #333333; + } + + /* Dark mode overrides - will be applied with data-theme="dark" */ + [data-theme="dark"] .node rect, + [data-theme="dark"] .node circle, + [data-theme="dark"] .node ellipse, + [data-theme="dark"] .node polygon, + [data-theme="dark"] .node path { + fill: #222222; + stroke: #5d4037; + } + [data-theme="dark"] .edgePath .path { + stroke: #9370db; + } + [data-theme="dark"] .edgeLabel { + background-color: transparent; + color: #f0f0f0; + } + [data-theme="dark"] .label { + color: #f0f0f0; + } + [data-theme="dark"] .cluster rect { + fill: #222222; + stroke: #5d4037; + } + [data-theme="dark"] .flowchart-link { + stroke: #9370db; + } + + /* Dark mode sequence diagram overrides */ + [data-theme="dark"] .actor { + fill: #222222; + stroke: #5d4037; + } + [data-theme="dark"] text.actor { + fill: #f0f0f0; + stroke: none; + } + [data-theme="dark"] .messageText { + fill: #f0f0f0; + stroke: none; + font-weight: 500; + } + [data-theme="dark"] .messageLine0, [data-theme="dark"] .messageLine1 { + stroke: #9370db; + stroke-width: 1.5px; + } + [data-theme="dark"] .noteText { + fill: #f0f0f0; + } + /* Additional styles for sequence diagram text */ + [data-theme="dark"] #sequenceNumber { + fill: #f0f0f0; + } + [data-theme="dark"] text.sequenceText { + fill: #f0f0f0; + font-weight: 500; + } + [data-theme="dark"] text.loopText, [data-theme="dark"] text.loopText tspan { + fill: #f0f0f0; + } + /* Add a subtle background to message text for better readability */ + [data-theme="dark"] .messageText, [data-theme="dark"] text.sequenceText { + paint-order: stroke; + stroke: #1a1a1a; + stroke-width: 2px; + stroke-linecap: round; + stroke-linejoin: round; + } + + /* Force text elements to be properly colored */ + text[text-anchor][dominant-baseline], + text[text-anchor][alignment-baseline], + .nodeLabel, + .edgeLabel, + .label, + text { + fill: #777 !important; + } + + [data-theme="dark"] text[text-anchor][dominant-baseline], + [data-theme="dark"] text[text-anchor][alignment-baseline], + [data-theme="dark"] .nodeLabel, + [data-theme="dark"] .edgeLabel, + [data-theme="dark"] .label, + [data-theme="dark"] text { + fill: #f0f0f0 !important; + } + + /* Add clickable element styles with subtle transitions */ + .clickable { + transition: all 0.3s ease; + } + .clickable:hover { + transform: scale(1.03); + cursor: pointer; + } + .clickable:hover > * { + filter: brightness(0.95); + } + `, + fontFamily: 'var(--font-geist-sans), var(--font-serif-jp), sans-serif', + fontSize: 12, +}); + +interface MermaidProps { + chart: string; + className?: string; + zoomingEnabled?: boolean; +} + +// Full screen modal component for the diagram +const FullScreenModal: React.FC<{ + isOpen: boolean; + onClose: () => void; + children: React.ReactNode; +}> = ({ isOpen, onClose, children }) => { + const modalRef = useRef(null); + const [zoom, setZoom] = useState(1); + + // Close on Escape key + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + onClose(); + } + }; + + if (isOpen) { + document.addEventListener('keydown', handleKeyDown); + } + + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, [isOpen, onClose]); + + // Handle click outside to close + useEffect(() => { + const handleOutsideClick = (e: MouseEvent) => { + if (modalRef.current && !modalRef.current.contains(e.target as Node)) { + onClose(); + } + }; + + if (isOpen) { + document.addEventListener('mousedown', handleOutsideClick); + } + + return () => { + document.removeEventListener('mousedown', handleOutsideClick); + }; + }, [isOpen, onClose]); + + // Reset zoom when modal opens + useEffect(() => { + if (isOpen) { + setZoom(1); + } + }, [isOpen]); + + if (!isOpen) return null; + + return ( +
    +
    + {/* Modal header with controls */} +
    +
    図表表示
    +
    +
    + + {Math.round(zoom * 100)}% + + +
    + +
    +
    + + {/* Modal content with zoom */} +
    +
    + {children} +
    +
    +
    +
    + ); +}; + +const Mermaid: React.FC = ({ chart, className = '', zoomingEnabled = false }) => { + const [svg, setSvg] = useState(''); + const [error, setError] = useState(null); + const [isFullscreen, setIsFullscreen] = useState(false); + const mermaidRef = useRef(null); + const containerRef = useRef(null); + const idRef = useRef(`mermaid-${Math.random().toString(36).substring(2, 9)}`); + const isDarkModeRef = useRef( + typeof window !== 'undefined' && + window.matchMedia && + window.matchMedia('(prefers-color-scheme: dark)').matches + ); + + // Initialize pan-zoom functionality when SVG is rendered + useEffect(() => { + if (svg && zoomingEnabled && containerRef.current) { + const initializePanZoom = async () => { + const svgElement = containerRef.current?.querySelector("svg"); + if (svgElement) { + // Remove any max-width constraints + svgElement.style.maxWidth = "none"; + svgElement.style.width = "100%"; + svgElement.style.height = "100%"; + + try { + // Dynamically import svg-pan-zoom only when needed in the browser + const svgPanZoom = (await import("svg-pan-zoom")).default; + + svgPanZoom(svgElement, { + zoomEnabled: true, + controlIconsEnabled: true, + fit: true, + center: true, + minZoom: 0.1, + maxZoom: 10, + zoomScaleSensitivity: 0.3, + }); + } catch (error) { + console.error("Failed to load svg-pan-zoom:", error); + } + } + }; + + // Wait for the SVG to be rendered + setTimeout(() => { + void initializePanZoom(); + }, 100); + } + }, [svg, zoomingEnabled]); + + useEffect(() => { + if (!chart) return; + + let isMounted = true; + + const renderChart = async () => { + if (!isMounted) return; + + try { + setError(null); + setSvg(''); + + // Render the chart directly without preprocessing + const { svg: renderedSvg } = await mermaid.render(idRef.current, chart); + + if (!isMounted) return; + + let processedSvg = renderedSvg; + if (isDarkModeRef.current) { + processedSvg = processedSvg.replace(' { + mermaid.contentLoaded(); + }, 50); + } catch (err) { + console.error('Mermaid rendering error:', err); + + const errorMessage = err instanceof Error ? err.message : String(err); + + if (isMounted) { + setError(`Failed to render diagram: ${errorMessage}`); + + if (mermaidRef.current) { + mermaidRef.current.innerHTML = ` +
    Syntax error in diagram
    +
    ${chart}
    + `; + } + } + } + }; + + renderChart(); + + return () => { + isMounted = false; + }; + }, [chart]); + + const handleDiagramClick = () => { + if (!error && svg) { + setIsFullscreen(true); + } + }; + + if (error) { + return ( +
    +
    +
    + + + + 図表レンダリングエラー +
    +
    +
    +
    + 図表に構文エラーがあり、レンダリングできません。 +
    +
    + ); + } + + if (!svg) { + return ( +
    +
    +
    +
    +
    + 図表を描画中... +
    +
    + ); + } + + return ( + <> +
    +
    +
    + + {!zoomingEnabled && ( +
    + + + + + + + Click to zoom +
    + )} +
    +
    + + {!zoomingEnabled && ( + setIsFullscreen(false)} + > +
    + + )} + + ); +}; + + + +export default Mermaid; +``` + +## src/components/Ask.tsx + +```text +'use client'; + +import React, {useState, useRef, useEffect} from 'react'; +import {FaChevronLeft, FaChevronRight } from 'react-icons/fa'; +import Markdown from './Markdown'; +import { useLanguage } from '@/contexts/LanguageContext'; +import RepoInfo from '@/types/repoinfo'; +import getRepoUrl from '@/utils/getRepoUrl'; +import ModelSelectionModal from './ModelSelectionModal'; +import { createChatWebSocket, closeWebSocket, ChatCompletionRequest } from '@/utils/websocketClient'; + +interface Model { + id: string; + name: string; +} + +interface Provider { + id: string; + name: string; + models: Model[]; + supportsCustomModel?: boolean; +} + +interface Message { + role: 'user' | 'assistant' | 'system'; + content: string; +} + +interface ResearchStage { + title: string; + content: string; + iteration: number; + type: 'plan' | 'update' | 'conclusion'; +} + +interface AskProps { + repoInfo: RepoInfo; + provider?: string; + model?: string; + isCustomModel?: boolean; + customModel?: string; + language?: string; + onRef?: (ref: { clearConversation: () => void }) => void; +} + +const Ask: React.FC = ({ + repoInfo, + provider = '', + model = '', + isCustomModel = false, + customModel = '', + language = 'en', + onRef +}) => { + const [question, setQuestion] = useState(''); + const [response, setResponse] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [deepResearch, setDeepResearch] = useState(false); + + // Model selection state + const [selectedProvider, setSelectedProvider] = useState(provider); + const [selectedModel, setSelectedModel] = useState(model); + const [isCustomSelectedModel, setIsCustomSelectedModel] = useState(isCustomModel); + const [customSelectedModel, setCustomSelectedModel] = useState(customModel); + const [isModelSelectionModalOpen, setIsModelSelectionModalOpen] = useState(false); + const [isComprehensiveView, setIsComprehensiveView] = useState(true); + + // Get language context for translations + const { messages } = useLanguage(); + + // Research navigation state + const [researchStages, setResearchStages] = useState([]); + const [currentStageIndex, setCurrentStageIndex] = useState(0); + const [conversationHistory, setConversationHistory] = useState([]); + const [researchIteration, setResearchIteration] = useState(0); + const [researchComplete, setResearchComplete] = useState(false); + const inputRef = useRef(null); + const responseRef = useRef(null); + const providerRef = useRef(provider); + const modelRef = useRef(model); + + // Focus input on component mount + useEffect(() => { + if (inputRef.current) { + inputRef.current.focus(); + } + }, []); + + // Expose clearConversation method to parent component + useEffect(() => { + if (onRef) { + onRef({ clearConversation }); + } + }, [onRef]); + + // Scroll to bottom of response when it changes + useEffect(() => { + if (responseRef.current) { + responseRef.current.scrollTop = responseRef.current.scrollHeight; + } + }, [response]); + + // Close WebSocket when component unmounts + useEffect(() => { + return () => { + closeWebSocket(webSocketRef.current); + }; + }, []); + + useEffect(() => { + providerRef.current = provider; + modelRef.current = model; + }, [provider, model]); + + useEffect(() => { + const fetchModel = async () => { + try { + setIsLoading(true); + + const response = await fetch('/api/models/config'); + if (!response.ok) { + throw new Error(`Error fetching model configurations: ${response.status}`); + } + + const data = await response.json(); + + // use latest provider/model ref to check + if(providerRef.current == '' || modelRef.current== '') { + setSelectedProvider(data.defaultProvider); + + // Find the default provider and set its default model + const selectedProvider = data.providers.find((p:Provider) => p.id === data.defaultProvider); + if (selectedProvider && selectedProvider.models.length > 0) { + setSelectedModel(selectedProvider.models[0].id); + } + } else { + setSelectedProvider(providerRef.current); + setSelectedModel(modelRef.current); + } + } catch (err) { + console.error('Failed to fetch model configurations:', err); + } finally { + setIsLoading(false); + } + }; + if(provider == '' || model == '') { + fetchModel() + } + }, [provider, model]); + + const clearConversation = () => { + setQuestion(''); + setResponse(''); + setConversationHistory([]); + setResearchIteration(0); + setResearchComplete(false); + setResearchStages([]); + setCurrentStageIndex(0); + if (inputRef.current) { + inputRef.current.focus(); + } + }; + const downloadresponse = () =>{ + const blob = new Blob([response], { type: 'text/markdown' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `response-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.md`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +} + + // Function to check if research is complete based on response content + const checkIfResearchComplete = (content: string): boolean => { + // Check for explicit final conclusion markers + if (content.includes('## Final Conclusion')) { + return true; + } + + // Check for conclusion sections that don't indicate further research + if ((content.includes('## Conclusion') || content.includes('## Summary')) && + !content.includes('I will now proceed to') && + !content.includes('Next Steps') && + !content.includes('next iteration')) { + return true; + } + + // Check for phrases that explicitly indicate completion + if (content.includes('This concludes our research') || + content.includes('This completes our investigation') || + content.includes('This concludes the deep research process') || + content.includes('Key Findings and Implementation Details') || + content.includes('In conclusion,') || + (content.includes('Final') && content.includes('Conclusion'))) { + return true; + } + + // Check for topic-specific completion indicators + if (content.includes('Dockerfile') && + (content.includes('This Dockerfile') || content.includes('The Dockerfile')) && + !content.includes('Next Steps') && + !content.includes('In the next iteration')) { + return true; + } + + return false; + }; + + // Function to extract research stages from the response + const extractResearchStage = (content: string, iteration: number): ResearchStage | null => { + // Check for research plan (first iteration) + if (iteration === 1 && content.includes('## Research Plan')) { + const planMatch = content.match(/## Research Plan([\s\S]*?)(?:## Next Steps|$)/); + if (planMatch) { + return { + title: 'Research Plan', + content: content, + iteration: 1, + type: 'plan' + }; + } + } + + // Check for research updates (iterations 1-4) + if (iteration >= 1 && iteration <= 4) { + const updateMatch = content.match(new RegExp(`## Research Update ${iteration}([\\s\\S]*?)(?:## Next Steps|$)`)); + if (updateMatch) { + return { + title: `Research Update ${iteration}`, + content: content, + iteration: iteration, + type: 'update' + }; + } + } + + // Check for final conclusion + if (content.includes('## Final Conclusion')) { + const conclusionMatch = content.match(/## Final Conclusion([\s\S]*?)$/); + if (conclusionMatch) { + return { + title: 'Final Conclusion', + content: content, + iteration: iteration, + type: 'conclusion' + }; + } + } + + return null; + }; + + // Function to navigate to a specific research stage + const navigateToStage = (index: number) => { + if (index >= 0 && index < researchStages.length) { + setCurrentStageIndex(index); + setResponse(researchStages[index].content); + } + }; + + // Function to navigate to the next research stage + const navigateToNextStage = () => { + if (currentStageIndex < researchStages.length - 1) { + navigateToStage(currentStageIndex + 1); + } + }; + + // Function to navigate to the previous research stage + const navigateToPreviousStage = () => { + if (currentStageIndex > 0) { + navigateToStage(currentStageIndex - 1); + } + }; + + // WebSocket reference + const webSocketRef = useRef(null); + + // Function to continue research automatically + const continueResearch = async () => { + if (!deepResearch || researchComplete || !response || isLoading) return; + + // Add a small delay to allow the user to read the current response + await new Promise(resolve => setTimeout(resolve, 2000)); + + setIsLoading(true); + + try { + // Store the current response for use in the history + const currentResponse = response; + + // Create a new message from the AI's previous response + const newHistory: Message[] = [ + ...conversationHistory, + { + role: 'assistant', + content: currentResponse + }, + { + role: 'user', + content: '[DEEP RESEARCH] Continue the research' + } + ]; + + // Update conversation history + setConversationHistory(newHistory); + + // Increment research iteration + const newIteration = researchIteration + 1; + setResearchIteration(newIteration); + + // Clear previous response + setResponse(''); + + // Prepare the request body + const requestBody: ChatCompletionRequest = { + repo_url: getRepoUrl(repoInfo), + type: repoInfo.type, + messages: newHistory.map(msg => ({ role: msg.role as 'user' | 'assistant', content: msg.content })), + provider: selectedProvider, + model: isCustomSelectedModel ? customSelectedModel : selectedModel, + language: language + }; + + // Add tokens if available + if (repoInfo?.token) { + requestBody.token = repoInfo.token; + } + + // Close any existing WebSocket connection + closeWebSocket(webSocketRef.current); + + let fullResponse = ''; + + // Create a new WebSocket connection + webSocketRef.current = createChatWebSocket( + requestBody, + // Message handler + (message: string) => { + fullResponse += message; + setResponse(fullResponse); + + // Extract research stage if this is a deep research response + if (deepResearch) { + const stage = extractResearchStage(fullResponse, newIteration); + if (stage) { + // Add the stage to the research stages if it's not already there + setResearchStages(prev => { + // Check if we already have this stage + const existingStageIndex = prev.findIndex(s => s.iteration === stage.iteration && s.type === stage.type); + if (existingStageIndex >= 0) { + // Update existing stage + const newStages = [...prev]; + newStages[existingStageIndex] = stage; + return newStages; + } else { + // Add new stage + return [...prev, stage]; + } + }); + + // Update current stage index to the latest stage + setCurrentStageIndex(researchStages.length); + } + } + }, + // Error handler + (error: Event) => { + console.error('WebSocket error:', error); + setResponse(prev => prev + '\n\nError: WebSocket connection failed. Falling back to HTTP...'); + + // Fallback to HTTP if WebSocket fails + fallbackToHttp(requestBody); + }, + // Close handler + () => { + // Check if research is complete when the WebSocket closes + const isComplete = checkIfResearchComplete(fullResponse); + + // Force completion after a maximum number of iterations (5) + const forceComplete = newIteration >= 5; + + if (forceComplete && !isComplete) { + // If we're forcing completion, append a comprehensive conclusion to the response + const completionNote = "\n\n## Final Conclusion\nAfter multiple iterations of deep research, we've gathered significant insights about this topic. This concludes our investigation process, having reached the maximum number of research iterations. The findings presented across all iterations collectively form our comprehensive answer to the original question."; + fullResponse += completionNote; + setResponse(fullResponse); + setResearchComplete(true); + } else { + setResearchComplete(isComplete); + } + + setIsLoading(false); + } + ); + } catch (error) { + console.error('Error during API call:', error); + setResponse(prev => prev + '\n\nError: Failed to continue research. Please try again.'); + setResearchComplete(true); + setIsLoading(false); + } + }; + + // Fallback to HTTP if WebSocket fails + const fallbackToHttp = async (requestBody: ChatCompletionRequest) => { + try { + // Make the API call using HTTP + const apiResponse = await fetch(`/api/chat/stream`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody) + }); + + if (!apiResponse.ok) { + throw new Error(`API error: ${apiResponse.status}`); + } + + // Process the streaming response + const reader = apiResponse.body?.getReader(); + const decoder = new TextDecoder(); + + if (!reader) { + throw new Error('Failed to get response reader'); + } + + // Read the stream + let fullResponse = ''; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value, { stream: true }); + fullResponse += chunk; + setResponse(fullResponse); + + // Extract research stage if this is a deep research response + if (deepResearch) { + const stage = extractResearchStage(fullResponse, researchIteration); + if (stage) { + // Add the stage to the research stages + setResearchStages(prev => { + const existingStageIndex = prev.findIndex(s => s.iteration === stage.iteration && s.type === stage.type); + if (existingStageIndex >= 0) { + const newStages = [...prev]; + newStages[existingStageIndex] = stage; + return newStages; + } else { + return [...prev, stage]; + } + }); + } + } + } + + // Check if research is complete + const isComplete = checkIfResearchComplete(fullResponse); + + // Force completion after a maximum number of iterations (5) + const forceComplete = researchIteration >= 5; + + if (forceComplete && !isComplete) { + // If we're forcing completion, append a comprehensive conclusion to the response + const completionNote = "\n\n## Final Conclusion\nAfter multiple iterations of deep research, we've gathered significant insights about this topic. This concludes our investigation process, having reached the maximum number of research iterations. The findings presented across all iterations collectively form our comprehensive answer to the original question."; + fullResponse += completionNote; + setResponse(fullResponse); + setResearchComplete(true); + } else { + setResearchComplete(isComplete); + } + } catch (error) { + console.error('Error during HTTP fallback:', error); + setResponse(prev => prev + '\n\nError: Failed to get a response. Please try again.'); + setResearchComplete(true); + } finally { + setIsLoading(false); + } + }; + + // Effect to continue research when response is updated + useEffect(() => { + if (deepResearch && response && !isLoading && !researchComplete) { + const isComplete = checkIfResearchComplete(response); + if (isComplete) { + setResearchComplete(true); + } else if (researchIteration > 0 && researchIteration < 5) { + // Only auto-continue if we're already in a research process and haven't reached max iterations + // Use setTimeout to avoid potential infinite loops + const timer = setTimeout(() => { + continueResearch(); + }, 1000); + return () => clearTimeout(timer); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [response, isLoading, deepResearch, researchComplete, researchIteration]); + + // Effect to update research stages when the response changes + useEffect(() => { + if (deepResearch && response && !isLoading) { + // Try to extract a research stage from the response + const stage = extractResearchStage(response, researchIteration); + if (stage) { + // Add or update the stage in the research stages + setResearchStages(prev => { + // Check if we already have this stage + const existingStageIndex = prev.findIndex(s => s.iteration === stage.iteration && s.type === stage.type); + if (existingStageIndex >= 0) { + // Update existing stage + const newStages = [...prev]; + newStages[existingStageIndex] = stage; + return newStages; + } else { + // Add new stage + return [...prev, stage]; + } + }); + + // Update current stage index to point to this stage + setCurrentStageIndex(prev => { + const newIndex = researchStages.findIndex(s => s.iteration === stage.iteration && s.type === stage.type); + return newIndex >= 0 ? newIndex : prev; + }); + } + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [response, isLoading, deepResearch, researchIteration]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!question.trim() || isLoading) return; + + handleConfirmAsk(); + }; + + // Handle confirm and send request + const handleConfirmAsk = async () => { + setIsLoading(true); + setResponse(''); + setResearchIteration(0); + setResearchComplete(false); + + try { + // Create initial message + const initialMessage: Message = { + role: 'user', + content: deepResearch ? `[DEEP RESEARCH] ${question}` : question + }; + + // Set initial conversation history + const newHistory: Message[] = [initialMessage]; + setConversationHistory(newHistory); + + // Prepare request body + const requestBody: ChatCompletionRequest = { + repo_url: getRepoUrl(repoInfo), + type: repoInfo.type, + messages: newHistory.map(msg => ({ role: msg.role as 'user' | 'assistant', content: msg.content })), + provider: selectedProvider, + model: isCustomSelectedModel ? customSelectedModel : selectedModel, + language: language + }; + + // Add tokens if available + if (repoInfo?.token) { + requestBody.token = repoInfo.token; + } + + // Close any existing WebSocket connection + closeWebSocket(webSocketRef.current); + + let fullResponse = ''; + + // Create a new WebSocket connection + webSocketRef.current = createChatWebSocket( + requestBody, + // Message handler + (message: string) => { + fullResponse += message; + setResponse(fullResponse); + + // Extract research stage if this is a deep research response + if (deepResearch) { + const stage = extractResearchStage(fullResponse, 1); // First iteration + if (stage) { + // Add the stage to the research stages + setResearchStages([stage]); + setCurrentStageIndex(0); + } + } + }, + // Error handler + (error: Event) => { + console.error('WebSocket error:', error); + setResponse(prev => prev + '\n\nError: WebSocket connection failed. Falling back to HTTP...'); + + // Fallback to HTTP if WebSocket fails + fallbackToHttp(requestBody); + }, + // Close handler + () => { + // If deep research is enabled, check if we should continue + if (deepResearch) { + const isComplete = checkIfResearchComplete(fullResponse); + setResearchComplete(isComplete); + + // If not complete, start the research process + if (!isComplete) { + setResearchIteration(1); + // The continueResearch function will be triggered by the useEffect + } + } + + setIsLoading(false); + } + ); + } catch (error) { + console.error('Error during API call:', error); + setResponse(prev => prev + '\n\nError: Failed to get a response. Please try again.'); + setResearchComplete(true); + setIsLoading(false); + } + }; + + const [buttonWidth, setButtonWidth] = useState(0); + const buttonRef = useRef(null); + + // Measure button width and update state + useEffect(() => { + if (buttonRef.current) { + const width = buttonRef.current.offsetWidth; + setButtonWidth(width); + } + }, [messages.ask?.askButton, isLoading]); + + return ( +
    +
    +
    + {/* Model selection button */} + +
    + + {/* Question input */} +
    +
    + setQuestion(e.target.value)} + placeholder={messages.ask?.placeholder || 'What would you like to know about this codebase?'} + className="block w-full rounded-md border border-[var(--border-color)] bg-[var(--input-bg)] text-[var(--foreground)] px-5 py-3.5 text-base shadow-sm focus:border-[var(--accent-primary)] focus:ring-2 focus:ring-[var(--accent-primary)]/30 focus:outline-none transition-all" + style={{ paddingRight: `${buttonWidth + 24}px` }} + disabled={isLoading} + /> + +
    + + {/* Deep Research toggle */} +
    +
    + +
    +
    +
    +

    Deep Research conducts a multi-turn investigation process:

    +
      +
    • Initial Research: Creates a research plan and initial findings
    • +
    • Iteration 1: Explores specific aspects in depth
    • +
    • Iteration 2: Investigates remaining questions
    • +
    • Iterations 3-4: Dives deeper into complex areas
    • +
    • Final Conclusion: Comprehensive answer based on all iterations
    • +
    +

    The AI automatically continues research until complete (up to 5 iterations)

    +
    +
    +
    + {deepResearch && ( +
    + Multi-turn research process enabled + {researchIteration > 0 && !researchComplete && ` (iteration ${researchIteration})`} + {researchComplete && ` (complete)`} +
    + )} +
    +
    + + {/* Response area */} + {response && ( +
    +
    + +
    + + {/* Research navigation and clear button */} +
    + {/* Research navigation */} + {deepResearch && researchStages.length > 1 && ( +
    + + +
    + {currentStageIndex + 1} / {researchStages.length} +
    + + + +
    + {researchStages[currentStageIndex]?.title || `Stage ${currentStageIndex + 1}`} +
    +
    + )} + +
    + {/* Download button */} + + + {/* Clear button */} + +
    +
    +
    + )} + + {/* Loading indicator */} + {isLoading && !response && ( +
    +
    +
    +
    +
    +
    +
    + + {deepResearch + ? (researchIteration === 0 + ? "Planning research approach..." + : `Research iteration ${researchIteration} in progress...`) + : "Thinking..."} + +
    + {deepResearch && ( +
    +
    + {researchIteration === 0 && ( + <> +
    +
    + Creating research plan... +
    +
    +
    + Identifying key areas to investigate... +
    + + )} + {researchIteration === 1 && ( + <> +
    +
    + Exploring first research area in depth... +
    +
    +
    + Analyzing code patterns and structures... +
    + + )} + {researchIteration === 2 && ( + <> +
    +
    + Investigating remaining questions... +
    +
    +
    + Connecting findings from previous iterations... +
    + + )} + {researchIteration === 3 && ( + <> +
    +
    + Exploring deeper connections... +
    +
    +
    + Analyzing complex patterns... +
    + + )} + {researchIteration === 4 && ( + <> +
    +
    + Refining research conclusions... +
    +
    +
    + Addressing remaining edge cases... +
    + + )} + {researchIteration >= 5 && ( + <> +
    +
    + Finalizing comprehensive answer... +
    +
    +
    + Synthesizing all research findings... +
    + + )} +
    +
    + )} +
    + )} +
    + + {/* Model Selection Modal */} + setIsModelSelectionModalOpen(false)} + provider={selectedProvider} + setProvider={setSelectedProvider} + model={selectedModel} + setModel={setSelectedModel} + isCustomModel={isCustomSelectedModel} + setIsCustomModel={setIsCustomSelectedModel} + customModel={customSelectedModel} + setCustomModel={setCustomSelectedModel} + isComprehensiveView={isComprehensiveView} + setIsComprehensiveView={setIsComprehensiveView} + showFileFilters={false} + onApply={() => { + console.log('Model selection applied:', selectedProvider, selectedModel); + }} + showWikiType={false} + authRequired={false} + isAuthLoading={false} + /> +
    + ); +}; + +export default Ask; +``` + +## src/components/UserSelector.tsx + +```text +'use client'; + +import React, { useState, useEffect } from 'react'; +import { useLanguage } from '@/contexts/LanguageContext'; + +// Define the interfaces for our model configuration +interface Model { + id: string; + name: string; +} + +interface Provider { + id: string; + name: string; + models: Model[]; + supportsCustomModel?: boolean; +} + +interface ModelConfig { + providers: Provider[]; + defaultProvider: string; +} + +interface ModelSelectorProps { + provider: string; + setProvider: (value: string) => void; + model: string; + setModel: (value: string) => void; + isCustomModel: boolean; + setIsCustomModel: (value: boolean) => void; + customModel: string; + setCustomModel: (value: string) => void; + + // File filter configuration + showFileFilters?: boolean; + excludedDirs?: string; + setExcludedDirs?: (value: string) => void; + excludedFiles?: string; + setExcludedFiles?: (value: string) => void; + includedDirs?: string; + setIncludedDirs?: (value: string) => void; + includedFiles?: string; + setIncludedFiles?: (value: string) => void; +} + +export default function UserSelector({ + provider, + setProvider, + model, + setModel, + isCustomModel, + setIsCustomModel, + customModel, + setCustomModel, + + // File filter configuration + showFileFilters = false, + excludedDirs = '', + setExcludedDirs, + excludedFiles = '', + setExcludedFiles, + includedDirs = '', + setIncludedDirs, + includedFiles = '', + setIncludedFiles +}: ModelSelectorProps) { + // State to manage the visibility of the filters modal and filter section + const [isFilterSectionOpen, setIsFilterSectionOpen] = useState(false); + // State to manage filter mode: 'exclude' or 'include' + const [filterMode, setFilterMode] = useState<'exclude' | 'include'>('exclude'); + const { messages: t } = useLanguage(); + + // State for model configurations from backend + const [modelConfig, setModelConfig] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + // State for viewing default values + const [showDefaultDirs, setShowDefaultDirs] = useState(false); + const [showDefaultFiles, setShowDefaultFiles] = useState(false); + + // Fetch model configurations from the backend + useEffect(() => { + const fetchModelConfig = async () => { + try { + setIsLoading(true); + setError(null); + + const response = await fetch('/api/models/config'); + + if (!response.ok) { + throw new Error(`Error fetching model configurations: ${response.status}`); + } + + const data = await response.json(); + setModelConfig(data); + + // Initialize provider and model with defaults from API if not already set + if (!provider && data.defaultProvider) { + setProvider(data.defaultProvider); + + // Find the default provider and set its default model + const selectedProvider = data.providers.find((p: Provider) => p.id === data.defaultProvider); + if (selectedProvider && selectedProvider.models.length > 0) { + setModel(selectedProvider.models[0].id); + } + } + } catch (err) { + console.error('Failed to fetch model configurations:', err); + setError('Failed to load model configurations. Using default options.'); + } finally { + setIsLoading(false); + } + }; + + fetchModelConfig(); + }, [provider, setModel, setProvider]); + + // Handler for changing provider + const handleProviderChange = (newProvider: string) => { + setProvider(newProvider); + setTimeout(() => { + // Reset custom model state when changing providers + setIsCustomModel(false); + + // Set default model for the selected provider + if (modelConfig) { + const selectedProvider = modelConfig.providers.find((p: Provider) => p.id === newProvider); + if (selectedProvider && selectedProvider.models.length > 0) { + setModel(selectedProvider.models[0].id); + } + } + }, 10); + }; + + // Default excluded directories from config.py + const defaultExcludedDirs = +`./.venv/ +./venv/ +./env/ +./virtualenv/ +./node_modules/ +./bower_components/ +./jspm_packages/ +./.git/ +./.svn/ +./.hg/ +./.bzr/ +./__pycache__/ +./.pytest_cache/ +./.mypy_cache/ +./.ruff_cache/ +./.coverage/ +./dist/ +./build/ +./out/ +./target/ +./bin/ +./obj/ +./docs/ +./_docs/ +./site-docs/ +./_site/ +./.idea/ +./.vscode/ +./.vs/ +./.eclipse/ +./.settings/ +./logs/ +./log/ +./tmp/ +./temp/ +./.eng`; + + // Default excluded files from config.py + const defaultExcludedFiles = +`package-lock.json +yarn.lock +pnpm-lock.yaml +npm-shrinkwrap.json +poetry.lock +Pipfile.lock +requirements.txt.lock +Cargo.lock +composer.lock +.lock +.DS_Store +Thumbs.db +desktop.ini +*.lnk +.env +.env.* +*.env +*.cfg +*.ini +.flaskenv +.gitignore +.gitattributes +.gitmodules +.github +.gitlab-ci.yml +.prettierrc +.eslintrc +.eslintignore +.stylelintrc +.editorconfig +.jshintrc +.pylintrc +.flake8 +mypy.ini +pyproject.toml +tsconfig.json +webpack.config.js +babel.config.js +rollup.config.js +jest.config.js +karma.conf.js +vite.config.js +next.config.js +*.min.js +*.min.css +*.bundle.js +*.bundle.css +*.map +*.gz +*.zip +*.tar +*.tgz +*.rar +*.pyc +*.pyo +*.pyd +*.so +*.dll +*.class +*.exe +*.o +*.a +*.jpg +*.jpeg +*.png +*.gif +*.ico +*.svg +*.webp +*.mp3 +*.mp4 +*.wav +*.avi +*.mov +*.webm +*.csv +*.tsv +*.xls +*.xlsx +*.db +*.sqlite +*.sqlite3 +*.pdf +*.docx +*.pptx`; + + // Display loading state + if (isLoading) { + return ( +
    +
    Loading model configurations...
    +
    + ); + } + + return ( +
    +
    + {error && ( +
    {error}
    + )} + + {/* Provider Selection */} +
    + + +
    + + {/* Model Selection - consistent height regardless of type */} +
    + + + {isCustomModel ? ( + { + setCustomModel(e.target.value); + setModel(e.target.value); + }} + placeholder={t.form?.customModelPlaceholder || 'Enter custom model name'} + className="input-japanese block w-full px-2.5 py-1.5 text-sm rounded-md bg-transparent text-[var(--foreground)] focus:outline-none focus:border-[var(--accent-primary)]" + /> + ) : ( + + )} +
    + + {/* Custom model toggle - only when provider supports it */} + {modelConfig?.providers.find((p: Provider) => p.id === provider)?.supportsCustomModel && ( +
    +
    +
    { + const newValue = !isCustomModel; + setIsCustomModel(newValue); + if (newValue) { + setCustomModel(model); + } + }} + > + {}} + className="sr-only" + /> +
    +
    +
    + +
    +
    + )} + + {showFileFilters && ( +
    + + + {isFilterSectionOpen && ( +
    + {/* Filter Mode Selection */} +
    + +
    + + +
    +

    + {filterMode === 'exclude' + ? (t.form?.excludeModeDescription || 'Specify paths to exclude from processing (default behavior)') + : (t.form?.includeModeDescription || 'Specify only the paths to include, ignoring all others') + } +

    +
    + + {/* Directories Section */} +
    + +