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
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:
User Interaction
User interacts with UI (submits form, changes settings)
Event Handler
Component calls hook function (e.g., handleSubmit)
State Update
Hook updates local state (e.g., setIsLoading(true))
API Call
Hook makes asynchronous API request
Response Handling
Hook processes response and updates state
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 );
}
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 >
)}
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