Skip to main content

Overview

This page documents the main custom components built for the Argument Analysis Tool. These components combine shadcn/ui primitives with application-specific logic.

AnalysisAndSocialLayout

The main layout component that displays the argument analysis alongside social media pulse data. Location: src/components/analysis/AnalysisAndSocialLayout.tsx

Props

analysisData
AnalysisResult
required
The complete analysis result including blueprint, summary, analysis, social pulse, and tweets
onReset
() => void
required
Callback function to reset the analysis and return to the input form

Type Definition

interface AnalysisAndSocialLayoutProps {
  analysisData: AnalysisResult;
  onReset: () => void;
}

type AnalysisResult = {
  blueprint: ArgumentNode[];
  summary: string;
  analysis: string;
  socialPulse: string;
  tweets: Tweet[];
};

Usage Example

src/app/page.tsx
import { AnalysisAndSocialLayout } from '@/components/analysis/AnalysisAndSocialLayout';

export default function Home() {
  const [state, formAction, isPending] = useActionState(handleAnalysis, initialState);
  
  if (state.data) {
    return (
      <AnalysisAndSocialLayout 
        analysisData={state.data} 
        onReset={() => window.location.reload()} 
      />
    );
  }
  
  // ... render input form
}

Features

Responsive Split ViewThe component creates a two-column grid layout:
<div className={cn(
  "grid h-[calc(100vh-80px)] w-full transition-all duration-300",
  isSocialOpen ? "grid-cols-[1fr,384px]" : "grid-cols-[1fr,0px]"
)}>
  {/* Analysis view on left */}
  {/* Social view on right */}
</div>
  • Left column: Analysis visualizations (flexible width)
  • Right column: Social pulse (384px fixed width)
  • Smooth transitions when toggling social panel

Component Tree

AnalysisAndSocialLayout
├── AnalysisView
│   ├── AnalysisToolbar
│   │   ├── View mode buttons
│   │   ├── Export button
│   │   ├── Social toggle
│   │   └── Reset button
│   └── Visualization views
│       ├── BalancedView
│       ├── TreeView
│       ├── PillarView
│       ├── CircularView
│       └── FlowchartView
└── SocialView
    ├── Social pulse analysis
    └── TweetCard (multiple)

InputForm

The main input form for analyzing topics, URLs, or documents. Location: src/components/home/InputForm.tsx

Props

formAction
(formData: FormData) => void
required
Server action to handle form submission and trigger analysis
authToken
string | null
required
Firebase authentication token for API requests

Type Definition

type InputType = 'Topic' | 'URL' | 'Document';

interface InputFormProps {
  formAction: (formData: FormData) => void;
  authToken: string | null;
}

Usage Example

src/app/page.tsx
import { InputForm } from '@/components/home/InputForm';
import { handleAnalysis } from '@/lib/actions';

export default function Home() {
  const [state, formAction, isPending] = useActionState(handleAnalysis, initialState);
  const [authToken, setAuthToken] = useState<string | null>(null);
  
  useEffect(() => {
    if (user) {
      user.getIdToken().then(token => setAuthToken(token));
    }
  }, [user]);
  
  return <InputForm formAction={formAction} authToken={authToken} />;
}

Features

Three Input ModesUsers can analyze arguments from three different sources:1. Topic
  • Free-text input for any topic
  • Example: “The pros and cons of universal basic income”
  • Uses <Textarea> component
2. URL3. Document
  • File upload or text paste
  • Supported formats: .txt, .md, .pdf
  • PDF parsing using pdf.js
  • Text extraction preview

Form Data Structure

The form submits the following data:
formData: {
  inputType: 'Topic' | 'URL' | 'Document',
  input: string,           // User input or extracted text
  authToken: string | null // Firebase auth token
}

Styling

The form uses the neobrutalism design system:
<Card className="w-full border-4 shadow-[8px_8px_0px_hsl(var(--border))]">
  {/* Bold 4px border with hard drop shadow */}
</Card>

<TabsTrigger className="
  rounded-sm 
  border-2 
  border-transparent 
  data-[state=active]:border-border 
  data-[state=active]:bg-background
">
  {/* Active tab has 2px border */}
</TabsTrigger>

LoadingSpinner

A simple, reusable loading spinner component. Location: src/components/common/LoadingSpinner.tsx

Props

className
string
Optional Tailwind classes to customize size and color

Implementation

src/components/common/LoadingSpinner.tsx
import { Loader2 } from "lucide-react";
import { cn } from "@/lib/utils";

export function LoadingSpinner({ className }: { className?: string }) {
  return (
    <Loader2 className={cn("animate-spin text-accent", className)} />
  );
}

Usage Examples

<LoadingSpinner />

Real Usage

From src/app/page.tsx:
// Loading state while checking authentication
if (isUserLoading || !user) {
  return (
    <div className="container mx-auto flex h-[calc(100vh-80px)] max-w-4xl flex-col items-center justify-center p-4">
      <LoadingSpinner className="h-12 w-12" />
    </div>
  );
}

// Loading state during analysis
if (isPending) {
  return (
    <Card className="w-full max-w-2xl border-4 p-8 text-center">
      <CardContent className="flex flex-col items-center gap-4 p-0">
        <LoadingSpinner className="h-12 w-12" />
        <h2 className="font-headline text-3xl font-bold">Analyzing Arguments...</h2>
        <p className="text-muted-foreground">The AI is deconstructing the source material.</p>
      </CardContent>
    </Card>
  );
}

FirebaseErrorListener

An invisible component that listens for global Firebase permission errors and throws them to be caught by Next.js error boundaries. Location: src/components/FirebaseErrorListener.tsx

Props

This component accepts no props.

Type Definition

import { FirestorePermissionError } from '@/firebase/errors';

// Component returns null (renders nothing)
export function FirebaseErrorListener(): null

Implementation

src/components/FirebaseErrorListener.tsx
'use client';

import { useState, useEffect } from 'react';
import { errorEmitter } from '@/firebase/error-emitter';
import { FirestorePermissionError } from '@/firebase/errors';

export function FirebaseErrorListener() {
  const [error, setError] = useState<FirestorePermissionError | null>(null);

  useEffect(() => {
    const handleError = (error: FirestorePermissionError) => {
      setError(error);
    };

    errorEmitter.on('permission-error', handleError);

    return () => {
      errorEmitter.off('permission-error', handleError);
    };
  }, []);

  // Throw error to Next.js error boundary
  if (error) {
    throw error;
  }

  return null; // Renders nothing
}

How It Works

1

Error Event Emission

Firebase operations emit permission errors to a global event emitter:
// In Firebase service
catch (error) {
  errorEmitter.emit('permission-error', new FirestorePermissionError());
}
2

Component Listens

The FirebaseErrorListener subscribes to the ‘permission-error’ event:
errorEmitter.on('permission-error', handleError);
3

State Update

When an error is received, it updates component state:
const handleError = (error: FirestorePermissionError) => {
  setError(error);
};
4

Error Boundary Catch

On re-render, the component throws the error:
if (error) {
  throw error; // Caught by global-error.tsx
}

Usage

Add to your root layout:
app/layout.tsx
import { FirebaseErrorListener } from '@/components/FirebaseErrorListener';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <FirebaseErrorListener />
        {children}
      </body>
    </html>
  );
}

Why This Pattern?

Firebase services don’t need to know about React error boundaries. They just emit events:
// Firebase service remains clean
export async function getData() {
  try {
    return await getDoc(docRef);
  } catch (error) {
    errorEmitter.emit('permission-error', error);
  }
}
A single component catches errors from anywhere in the app:
  • Firestore operations
  • Firebase Auth operations
  • Real-time listeners
  • Cloud Functions calls
All permission errors are routed to Next.js error boundaries.
The event emitter is strongly typed:
type ErrorEvents = {
  'permission-error': FirestorePermissionError;
};

const errorEmitter = new TypedEventEmitter<ErrorEvents>();
TypeScript enforces that listeners receive the correct error type.
The component properly cleans up the event listener:
return () => {
  errorEmitter.off('permission-error', handleError);
};

Error Boundary Integration

The thrown error is caught by global-error.tsx:
app/global-error.tsx
'use client';

export default function GlobalError({ error, reset }) {
  return (
    <html>
      <body>
        <h2>Permission Error</h2>
        <p>{error.message}</p>
        <button onClick={() => reset()}>Try again</button>
      </body>
    </html>
  );
}

Component Composition Patterns

Server vs Client Components

The app uses both server and client components appropriately:
// Server Component (default)
export default async function Page() {
  const data = await fetchData();
  return <StaticContent data={data} />;
}

// Client Component (with 'use client')
'use client';
export function InteractiveForm() {
  const [value, setValue] = useState('');
  return <Input value={value} onChange={e => setValue(e.target.value)} />;
}
Client Components (require 'use client'):
  • InputForm - Uses useState, event handlers
  • AnalysisAndSocialLayout - Uses useState for toggle
  • FirebaseErrorListener - Uses useEffect, useState
  • LoadingSpinner - Can be used in both

Compound Components

Many shadcn/ui components use the compound component pattern:
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';

<Card>
  <CardHeader>
    <CardTitle>Title</CardTitle>
  </CardHeader>
  <CardContent>
    Content here
  </CardContent>
</Card>
This provides:
  • Flexible composition
  • Consistent spacing and styling
  • Clear component hierarchy

Render Props

Some components accept render functions:
<Tabs>
  <TabsList>
    <TabsTrigger value="topic">Topic</TabsTrigger>
  </TabsList>
  <TabsContent value="topic">
    {/* Content rendered when active */}
  </TabsContent>
</Tabs>

Best Practices

Component Organization

  • Place reusable components in /common
  • Feature-specific components in feature folders
  • UI primitives in /ui
  • Keep components focused and single-purpose

Props Documentation

  • Define TypeScript interfaces for props
  • Use descriptive prop names
  • Provide default values when sensible
  • Document complex props with JSDoc

State Management

  • Keep state as local as possible
  • Lift state only when needed
  • Use Server Actions for mutations
  • Prefer controlled components

Error Handling

  • Handle errors at component boundaries
  • Show user-friendly error messages
  • Use toast notifications for feedback
  • Implement error boundaries for crashes

Next Steps

Component Library

Learn about the component architecture

Server Actions

Understand form handling and mutations

Build docs developers (and LLMs) love