Welcome to the Frontend of the DocuThinker application! This React-based frontend integrates with the DocuThinker backend, allowing users to upload documents, chat with an AI, and extract key insights from their documents. The frontend also provides various authentication functionalities such as registration, login, and password recovery.
Documents in many formats β PDF, Word (DOCX), Markdown, HTML, CSV/TSV, JSON, plain text, and a broad set of code/config files β are parsed in the browser: clean text is extracted for the AI and a faithful display rendering is produced for the viewer, while the original file is uploaded directly to Supabase Storage so the real document can be re-rendered later β for both live uploads and reopened history.
src/utils/auth.js)The DocuThinker Frontend is built using React 18 and Material-UI to create a clean and responsive interface. It allows users to:
Backend: the app talks to the deployed backend at
https://docuthinker-app-backend-api.vercel.app. The original file bytes are stored in Supabase Storage via a backend-minted signed-upload token.
| Area | Technology |
|---|---|
| UI framework | React 18 (functional components + hooks) |
| Component library | Material-UI (MUI) v6 with Emotion styling |
| Font | Poppins (@fontsource/poppins) |
| Routing | react-router-dom v6 |
| HTTP | axios |
| Build tooling | Create React App compiled via CRACO (craco.config.js) |
| Storage client | @supabase/supabase-js (direct-to-Storage signed uploads) |
| Client-side extraction | A single extractDocument() dispatcher over pdfjs-dist (PDF), mammoth (DOCX), DOMPurify (HTML), file.text() (Markdown/text/code), and a custom CSV/TSVβtable + JSON pretty-printer |
| PDF text extraction | pdfjs-dist (legacy build, line/paragraph reconstruction) |
| DOCX conversion | mammoth β plain text + display HTML |
| HTML sanitizing | dompurify for the original-document viewer |
| Markdown & summary rendering | react-markdown + remark-gfm / remark-math / rehype-katex (KaTeX math) |
| Drag-and-drop upload | react-dropzone |
| Google Drive import | gapi-script (Drive read-only) |
| Passkeys | @simplewebauthn/browser |
| Analytics | Google Analytics + @vercel/analytics / @vercel/speed-insights |
| Route | Page | Description |
|---|---|---|
/ |
Landing | Marketing / welcome page with light & dark variants. |
/home |
Home | Upload a document, then view the original + summary and run all AI tools. Shows a sign-in card when logged out. |
/documents |
Documents | Instant client-side search (by title or summary), sort (newest / oldest / title AβZ / ZβA), type filter (PDF / Word / Markdown / HTML / CSV / JSON / Text, each with its own icon + colored chip), paginated (5 per page), plus a spinner while a doc opens. Rename, delete, or re-open any document. |
/profile |
Profile | Avatar, email, account stats (days since joined, document count), social links, and a hero card. Sign-in gated. |
/passkeys |
Passkeys | WebAuthn management β add, rename, and delete passkeys (auth-only). |
/how-to-use |
How to Use | Step-by-step guide to every feature. |
/login |
Login | Email/password, Google OAuth, and βSign in with a passkeyβ. |
/register |
Register | Account creation, followed by an optional βcreate a passkeyβ prompt. |
/forgot-password |
Forgot Password | Password reset flow. |
The frontend consists of several pages and components that make up the user interface. Here are the main pages:
react-dropzone), or import straight from Google Drive (gapi-script). Supports PDF, Word (DOCX), Markdown, HTML, CSV/TSV, JSON, plain text, and many code/config files β see Supported Upload Formats.extractDocument() dispatcher. pdfjs-dist reconstructs lines/paragraphs from each text itemβs vertical position (instead of collapsing into one blob); mammoth returns raw text + structured HTML from DOCX; CSV/TSV becomes an HTML table, JSON is pretty-printed, and code/text files render as monospace blocks. The original file uploads directly to Supabase while the AI receives clean text, keeping the request payload small and Vercel-serverless-friendly.<iframe>, DOCX/HTML as sanitized HTML, Markdown via react-markdown, CSV as a table, JSON/code in a monospace <pre>, and plain text as pre-wrap β for live uploads and reopened history. See Original Document Viewer.| Drag-resizable columns: the Original | Summary split is resizable via a draggable divider (double-click to reset). |
utils/auth.js β no polling, no prop drilling.mobile-app/.The upload modal (components/UploadModal.js) accepts a wide range of formats. A single extractDocument() dispatcher inspects the fileβs MIME type / extension and routes it to the right handler, returning clean text (for the AI), display HTML (for the viewer), and a resolved fileType. The original file is always stored as-is; the AI only ever sees the extracted text.
| Format | Extensions | How itβs extracted | Viewer rendering |
|---|---|---|---|
.pdf |
pdfjs-dist (line/paragraph reconstruction) |
Native <iframe> of the real PDF |
|
| Word | .docx |
mammoth β raw text + structured HTML |
Sanitized HTML |
| Markdown | .md, .markdown |
file.text() |
react-markdown (GFM + KaTeX) |
| HTML | .html, .htm |
file.text(), tags stripped for the AI |
DOMPurify-sanitized HTML |
| CSV / TSV | .csv, .tsv |
file.text() β parsed delimited rows |
HTML <table> (first row = header) |
| JSON | .json |
file.text(), pretty-printed via JSON.stringify(β¦, 2) |
Monospace <pre> |
| Plain text | .txt, .text, .log |
file.text() |
pre-wrap plaintext |
| Code / config | .xml .yaml/.yml .js/.jsx/.mjs/.cjs .ts/.tsx .py .java .c/.cpp/.cc/.h/.hpp .cs .go .rs .rb .php .sql .sh/.bash .css/.scss/.less .ini/.toml/.conf/.env .kt .swift .r .lua .pl |
file.text() |
Monospace <pre> |
Unsupported files surface a clear error listing the accepted formats; nothing is sent to the backend.
The upload pipeline (components/UploadModal.js + utils/supabaseClient.js) does three things: extract text/HTML for the AI and viewer, store the original file, then ask the backend to summarize.
extractDocument() dispatches on the file type: extractFromPdf (via pdfjs-dist) returns clean plaintext + reconstructed <p> HTML; extractFromDocx (via mammoth) returns raw text and structured HTML; Markdown/HTML/CSV/TSV/JSON/text/code are read with file.text() and turned into their display form (sanitized HTML, table, pretty-printed JSON, or monospace block). See Supported Upload Formats.POST /document-upload-url), then uploads the file bytes directly to Supabase Storage with supabase.storage.from(BUCKET).uploadToSignedUrl(...). This bypasses the serverless body-size limit, so large PDFs upload fine. If the direct upload canβt run (e.g. the frontend Supabase env vars arenβt set), it falls back to a through-backend multipart upload (POST /document-file). Storage is non-fatal: if it fails entirely, the app still summarizes and falls back to the HTML/text view.POST /upload, which returns the summary plus a signed fileUrl the viewer can render.sequenceDiagram
autonumber
actor User
participant Modal as UploadModal.js
participant Backend as Backend API
participant Storage as Supabase Storage
User->>Modal: Drop / pick a file (PDF, DOCX, MD, HTML, CSV, JSON, text, codeβ¦)
Modal->>Modal: extractDocument() β text + display HTML
Modal->>Backend: POST /document-upload-url (userId, fileName)
Backend-->>Modal: { path, token } (signed upload)
alt Supabase envs present
Modal->>Storage: uploadToSignedUrl(path, token, file)
else Fallback
Modal->>Backend: POST /document-file (multipart)
end
Modal->>Backend: POST /upload (text, html, filePath, fileType)
Backend-->>Modal: { summary, originalText, originalHtml, fileUrl, fileType }
Modal->>User: Render Original + Summary
The Supabase browser client is created in utils/supabaseClient.js from REACT_APP_SUPABASE_* env vars. The public anon key cannot read the private bucket on its own β every upload is authorized by the server-minted signed token (the service_role key stays on the backend). When the env vars are absent the client is null, and callers transparently use the through-backend fallback.
The Home page renders the left column based on the stored file type, so the viewer works identically for a fresh upload and a document reopened from history:
| Source | Rendering |
|---|---|
PDF (fileType includes pdf and a signed fileUrl exists) |
Native <iframe> pointing at the signed Supabase URL β real, paginated PDF pages. |
DOCX / HTML / CSV / TSV / code (any originalHtml present) |
The display HTML is sanitized with DOMPurify and styled for headings, bold, lists, tables, blockquotes, images, and <pre> code blocks. |
| Markdown | Rendered with react-markdown (GFM tables + KaTeX math). |
| Anything else / no file | Readable pre-wrap plaintext fallback. |
While dragging the column splitter, a transparent overlay sits above the iframe so the PDF doesnβt swallow the mouse events and break the drag.
All AI tools run against the deployed backend. The document title is prepended as extra context (withTitle(...)) on non-persisted payloads, giving the model a stronger signal without polluting the stored text.
| Tool | What it does |
|---|---|
| Summary | Generated on upload; rendered as Markdown with GFM tables + KaTeX math. |
| Key Ideas | Extracts the most important points. |
| Discussion Points | Prompts for debate / group analysis. |
| Bullet-Point Summary | A concise bulleted digest. |
| Change Language | Re-renders the summary in any of ~45 languages. |
| Sentiment Analysis | A positive/neutral/negative meter. Cached per document in localStorage (content-addressed key) so revisits and history switches donβt recompute. |
| Document Analytics | A client-side stats modal: a Flesch readability gauge, top-word bars, word-length and sentence-length distributions, punctuation analysis, lexical diversity, reading & speaking time, and animated counters β all computed in the browser. |
| Generate Recommendations | Actionable next steps based on the content. |
| Rewrite Content | Rewrites the whole document β or just a highlighted passage β in a chosen tone/style. |
| Refine Summary | Re-summarizes with custom instructions. |
| Voice Chat | Upload or record audio (mic-recorder-to-mp3) and talk to the AI. |
| AI Chat | Ask questions grounded in the document. Shows a friendly greeting when the thread is empty, and prepends the document title as context so the model has a stronger signal (also aware of todayβs date). |
Here is the complete file structure for the DocuThinker Frontend. The frontend is located under DocuThinker-AI-App/frontend:
DocuThinker-AI-App/
βββ frontend/
β βββ public/
β β βββ index.html # Main HTML template
β β βββ manifest.json # Manifest for PWA settings
β β βββ pdf.worker.min.mjs # Local pdf.js worker (served for client-side extraction)
β βββ src/
β β βββ assets/ # Static assets like images and fonts
β β β βββ logo.png # App logo or images
β β βββ components/
β β β βββ ChatModal.js # AI chat modal
β β β βββ Spinner.js # Loading spinner component
β β β βββ UploadModal.js # Upload + client-side extraction + Supabase storage
β β β βββ GoogleDriveFileSelectorModal.js # Google Drive file picker
β β β βββ PasskeyPromptModal.js # Post-sign-up "create a passkey" modal
β β β βββ useErrorToast.js # Reusable error-toast hook
β β β βββ Navbar.js # Navigation bar (orange-accent hover, Account dropdown, mobile drawer)
β β β βββ Footer.js # Footer component
β β β βββ GoogleAnalytics.js # Google Analytics integration component
β β βββ pages/
β β β βββ Home.js # Upload + original/summary viewer + all AI tools
β β β βββ DocumentsPage.js # History: search / sort / filter / paginate / rename / delete
β β β βββ Profile.js # Avatar, email, stats, social links, hero card
β β β βββ LandingPage.js # Welcome and information page
β β β βββ Login.js # Login page (incl. "Sign in with a passkey")
β β β βββ Register.js # Registration page
β β β βββ Passkeys.js # Passkey management page (add/rename/delete)
β β β βββ ForgotPassword.js # Forgot password page
β β β βββ PrivacyPolicy.js # Privacy Policy page (dark-mode-aware)
β β β βββ TermsOfService.js # Terms of Service page (dark-mode-aware)
β β β βββ NotFoundPage.js # 404 page (dark-mode-aware)
β β β βββ HowToUse.js # Page explaining how to use the app
β β βββ utils/
β β β βββ auth.js # Event-driven client session helper
β β β βββ api.js # Centralized API base URL
β β β βββ supabaseClient.js # Browser Supabase client (REACT_APP_SUPABASE_*)
β β β βββ passkeys.js # WebAuthn (passkey) client helpers
β β βββ App.js # Main App component
β β βββ index.js # Entry point for the React app
β β βββ App.css # Global CSS 1
β β βββ index.css # Global CSS 2
β β βββ reportWebVitals.js # Web Vitals reporting
β β βββ styles.css # Custom styles for different components
β β βββ config.js # Configuration file for environment variables
β βββ craco.config.js # CRACO config (Node polyfill fallbacks for the browser bundle)
β βββ .env # Environment variables file (REACT_APP_* only)
β βββ package.json # Project dependencies and scripts
β βββ README.md # This README file
β βββ package-lock.json # Lock file for dependencies
Navbar, Footer, UploadModal, ChatModal, and GoogleAnalytics.Home, DocumentsPage, Profile, LandingPage, Login).auth.js (event-driven session helper shared by Login, Profile, and Navbar), supabaseClient.js (browser Supabase client), api.js, and passkeys.js.index.html, manifest.json, and the local pdf.worker.min.mjs used by client-side PDF extraction β files that arenβt processed by Webpack.src/utils/auth.js)Session state lives in localStorage under two keys (token, userId) and is broadcast through a custom "auth-change" event plus the native cross-tab storage event. Screens subscribe via onAuthChange(handler) and re-render the instant the keys change β there is no polling.
sequenceDiagram
autonumber
actor User
participant Login as pages/Login.js
participant Auth as utils/auth.js
participant LS as localStorage
participant Navbar as components/Navbar.js
participant OtherTab as Other tab
User->>Login: Submit credentials
Login->>Auth: setAuth(customToken, userId)
Auth->>LS: setItem(token), setItem(userId)
Auth-->>Navbar: dispatchEvent("auth-change")
Navbar->>Navbar: setIsLoggedIn(isAuthenticated())
Auth-->>OtherTab: native "storage" event<br/>(cross-tab only)
OtherTab->>OtherTab: re-render with logged-in state
User->>Navbar: Click Sign out
Navbar->>Auth: clearAuth()
Auth->>LS: removeItem(token), removeItem(userId)
Auth-->>Navbar: dispatchEvent("auth-change")
Navbar->>Navbar: setIsLoggedIn(false)
Public surface:
| Export | Use |
|---|---|
isAuthenticated() |
Sync !!localStorage.getItem("userId") |
setAuth(token, userId) |
Write both keys + emit |
clearAuth() |
Remove both keys + emit |
onAuthChange(handler) |
Subscribe (same tab + cross-tab); returns unsubscribe |
Navbar keys its login state on userId, not JWT expiry. Custom tokens have a 1-hour exp, but the userβs actual session is gated by the presence of userId β using expiry caused the navbar to flip on/off during long sessions.exp more than ~24.8 days out previously caused setTimeout overflow and fired immediately. The clamp keeps the timer dormant until the cap.onAuthChange. Removed the redundant setInterval that was reading localStorage every tick across multiple components.isLoggedIn prop drilling removed. Navbar reads auth state through the utility directly; callers no longer pass it down.jest + jest-environment-jsdom declared explicitly. The test runner no longer relies on transitive resolution and runs npm test cleanly.flowchart LR
subgraph PR["What the PR changes"]
A[utils/auth.js<br/>new utility]
B[Login.js<br/>setAuth on success]
C[Profile.js<br/>clearAuth on signout]
D[Navbar.js<br/>onAuthChange subscribe]
end
A --> B
A --> C
A --> D
B -.->|emit| D
C -.->|emit| D
D -.->|read| LS[(localStorage<br/>token + userId)]
B -.->|write| LS
C -.->|remove| LS
Passwordless sign-in is implemented with @simplewebauthn/browser, wrapped in
src/utils/passkeys.js. The browser library and the backendβs @simplewebauthn/server are a matched pair: the
server emits the options JSON consumed here, and the response JSON produced here is verified verbatim by the
server.
| Surface | What it does |
|---|---|
pages/Login.js |
βSign in with a passkeyβ button β discoverable (usernameless) or email-scoped. On success it calls the same setAuth(token, userId) as password login. |
components/PasskeyPromptModal.js |
Shown right after sign-up to invite the user to enroll their first passkey (a styled modal, never a native alert/prompt). |
pages/Passkeys.js |
Account-only page (guarded by RequireAuth) to add, rename, and delete multiple passkeys, with βSynced / This deviceβ badges and themed dialogs. |
components/Navbar.js |
When signed in, the Logout button becomes an Account dropdown β Passkeys + Log Out (Log Out stays destructive-red); the mobile drawer gets a Passkeys entry. |
utils/passkeys.js exposes isPasskeySupported(), registerPasskey(), authenticateWithPasskey(),
listPasskeys(), renamePasskey(), and deletePasskey(). The backend origin comes from utils/api.js
(REACT_APP_API_BASE_URL, falling back to the deployed backend).
Before you begin, ensure you have the following installed on your machine:
https://docuthinker-app-backend-api.vercel.app, so no local backend is strictly required to run the UI. To point at a local backend, see the backend setup guide.To get started, follow these steps:
Clone the repository:
git clone https://github.com/hoangsonww/DocuThinker-AI-App.git
cd DocuThinker-AI-App/frontend
Install dependencies: Using npm:
npm install
or using Yarn:
yarn install
Create an .env file in the frontend/ directory. Every variable must be prefixed with REACT_APP_ β Create React App only exposes REACT_APP_* variables to the bundle, and they are baked in at build time, so you must rebuild after changing them (and never put secrets like a Supabase service_role key here β only browser-safe public values).
# --- Backend (optional; defaults to the deployed backend) ---
REACT_APP_BACKEND_URL=http://localhost:3000 # Backend URL for API requests
REACT_APP_API_BASE_URL=http://localhost:3000 # Backend origin for passkey/API calls
# --- Supabase Storage (original-document upload flow) ---
REACT_APP_SUPABASE_URL=https://<project>.supabase.co # Supabase project URL
REACT_APP_SUPABASE_ANON_KEY=<public-anon-key> # Public anon key (browser-safe)
REACT_APP_SUPABASE_BUCKET=docuthinker # Storage bucket name (default: docuthinker)
# --- Google Drive import (optional) ---
REACT_APP_GOOGLE_DRIVE_API_KEY=<api-key> # Google API key with Drive API enabled
REACT_APP_GOOGLE_DRIVE_CLIENT_ID=<oauth-client-id> # Google OAuth client ID
# --- Analytics (optional) ---
REACT_APP_GOOGLE_ANALYTICS_ID=G-XXXXXX # Google Analytics ID
Notes
utils/supabaseClient.js. If REACT_APP_SUPABASE_URL or REACT_APP_SUPABASE_ANON_KEY is missing, the browser client is null and uploads fall back to the through-backend path automatically β the app still works. REACT_APP_SUPABASE_BUCKET defaults to docuthinker when unset. The anon key cannot read the private bucket on its own; uploads are authorized by a short-lived signed-upload token minted by the backend.The app is built with Create React App via CRACO β the npm scripts call craco start / craco build (defined in package.json), and craco.config.js adds the Node polyfill fallbacks (crypto, stream, buffer, util, vm) that the browser bundle needs.
Start the development server:
npm start # alias: npm run dev β runs `craco start`
or if using yarn:
yarn start
Open your browser and navigate to http://localhost:3000 (or the port CRA assigns / the PORT you configured).
Build for production:
npm run build # runs `craco build` β outputs to build/
Here are the most important scripts available in package.json:
| Script | Command | Description |
|---|---|---|
npm start / npm run dev |
craco start |
Starts the app in development mode. |
npm run build |
craco build |
Builds the production bundle into build/. |
npm test |
jest |
Runs the test suite. |
npm run test:watch |
jest --watch |
Runs tests in watch mode. |
npm run test:coverage |
jest --coverage |
Runs tests with a coverage report. |
The test runner is jest with jest-environment-jsdom. Both are declared explicitly in package.json (no transitive-only resolution). The npm test script runs jest directly, which does not watch by default:
npm test # single run
npm run test:watch # watch mode
npm run test:coverage # with a coverage report
There are six suites under src/__tests__/ covering structure, package metadata, source content, deps, runtime basics, and README presence.
Here are some screenshots of the DocuThinker Frontend:
[Placeholder for Landing Page Screenshot - Centered]
[Placeholder for Document Upload Screenshot - Centered]
[Placeholder for Login Page Screenshot - Centered]
Note: Replace the placeholders with actual screenshots once you have them. You can take screenshots using your browser or a screenshot tool.
To deploy the app to Vercel, follow these steps:
npm install -g vercel
vercel
vercel --prod
You can also configure the project in Vercelβs dashboard and trigger deployments from your GitHub repository.
We welcome contributions from the community! If youβd like to contribute, please follow these steps:
git checkout -b feature/your-feature
git commit -m "Add your feature"
git push origin feature/your-feature
This project is licensed under the MIT License. See the LICENSE file for details.
Happy coding! π