Skip to main content
The frontend is a modern React 18 application built with TypeScript and Material-UI v7. It provides a clean, responsive interface for submitting research queries and viewing results in real-time.

Frontend Architecture Overview

Directory Structure

The frontend follows a component-based architecture:
frontend/src/
├── components/             # React components
   ├── Header/            # Application header
   ├── Header.tsx
   └── index.tsx
   ├── SearchInterface/   # Search form and settings
   ├── SearchForm.tsx
   ├── SearchControls.tsx
   ├── SettingsMenu.tsx
   └── index.tsx
   └── ResearchResults/   # Results display
       ├── ResearchResults.tsx
       └── index.tsx
├── hooks/                  # Custom React hooks
   ├── useSearch.ts       # Search state and logic
   └── useSettings.ts     # Settings management
├── types/                  # TypeScript definitions
   ├── index.ts
   ├── search.ts          # Search-related types
   └── settings.ts        # Settings types
├── App.tsx                 # Main application component
├── index.tsx              # Application entry point
└── theme.ts               # MUI theme configuration

Application Entry Point

The application starts in index.tsx:
import React from "react";
import ReactDOM from "react-dom/client";
import { ThemeProvider } from "@mui/material/styles";
import CssBaseline from "@mui/material/CssBaseline";

import App from "./App";
import theme from "./theme";

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);

root.render(
  <React.StrictMode>
    <ThemeProvider theme={theme}>
      <CssBaseline />
      <App />
    </ThemeProvider>
  </React.StrictMode>
);
The app uses Material-UI’s ThemeProvider for consistent styling and CssBaseline for CSS normalization across browsers.

Main Application Component

The App.tsx component orchestrates the entire user interface:
import React from "react";
import Box from "@mui/material/Box";
import Container from "@mui/material/Container";
import Fade from "@mui/material/Fade";

import { Header } from "./components/Header";
import { SearchInterface } from "./components/SearchInterface";
import { ResearchResultsComponent } from "./components/ResearchResults";
import { useSearch } from "./hooks/useSearch";
import { useSettings } from "./hooks/useSettings";

const App: React.FC = () => {
  // Search state management
  const {
    searchQuery,
    setSearchQuery,
    isLoading,
    results,
    error,
    handleSubmit,
    clearResults,
  } = useSearch();

  // Settings state management
  const {
    settingsAnchor,
    maxParallelSearches,
    companyDomains,
    searchDepth,
    confidenceThreshold,
    handleSettingsClick,
    handleSettingsClose,
    // ... other settings methods
    getSettings,
  } = useSettings();

  // Submit handler combining search and settings
  const onSubmit = () => {
    const settings = getSettings();
    handleSubmit({
      research_goal: searchQuery,
      company_domains: settings.companyDomains,
      search_depth: settings.searchDepth,
      max_parallel_searches: settings.maxParallelSearches,
      confidence_threshold: settings.confidenceThreshold,
    });
  };

  return (
    <Box sx={{ minHeight: "100vh", background: "rgba(250, 249, 245, 1)" }}>
      <Container maxWidth="md">
        <Fade in timeout={800}>
          <Box sx={{ textAlign: "center" }}>
            <Header />
            
            {!results ? (
              <SearchInterface
                searchQuery={searchQuery}
                onSearchChange={setSearchQuery}
                onSettingsClick={handleSettingsClick}
                onSubmit={onSubmit}
                isLoading={isLoading}
                settings={{ maxParallelSearches, companyDomains, searchDepth, confidenceThreshold }}
                // ... other props
              />
            ) : (
              <ResearchResultsComponent
                results={results}
                onClear={clearResults}
              />
            )}

            {error && (
              <Box sx={{ mt: 3, p: 2, backgroundColor: "#ffebee" }}>
                <Typography color="error">{error}</Typography>
              </Box>
            )}
          </Box>
        </Fade>
      </Container>
    </Box>
  );
};

export default App;

Key Features

Conditional Rendering

Shows search interface or results based on state

Custom Hooks

Separates business logic from UI components

Error Handling

Displays errors in a user-friendly manner

Loading States

Shows loading indicators during API calls

Custom Hooks

The application uses custom React hooks to manage state and side effects:

useSearch Hook

Manages search query, API calls, and results:
// hooks/useSearch.ts
import { useState } from "react";
import { ResearchResults, SearchQuery } from "../types/search";

export const useSearch = () => {
  const [searchQuery, setSearchQuery] = useState<string>("");
  const [isLoading, setIsLoading] = useState<boolean>(false);
  const [results, setResults] = useState<ResearchResults | null>(null);
  const [error, setError] = useState<string | null>(null);

  const handleSubmit = async (query: SearchQuery) => {
    if (!query.research_goal.trim()) return;

    setIsLoading(true);
    setError(null);
    setResults(null);

    try {
      const response = await fetch("http://localhost:8000/research/batch", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(query),
      });

      if (!response.ok) {
        throw new Error(`API Error: ${response.status} ${response.statusText}`);
      }

      const data: ResearchResults = await response.json();
      setResults(data);
      console.log("Research results:", data);
    } catch (err) {
      const errorMessage =
        err instanceof Error ? err.message : "An error occurred";
      setError(errorMessage);
      console.error("Search error:", err);
    } finally {
      setIsLoading(false);
    }
  };

  const clearResults = () => {
    setResults(null);
    setError(null);
  };

  return {
    searchQuery,
    setSearchQuery,
    isLoading,
    results,
    error,
    handleSubmit,
    clearResults,
  };
};
The useSearch hook encapsulates all search-related logic, keeping the App component clean and focused on UI rendering.

useSettings Hook

Manages research settings and validation:
// hooks/useSettings.ts (simplified)
import { useState } from "react";

export const useSettings = () => {
  const [settingsAnchor, setSettingsAnchor] = useState<null | HTMLElement>(null);
  const [maxParallelSearches, setMaxParallelSearches] = useState<number>(5);
  const [maxSearchesInput, setMaxSearchesInput] = useState<string>("5");
  const [maxSearchesError, setMaxSearchesError] = useState<string>("");
  const [companyDomains, setCompanyDomains] = useState<string[]>([]);
  const [searchDepth, setSearchDepth] = useState<"quick" | "standard" | "comprehensive">("standard");
  const [confidenceThreshold, setConfidenceThreshold] = useState<number>(0.7);
  const [newDomain, setNewDomain] = useState<string>("");

  const handleSettingsClick = (event: React.MouseEvent<HTMLElement>) => {
    setSettingsAnchor(event.currentTarget);
  };

  const handleSettingsClose = () => {
    setSettingsAnchor(null);
  };

  const handleMaxSearchesChange = (value: string) => {
    setMaxSearchesInput(value);
    const num = parseInt(value, 10);
    
    if (isNaN(num) || num < 1 || num > 20) {
      setMaxSearchesError("Must be between 1 and 20");
    } else {
      setMaxSearchesError("");
      setMaxParallelSearches(num);
    }
  };

  const handleAddDomain = () => {
    if (newDomain.trim() && !companyDomains.includes(newDomain.trim())) {
      setCompanyDomains([...companyDomains, newDomain.trim()]);
      setNewDomain("");
    }
  };

  const handleRemoveDomain = (domain: string) => {
    setCompanyDomains(companyDomains.filter((d) => d !== domain));
  };

  const getSettings = () => ({
    maxParallelSearches,
    companyDomains,
    searchDepth,
    confidenceThreshold,
  });

  return {
    settingsAnchor,
    maxParallelSearches,
    maxSearchesInput,
    maxSearchesError,
    companyDomains,
    searchDepth,
    confidenceThreshold,
    newDomain,
    handleSettingsClick,
    handleSettingsClose,
    handleMaxSearchesChange,
    handleAddDomain,
    handleRemoveDomain,
    setSearchDepth,
    setConfidenceThreshold,
    setNewDomain,
    getSettings,
  };
};

TypeScript Type System

The frontend uses comprehensive TypeScript types for type safety:

Search Types

// types/search.ts
export interface SearchQuery {
  research_goal: string;
  company_domains: string[];
  search_depth: "quick" | "standard" | "comprehensive";
  max_parallel_searches: number;
  confidence_threshold: number;
}

export interface Evidence {
  url: string;
  title: string;
  snippet: string;
  source_name: string;
}

export interface Findings {
  technologies: string[];
  evidence: Evidence[];
  signals_found: number;
}

export interface CompanyResearchResult {
  domain: string;
  confidence_score: number;
  evidence_sources: number;
  findings: Findings;
}

export interface SearchPerformance {
  queries_per_second: number;
  failed_requests: number;
}

export interface ResearchResults {
  research_id: string;
  total_companies: number;
  search_strategies_generated: number;
  total_searches_executed: number;
  processing_time_ms: number;
  results: CompanyResearchResult[];
  search_performance: SearchPerformance;
}
These types mirror the backend Pydantic models, ensuring end-to-end type safety from API to UI.

Component Architecture

Header Component

Simple branding header:
// components/Header/Header.tsx
import React from "react";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";

export const Header: React.FC = () => {
  return (
    <Box sx={{ mb: 4 }}>
      <Typography
        variant="h3"
        component="h1"
        sx={{
          fontWeight: 700,
          background: "linear-gradient(135deg, #FF7A00 0%, #FF9A3D 100%)",
          WebkitBackgroundClip: "text",
          WebkitTextFillColor: "transparent",
          mb: 1,
        }}
      >
        GTM Research Engine
      </Typography>
      <Typography variant="body1" color="text.secondary">
        AI-powered company research and analysis
      </Typography>
    </Box>
  );
};

SearchInterface Component

Composite component containing the search form and settings:
// components/SearchInterface/index.tsx (simplified)
import React from "react";
import Box from "@mui/material/Box";
import SearchForm from "./SearchForm";
import SearchControls from "./SearchControls";
import SettingsMenu from "./SettingsMenu";

export const SearchInterface: React.FC<Props> = ({
  searchQuery,
  onSearchChange,
  onSettingsClick,
  onSubmit,
  isLoading,
  settings,
  settingsAnchor,
  onSettingsClose,
  // ... other props
}) => {
  return (
    <Box>
      <SearchForm
        value={searchQuery}
        onChange={onSearchChange}
        onSubmit={onSubmit}
        isLoading={isLoading}
      />
      
      <SearchControls
        onSettingsClick={onSettingsClick}
        settings={settings}
      />
      
      <SettingsMenu
        anchor={settingsAnchor}
        onClose={onSettingsClose}
        // ... settings props
      />
    </Box>
  );
};

ResearchResults Component

Displays research results in a tabbed interface:
// components/ResearchResults/ResearchResults.tsx (simplified)
import React, { useState } from "react";
import Box from "@mui/material/Box";
import Tabs from "@mui/material/Tabs";
import Tab from "@mui/material/Tab";
import Card from "@mui/material/Card";
import Typography from "@mui/material/Typography";

export const ResearchResultsComponent: React.FC<Props> = ({ results, onClear }) => {
  const [selectedTab, setSelectedTab] = useState(0);

  return (
    <Box sx={{ mt: 4 }}>
      {/* Summary Statistics */}
      <Card sx={{ p: 3, mb: 3 }}>
        <Typography variant="h6">Research Summary</Typography>
        <Typography>Companies: {results.total_companies}</Typography>
        <Typography>Searches: {results.total_searches_executed}</Typography>
        <Typography>Time: {results.processing_time_ms}ms</Typography>
      </Card>

      {/* Results Tabs */}
      <Tabs value={selectedTab} onChange={(e, v) => setSelectedTab(v)}>
        {results.results.map((result, index) => (
          <Tab key={index} label={result.domain} />
        ))}
      </Tabs>

      {/* Selected Company Details */}
      {results.results[selectedTab] && (
        <CompanyDetails result={results.results[selectedTab]} />
      )}

      <Button onClick={onClear}>New Search</Button>
    </Box>
  );
};

Material-UI Theme

Custom theme configuration in theme.ts:
import { createTheme } from "@mui/material/styles";

const theme = createTheme({
  palette: {
    primary: {
      main: "#FF7A00",
      light: "#FF9A3D",
      dark: "#CC6200",
    },
    secondary: {
      main: "#1976d2",
    },
    background: {
      default: "#FAF9F5",
      paper: "#FFFFFF",
    },
    text: {
      primary: "#1A1A1A",
      secondary: "#666666",
    },
  },
  typography: {
    fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif',
    h3: {
      fontWeight: 700,
    },
    button: {
      textTransform: "none",
    },
  },
  shape: {
    borderRadius: 8,
  },
});

export default theme;
The theme uses a warm orange primary color (#FF7A00) for the GTM Research Engine brand identity.

State Management Flow

The frontend uses a unidirectional data flow:
1

User Interaction

User interacts with UI (submits form, changes settings)
2

Event Handler

Component calls hook function (e.g., handleSubmit)
3

State Update

Hook updates local state (e.g., setIsLoading(true))
4

API Call

Hook makes asynchronous API request
5

Response Handling

Hook processes response and updates state
6

Re-render

React re-renders components with new state

Error Handling

The frontend handles errors at multiple levels:

API Error Handling

try {
  const response = await fetch(API_URL, { method: "POST", body: JSON.stringify(query) });
  
  if (!response.ok) {
    throw new Error(`API Error: ${response.status} ${response.statusText}`);
  }
  
  const data = await response.json();
  setResults(data);
} catch (err) {
  const errorMessage = err instanceof Error ? err.message : "An error occurred";
  setError(errorMessage);
  console.error("Search error:", err);
} finally {
  setIsLoading(false);
}

Input Validation

const handleMaxSearchesChange = (value: string) => {
  const num = parseInt(value, 10);
  
  if (isNaN(num) || num < 1 || num > 20) {
    setMaxSearchesError("Must be between 1 and 20");
  } else {
    setMaxSearchesError("");
    setMaxParallelSearches(num);
  }
};

Display Errors to User

{error && (
  <Box
    sx={{
      mt: 3,
      p: 2,
      backgroundColor: "#ffebee",
      borderRadius: 1,
      border: "1px solid #f44336",
    }}
  >
    <Typography color="error" variant="body2">
      Error: {error}
    </Typography>
  </Box>
)}

Performance Optimizations

React.memo

Memoize components to prevent unnecessary re-renders

Lazy Loading

Code-split components for faster initial load

Debouncing

Debounce input fields to reduce re-renders

Vite Build

Fast HMR and optimized production builds

Building and Deployment

Development

cd frontend
npm install
npm run dev

Production Build

npm run build
npm run preview  # Preview production build locally

Docker

FROM node:18-alpine as build

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Next Steps

Backend Architecture

Learn about the FastAPI backend

Data Flow

Understand end-to-end data flow

API Reference

Explore API endpoints

Development Guide

Set up development environment

Build docs developers (and LLMs) love