This document provides a technical overview of Qwello's frontend implementation, focusing on the React component architecture, knowledge graph visualization with vis-network.js, state management, and user interface design.
React Architecture
Qwello's frontend is built with React and TypeScript, providing a responsive and intuitive user interface with strong type safety and component reusability.
Project Structure
The frontend codebase follows a modular structure organized by feature and responsibility:
Copy src/
├── main.tsx # Application entry point
├── app/ # Core application components
│ ├── App.tsx # Root application component
│ ├── app-error-boundary/ # Error handling
│ ├── guards/ # Authentication guards
│ ├── http/ # HTTP client configuration
│ ├── router/ # Routing configuration
│ ├── sidebar/ # Application sidebar
│ └── styles/ # Global styles
├── assets/ # Static assets
├── components/ # Shared UI components
├── pages/ # Page components
│ ├── auth/ # Authentication pages
│ ├── graph/ # Knowledge graph visualization
│ ├── library/ # Document library
│ └── thread/ # Document thread view
└── shared/ # Shared utilities and types
├── api/ # API client
├── store/ # State management
├── types/ # TypeScript type definitions
└── utils/ # Utility functions
Component Hierarchy
The component hierarchy is organized to promote reusability and separation of concerns:
Key Components
App Component
The root component that initializes the application, handles authentication state, and sets up routing:
Copy // App.tsx
export function App() {
const { isLoading } = store.useUser();
React.useEffect(() => {
if (localStorage.getItem(AUTH_TOKEN_LABEL)) {
const { getMe } = store.useUser.getState();
getMe();
} else {
const { stopLoading } = store.useUser.getState();
stopLoading();
}
}, []);
if (isLoading) {
return <LoaderPlaceholder style={{ height: "100vh" }} />;
}
return (
<AppErrorBoundary>
<BrowserRouter>
<AppRouter />
<NotificationLayer />
</BrowserRouter>
</AppErrorBoundary>
);
}
Router Configuration
The routing system defines the application's navigation structure:
Copy // AppRouter.tsx
export function AppRouter() {
return (
<Routes>
<Route path={routes.root} element={<Navigate to={routes.main} />} />
<Route path={routes.auth} element={<AuthPage />} />
<Route path={routes.authCallback} element={<AuthCallbackPage />} />
<Route element={<AuthGuard />}>
<Route path={routes.main} element={<MainPage />} />
<Route path={routes.thread} element={<ThreadPage />} />
<Route path={routes.graph} element={<GraphPage />} />
<Route path={routes.library} element={<LibraryPage />} />
</Route>
<Route path="*" element={<Navigate to={routes.main} />} />
</Routes>
);
}
Authentication Guard
The authentication guard ensures that protected routes are only accessible to authenticated users:
Copy // AuthGuard.tsx
export function AuthGuard() {
const { isAuthenticated } = store.useUser();
const location = useLocation();
if (!isAuthenticated) {
return <Navigate to={routes.auth} state={{ from: location }} replace />;
}
return <Outlet />;
}
Knowledge Graph Visualization
The knowledge graph visualization is a core feature of Qwello, implemented using vis-network.js to provide an interactive, force-directed graph layout.
GraphVisualization Component
The GraphVisualization component is responsible for rendering and managing the interactive knowledge graph:
Copy // GraphVisualization.tsx
export const GraphVisualization: React.FC<GraphVisualizationProps> = ({
data,
onNodeSelect,
className,
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const networkRef = useRef<Network | null>(null);
// Create or update network
useEffect(() => {
if (!containerRef.current || !data) return;
// Convert entities to nodes
const nodes = new DataSet(
data.entities.map(entity => ({
id: entity.id,
label: entity.name,
color: typeColors[entity.type] || typeColors.default,
title: entity.attributes.description || entity.name,
originalData: entity,
}))
);
// Convert relationships to edges
const edges = new DataSet(
data.relationships.map((rel, index) => ({
id: `e${index}`,
from: rel.source,
to: rel.target,
label: rel.type,
title: rel.attributes.description || rel.type,
originalData: rel,
}))
);
// Network options
const options = {
nodes: {
shape: "box",
margin: 10,
font: { size: 14 },
},
edges: {
width: 1.5,
smooth: { type: "continuous" },
arrows: { to: { enabled: true, scaleFactor: 0.5 } },
},
physics: {
solver: "forceAtlas2Based",
stabilization: { iterations: 300 },
},
interaction: {
hover: true,
navigationButtons: true,
keyboard: true,
},
};
// Create network
networkRef.current = new Network(
containerRef.current,
{ nodes, edges },
options
);
// Add event listeners
if (onNodeSelect) {
networkRef.current.on("click", params => {
if (params.nodes.length > 0) {
const nodeId = params.nodes[0];
const node = nodes.get(nodeId);
onNodeSelect(node.originalData);
}
});
}
// Fit graph on initial render
networkRef.current.once("stabilizationIterationsDone", () => {
networkRef.current?.fit();
});
// Cleanup
return () => {
if (networkRef.current) {
networkRef.current.destroy();
networkRef.current = null;
}
};
}, [data, onNodeSelect]);
return (
<div
ref={containerRef}
className={`${styles.graphContainer} ${className || ""}`}
/>
);
};
GraphPage Component
The GraphPage component integrates the GraphVisualization with controls and entity details:
Copy // GraphPage.tsx
export const GraphPage: React.FC = () => {
const { id } = useParams<{ id: string }>();
const [selectedEntity, setSelectedEntity] = useState(null);
const [filters, setFilters] = useState({
entityTypes: [],
searchQuery: "",
});
// Fetch knowledge graph data
const { data, isLoading, error } = useKnowledgeGraph(id);
// Apply filters to graph data
const filteredData = React.useMemo(() => {
if (!data) return null;
const filteredEntities = data.entities.filter(entity => {
// Filter by entity type
if (filters.entityTypes.length > 0 && !filters.entityTypes.includes(entity.type)) {
return false;
}
// Filter by search query
if (filters.searchQuery && !entity.name.toLowerCase().includes(filters.searchQuery.toLowerCase())) {
return false;
}
return true;
});
// Get IDs of filtered entities
const entityIds = new Set(filteredEntities.map(e => e.id));
// Filter relationships where both source and target are in filtered entities
const filteredRelationships = data.relationships.filter(
rel => entityIds.has(rel.source) && entityIds.has(rel.target)
);
return {
entities: filteredEntities,
relationships: filteredRelationships,
};
}, [data, filters]);
// Handle entity selection
const handleEntitySelect = (entity) => {
setSelectedEntity(entity);
};
if (isLoading) {
return <div className={styles.loading}>Loading knowledge graph...</div>;
}
return (
<div className={styles.graphPage}>
<div className={styles.sidebar}>
<FilterPanel
entityTypes={Array.from(new Set(data?.entities.map(e => e.type) || []))}
filters={filters}
onChange={setFilters}
/>
{selectedEntity && (
<EntityDetails
entity={selectedEntity}
onClose={() => setSelectedEntity(null)}
/>
)}
</div>
<div className={styles.mainContent}>
<GraphControls />
{filteredData && (
<GraphVisualization
data={filteredData}
onNodeSelect={handleEntitySelect}
className={styles.graph}
/>
)}
</div>
</div>
);
};
Several optimizations are implemented to ensure smooth performance even with large graphs:
Lazy Loading : Nodes and edges are loaded on demand
Virtualization : Only visible nodes and edges are rendered
Debounced Updates : Resize and filter operations are debounced
Incremental Rendering : Large graphs are rendered in stages
Web Workers : Heavy computations are offloaded to web workers
Copy // Example of web worker implementation for layout calculation
function createLayoutWorker() {
const workerCode = `
self.onmessage = function(e) {
const { nodes, edges, options } = e.data;
// Perform layout calculation
const positions = calculateLayout(nodes, edges, options);
// Send results back to main thread
self.postMessage({ positions });
};
`;
const blob = new Blob([workerCode], { type: 'application/javascript' });
return new Worker(URL.createObjectURL(blob));
}
// Usage in component
useEffect(() => {
if (!data?.entities || data.entities.length < 1000) return;
const worker = createLayoutWorker();
worker.onmessage = (e) => {
const { positions } = e.data;
applyPositions(positions);
};
worker.postMessage({
nodes: data.entities,
edges: data.relationships,
options: layoutOptions
});
return () => worker.terminate();
}, [data]);
State Management
Qwello uses a custom state management solution based on Zustand for efficient and type-safe state management.
Store Structure
The store is organized into slices for different domains:
Copy // store/index.ts
export const store = {
useUser: create<UserSlice>()((...a) => ({
...createUserSlice(...a),
})),
useChats: create<ChatsSlice>()((...a) => ({
...createChatsSlice(...a),
})),
useKnowledgeGraph: create<KnowledgeGraphSlice>()((...a) => ({
...createKnowledgeGraphSlice(...a),
})),
useUI: create<UISlice>()((...a) => ({
...createUISlice(...a),
})),
};
Knowledge Graph Slice
Manages knowledge graph data and operations:
Copy // knowledge-graph.slice.ts
export const createKnowledgeGraphSlice: StateCreator<KnowledgeGraphSlice> = (set, get) => ({
graphs: {},
activeGraphId: null,
isLoading: false,
error: null,
fetchGraph: async (id) => {
try {
set({ isLoading: true, error: null });
const graph = await api.knowledgeGraph.getById(id);
set(state => ({
graphs: { ...state.graphs, [id]: graph },
isLoading: false,
}));
} catch (error) {
set({ error: error.message, isLoading: false });
}
},
setActiveGraph: (id) => {
set({ activeGraphId: id });
},
applyFilters: (filters) => {
const { activeGraphId, graphs } = get();
if (!activeGraphId) return;
const graph = graphs[activeGraphId];
if (!graph) return;
// Apply filters to the active graph
// Implementation details omitted for brevity
},
});
User Interface Components
Qwello's UI is built with a set of reusable components that provide a consistent look and feel across the application.
A reusable button component with different variants:
Copy // PrimaryButton.tsx
export const PrimaryButton: React.FC<PrimaryButtonProps> = ({
children,
variant = "primary",
size = "medium",
fullWidth = false,
loading = false,
icon,
className,
disabled,
...props
}) => {
return (
<button
className={`
${styles.button}
${styles[variant]}
${styles[size]}
${fullWidth ? styles.fullWidth : ""}
${loading ? styles.loading : ""}
${className || ""}
`}
disabled={disabled || loading}
{...props}
>
{loading && <span className={styles.spinner} />}
{icon && <span className={styles.icon}>{icon}</span>}
{children}
</button>
);
};
Onboarding Modal
A modal component for user onboarding:
Copy // OnboardingModal.tsx
export const OnboardingModal: React.FC<OnboardingModalProps> = ({
isOpen,
onClose,
onComplete,
}) => {
const [step, setStep] = useState(0);
const [selectedPlan, setSelectedPlan] = useState<string | null>(null);
const steps = [
{
title: "Welcome to Qwello",
description: "Let's get you set up with the right plan for your needs.",
content: (
<div className={styles.plansContainer}>
<SubscriptionCard
title="Free"
price="$0"
features={["5 documents per month", "Basic knowledge graphs", "Standard support"]}
selected={selectedPlan === "free"}
onClick={() => setSelectedPlan("free")}
/>
<SubscriptionCard
title="Pro"
price="$19"
period="month"
features={["Unlimited documents", "Advanced knowledge graphs", "Priority support"]}
selected={selectedPlan === "pro"}
onClick={() => setSelectedPlan("pro")}
recommended
/>
</div>
),
},
// Additional steps omitted for brevity
];
const currentStep = steps[step];
const handleNext = () => {
if (step < steps.length - 1) {
setStep(step + 1);
} else {
onComplete();
}
};
return (
<Modal isOpen={isOpen} onClose={onClose} size="large">
<div className={styles.onboardingModal}>
<h2>{currentStep.title}</h2>
<p>{currentStep.description}</p>
<div className={styles.content}>
{currentStep.content}
</div>
<div className={styles.actions}>
{step > 0 && (
<PrimaryButton variant="outline" onClick={() => setStep(step - 1)}>
Back
</PrimaryButton>
)}
<PrimaryButton
onClick={handleNext}
disabled={step === 0 && !selectedPlan}
>
{step < steps.length - 1 ? "Continue" : "Get Started"}
</PrimaryButton>
</div>
</div>
</Modal>
);
};
Responsive Design
Qwello's frontend is designed to be responsive across different screen sizes and devices.
SCSS Mixins
Reusable mixins for responsive design:
Copy // mixins.scss
@mixin mobile {
@media (max-width: 767px) {
@content;
}
}
@mixin tablet {
@media (min-width: 768px) and (max-width: 1023px) {
@content;
}
}
@mixin desktop {
@media (min-width: 1024px) {
@content;
}
}
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
Responsive Layout
Example of responsive layout implementation:
Copy // styles.module.scss
.graphPage {
display: flex;
height: 100%;
@include mobile {
flex-direction: column;
}
.sidebar {
width: 300px;
@include mobile {
width: 100%;
height: auto;
border-right: none;
border-bottom: 1px solid var(--border-color);
}
}
.mainContent {
flex: 1;
overflow: hidden;
@include mobile {
height: 60vh;
}
}
}
Accessibility Features
Qwello implements various accessibility features to ensure the application is usable by people with disabilities:
Semantic HTML : Using appropriate HTML elements for their intended purpose
ARIA Attributes : Adding ARIA roles and attributes where necessary
Keyboard Navigation : Ensuring all interactive elements are keyboard accessible
Focus Management : Properly managing focus for modal dialogs and other components
Color Contrast : Maintaining sufficient color contrast for text and UI elements
Copy // Example of accessibility implementation in a component
export const AccessibleButton: React.FC<AccessibleButtonProps> = ({
children,
onClick,
ariaLabel,
disabled,
}) => {
return (
<button
onClick={onClick}
aria-label={ariaLabel}
disabled={disabled}
aria-disabled={disabled}
className={styles.button}
>
{children}
</button>
);
};
Testing Strategy
Qwello's frontend is tested using a combination of unit tests, integration tests, and end-to-end tests:
Unit Tests : Testing individual components and functions in isolation
Integration Tests : Testing interactions between components
End-to-End Tests : Testing complete user flows
Copy // Example of a component test
describe('GraphVisualization', () => {
it('renders without crashing', () => {
render(<GraphVisualization data={mockData} />);
expect(screen.getByTestId('graph-container')).toBeInTheDocument();
});
it('calls onNodeSelect when a node is clicked', async () => {
const onNodeSelect = jest.fn();
render(<GraphVisualization data={mockData} onNodeSelect={onNodeSelect} />);
// Simulate node click (implementation details omitted)
expect(onNodeSelect).toHaveBeenCalledWith(expect.objectContaining({
id: mockData.entities[0].id,
}));
});
});
This documentation provides an overview of Qwello's frontend implementation, focusing on the React component architecture, knowledge graph visualization, state management, and user interface design. The frontend is built with modern web technologies and follows best practices for performance, accessibility, and maintainability.