Sam is a React + TypeScript single-page application serving as the frontend for an AI-powered diagnostic assistant targeting Siemens S7-1500 programmable logic controllers. The interface is entirely in German, reflecting its deployment context in industrial manufacturing environments. The corresponding backend handles retrieval-augmented generation, pipeline orchestration, and incident persistence.
When a plant engineer encounters a cryptic PLC fault code during a production shift, the typical recourse — searching through hundreds of pages of Siemens technical documentation — is slow and error-prone. Sam provides precise, context-aware diagnostic guidance drawn directly from indexed technical manuals rather than generic search results.
The frontend opens a streaming diagnostic session over Server-Sent Events, visualizes a 14-stage AI inference pipeline in real time, and renders each response as sanitized Markdown with inline citation badges linking back to source documents. Every completed diagnosis is stored as a traceable incident with structured operator feedback. The system follows the Retrieval-Augmented Generation pattern — the AI grounds its responses in retrieved documentation rather than relying on parametric knowledge alone.
| Layer | Technology |
|---|---|
| UI Framework | React 19.2.4 with TypeScript 5.9.3 |
| Build Tool | Vite 7.3.1 (@vitejs/plugin-react) |
| Styling | Tailwind CSS 4.2.0 + DaisyUI 5.5.18 |
| Routing | React Router DOM 7.13.0 (lazy-loaded pages) |
| Markdown | marked 17.0.3 + dompurify 3.3.1 |
| PDF Viewer | react-pdf 10.4.1 |
| Linting | ESLint 9 + Prettier |
| Language | TypeScript 5.9.3 in strict mode |
| Category | Detail |
|---|---|
| Chat | Real-time SSE streaming with token-by-token rendering via connectSSE, 14-stage pipeline visualization through usePipelineProgress, AbortController integration for instant stream cancellation |
| Chat | Multi-turn conversation with thread_id context retention, clarification loop via connectResume on /api/diagnose/resume |
| RAG | Inline citation badges extracted by transformCitations from [Quelle: ...] patterns, source deduplication across inline and footer citations |
| RAG | DocumentDrawer with PDF preview via presigned URLs from /api/documents/:id/url, rendered with react-pdf and multi-page navigation |
| Safety | Safety-critical message detection via keyword scan (sicherheitshinweis, safety notice) in useChat, animated SafetyBanner with f-system and electrical variants |
| Sessions | Server-persisted chat sessions via /api/chat/sessions, ChatSidebar navigation, status tracking (active, resolved, documented), context retention across page reloads |
| Incidents | Automatic incident creation from completed diagnoses, feedback collection (confirmed, corrected, rejected), incident-to-chat navigation via session_id |
| Patterns | Aggregation by station, event ID, and module slot via /api/patterns/:plantId, configurable time range, PatternDashboard with frequency and temporal views |
| Auth | JWT Bearer token authentication, email verification via 6-digit code, password reset flow, ProtectedRoute and GuestRoute guards |
| Multi-Plant | Plant selector with PlantContext, optimistic profile update via updateProfile, useLocalStorage fallback for unauthenticated state |
| Performance | React.lazy + Suspense for every page component, AbortController on every async operation, useDeferredValue for resize-safe rendering |
| Resilience | ErrorBoundary wrapper on all layouts, global 401 interception with auto-redirect in api-client.ts, HealthIndicator polling backend status every 30 seconds |
The codebase follows a 5-layer numbered folder architecture where higher-numbered layers may import from lower-numbered layers but never the reverse. This enforces unidirectional dependencies and keeps domain logic isolated from presentational concerns.
graph TD
APP["App.tsx<br/>Context Providers"] --> ROUTER["routes.tsx<br/>Router Config"]
ROUTER --> PAGES["05-pages<br/>Route-level components (lazy-loaded)"]
PAGES --> LAYOUTS["04-layouts<br/>AuthLayout - MainLayout - ChatLayout"]
LAYOUTS --> FEATURES["03-features<br/>chat - incidents - patterns"]
FEATURES --> SHARED["02-shared<br/>hooks - context - utils - constants"]
SHARED --> UI["01-ui<br/>Button - Card - Modal - MarkdownContent"]
TYPES["types/<br/>Shared TypeScript Interfaces"] -.-> PAGES
TYPES -.-> FEATURES
TYPES -.-> SHARED
style APP fill:#6366f1,stroke:#4f46e5,color:#fff
style ROUTER fill:#8b5cf6,stroke:#7c3aed,color:#fff
style PAGES fill:#ec4899,stroke:#db2777,color:#fff
style LAYOUTS fill:#f59e0b,stroke:#d97706,color:#fff
style FEATURES fill:#10b981,stroke:#059669,color:#fff
style SHARED fill:#3b82f6,stroke:#2563eb,color:#fff
style UI fill:#06b6d4,stroke:#0891b2,color:#fff
style TYPES fill:#6b7280,stroke:#4b5563,color:#fff
graph LR
A([User Input<br/>ChatInput]) --> B[useChat<br/>sendMessage]
B --> C[connectDiagnose<br/>calls connectSSE]
C --> D{POST /api/diagnose<br/>SSE Stream}
D -->|metadata| E[Store thread_id<br/>and session_id]
D -->|node_transition| F[usePipelineProgress<br/>Progress Bar]
D -->|token| G[Token Accumulation<br/>in Message State]
D -->|content_replace| G
D -->|sources| H[Source Documents<br/>Array]
D -->|clarification| I{ClarificationPrompt<br/>with thread_id}
D -->|done| J[Finalize with<br/>incident_id]
G --> K[MarkdownContent<br/>marked + DOMPurify<br/>+ transformCitations]
H --> L[SourceCitations<br/>Badge Buttons]
L --> M([DocumentDrawer<br/>PDF via presigned URL])
style A fill:#6366f1,stroke:#4f46e5,color:#fff
style B fill:#3b82f6,stroke:#2563eb,color:#fff
style C fill:#3b82f6,stroke:#2563eb,color:#fff
style D fill:#f59e0b,stroke:#d97706,color:#fff
style K fill:#10b981,stroke:#059669,color:#fff
style L fill:#10b981,stroke:#059669,color:#fff
style M fill:#06b6d4,stroke:#0891b2,color:#fff
style F fill:#8b5cf6,stroke:#7c3aed,color:#fff
style I fill:#ef4444,stroke:#dc2626,color:#fff
The frontend uses a custom connectSSE function that opens a POST-based SSE connection with JWT authentication, parses event: / data: frames from the response body stream using ReadableStream, and dispatches typed handler callbacks defined in the SSEHandlers interface.
| Event | Handler | UI Action |
|---|---|---|
metadata |
onMetadata |
Stores thread_id and session_id for conversation continuity |
node_transition |
onNodeTransition |
Updates pipeline progress bar — enter adds to active set, exit moves to completed set |
token |
onToken |
Appends content string to the last assistant message for real-time token streaming |
content_replace |
onContentReplace |
Replaces entire assistant message content, used for the final formatted response |
sources |
onSources |
Stores source documents array for citation rendering in SourceCitations |
clarification |
onClarification |
Pauses streaming, shows ClarificationPrompt with thread_id for resumption via /api/diagnose/resume |
done |
onDone |
Finalizes message with incident_id, attaches sources, runs safety-critical content detection |
error |
onError |
Displays error message in ErrorMessage component, stops streaming indicator |
| Route | Page | Auth | Layout |
|---|---|---|---|
/login |
LoginPage |
GuestRoute |
AuthLayout |
/register |
RegisterPage |
GuestRoute |
AuthLayout |
/verify-email |
VerifyEmailPage |
GuestRoute |
AuthLayout |
/forgot-password |
ForgotPasswordPage |
GuestRoute |
AuthLayout |
/reset-password |
ResetPasswordPage |
GuestRoute |
AuthLayout |
/ |
Redirect to /chat |
ProtectedRoute |
MainLayout |
/incidents |
IncidentsPage |
ProtectedRoute |
MainLayout |
/incidents/:id |
IncidentDetailPage |
ProtectedRoute |
MainLayout |
/patterns |
PatternsPage |
ProtectedRoute |
MainLayout |
/chat |
ChatPage |
ProtectedRoute |
ChatLayout |
/chat/:sessionId |
ChatPage |
ProtectedRoute |
ChatLayout |
src/
├── types/ # Shared TypeScript interfaces
│ ├── api.ts # ApiError class and HTTP error types
│ ├── auth.ts # UserRead, LoginResponse, request types
│ ├── chat-sessions.ts # Session and message response types
│ ├── common.ts # Common shared types
│ ├── diagnosis.ts # SSEHandlers, ChatMessage, stream payloads
│ ├── incidents.ts # Incident, pattern, health types
│ └── index.ts # Barrel export
├── 01-ui/ # Stateless UI primitives
│ ├── badge.tsx # Status badge with variant colors
│ ├── button.tsx # DaisyUI-themed button
│ ├── card.tsx # Card, CardBody, CardTitle
│ ├── citation-transform.ts # Regex citation extraction and HTML rewrite
│ ├── icon-button.tsx # Icon-only button
│ ├── input.tsx # Styled text input
│ ├── markdown-content.tsx # marked + DOMPurify + citation transform
│ ├── modal.tsx # Dialog modal
│ ├── progress-steps.tsx # Multi-step progress indicator
│ ├── safety-banner.tsx # Safety-critical warning banner
│ ├── section-heading.tsx # Section heading component
│ ├── spinner.tsx # Loading spinner
│ ├── textarea.tsx # Styled textarea
│ └── index.ts # Barrel export
├── 02-shared/ # Cross-cutting concerns
│ ├── components/ # Reusable composed components
│ │ ├── error-boundary.tsx # Class-based error boundary with retry
│ │ ├── footer.tsx # Page footer
│ │ ├── guest-route.tsx # Auth guard for guest pages
│ │ ├── health-indicator.tsx # Polling health dot (30s interval)
│ │ ├── navbar.tsx # Top navigation bar
│ │ ├── protected-route.tsx # Auth guard for protected pages
│ │ ├── scroll-to-top.tsx # Scroll reset on route change
│ │ ├── toast-container.tsx # Toast notification display
│ │ └── index.ts
│ ├── constants/ # Application-wide constants
│ │ ├── brand.ts # APP_NAME, APP_VERSION, APP_TAGLINE
│ │ ├── endpoints.ts # All API endpoint paths
│ │ ├── pipeline-nodes.ts # 14 pipeline node keys and German labels
│ │ ├── plants.ts # Plant selector options
│ │ ├── routes.ts # Route path constants
│ │ └── index.ts
│ ├── context/ # React Context providers
│ │ ├── auth-context.tsx # JWT auth state and actions
│ │ ├── plant-context.tsx # Plant selection with optimistic update
│ │ ├── toast-context.tsx # Toast notification state
│ │ └── index.ts
│ ├── hooks/ # Generic reusable hooks
│ │ ├── use-debounce.ts # Debounced value hook
│ │ ├── use-fetch.ts # Data fetching with loading/error state
│ │ ├── use-local-storage.ts # localStorage-backed state
│ │ ├── use-scroll-position.ts # Scroll position tracking
│ │ └── index.ts
│ ├── utils/ # Utility modules
│ │ ├── api-client.ts # REST client with global 401 interception
│ │ ├── auth-service.ts # Auth API functions
│ │ ├── retry.ts # Retry utility
│ │ ├── sse-client.ts # POST-based SSE with ReadableStream
│ │ ├── token-storage.ts # JWT token persistence
│ │ └── index.ts
│ └── index.ts
├── 03-features/ # Domain feature modules
│ ├── chat/ # Chat and diagnosis feature
│ │ ├── components/ # Chat UI components
│ │ │ ├── assistant-message.tsx # Assistant message with citations
│ │ │ ├── chat-container.tsx # Main chat area with drawer
│ │ │ ├── chat-input.tsx # Message input with send/abort
│ │ │ ├── chat-sidebar.tsx # Session list sidebar
│ │ │ ├── clarification-prompt.tsx # Follow-up question prompt
│ │ │ ├── document-drawer.tsx # PDF preview with react-pdf
│ │ │ ├── error-message.tsx # Error display component
│ │ │ ├── message-list.tsx # Scrollable message container
│ │ │ ├── pipeline-progress.tsx # Pipeline stage visualization
│ │ │ ├── safety-disclaimer.tsx # Safety variant detection
│ │ │ ├── source-citations.tsx # Citation badge buttons
│ │ │ ├── streaming-indicator.tsx # Typing indicator animation
│ │ │ ├── unified-context-bar.tsx # Context metadata bar
│ │ │ ├── user-message.tsx # User message bubble
│ │ │ └── index.ts
│ │ ├── chat.service.ts # connectDiagnose, connectResume
│ │ ├── chat-sessions.service.ts # Session CRUD operations
│ │ ├── documents.service.ts # Presigned URL retrieval
│ │ ├── use-chat.ts # Core chat orchestration hook
│ │ ├── use-chat-incidents.ts # Auto-fetch incidents from messages
│ │ ├── use-chat-sessions.ts # Session list management
│ │ ├── use-pipeline-progress.ts # Pipeline node tracking
│ │ └── index.ts
│ ├── incidents/ # Incident management feature
│ │ ├── components/
│ │ │ ├── feedback-controls.tsx # Confirm/correct/reject buttons
│ │ │ ├── incident-card.tsx # Incident summary card
│ │ │ ├── incident-detail.tsx # Full incident detail view
│ │ │ ├── incident-filters.tsx # Station, event, time range filters
│ │ │ ├── incident-list.tsx # Filterable incident list
│ │ │ └── index.ts
│ │ ├── incidents.service.ts # Incident REST API calls
│ │ ├── use-incident-feedback.ts # Feedback submission hook
│ │ ├── use-incidents.ts # Incident list data hook
│ │ └── index.ts
│ ├── patterns/ # Pattern analysis feature
│ │ ├── components/
│ │ │ ├── pattern-card.tsx # Pattern summary with priority
│ │ │ ├── pattern-dashboard.tsx # Aggregation dashboard
│ │ │ ├── station-frequency.tsx # Station frequency view
│ │ │ ├── temporal-chart.tsx # Temporal distribution chart
│ │ │ └── index.ts
│ │ ├── patterns.service.ts # Pattern analysis REST calls
│ │ ├── use-patterns.ts # Pattern data hook
│ │ └── index.ts
│ └── index.ts
├── 04-layouts/ # Structural page wrappers
│ ├── auth-layout.tsx # Centered card for guest pages
│ ├── chat-layout.tsx # Full-height flex for chat
│ ├── main-layout.tsx # Navbar + Footer + ErrorBoundary
│ └── index.ts
├── 05-pages/ # Route-level page components (all lazy-loaded)
│ ├── chat-page/
│ ├── forgot-password-page/
│ ├── home-page/
│ ├── incident-detail-page/
│ ├── incidents-page/
│ ├── login-page/
│ ├── patterns-page/
│ ├── register-page/
│ ├── reset-password-page/
│ ├── verify-email-page/
│ └── index.ts
├── App.tsx # Root component with context providers
├── main.tsx # Entry point with StrictMode
├── routes.tsx # Router configuration with lazy loading
├── index.css # Tailwind + DaisyUI + custom theme
└── vite-env.d.ts # Vite type declarations
When the user types a fault description into ChatInput, the useChat hook's sendMessage function creates two optimistic ChatMessage objects — one with role "user" containing the input text and one empty with role "assistant" as a placeholder for the streamed response. The hook resets the pipeline progress state via usePipelineProgress.reset() and calls connectDiagnose, which invokes connectSSE to open a POST request to /api/diagnose. The request body carries the message, an optional session_id for session continuity, and an optional thread_id for multi-turn context. The connectSSE function attaches the JWT Bearer token from token-storage.ts in the Authorization header. An AbortController signal is threaded through the entire chain — the user can cancel a running stream at any time via the ChatInput abort button, and the hook cleans up gracefully when it catches the AbortError.
The connectSSE function reads the response body via ReadableStream.getReader() and a TextDecoder, accumulating incoming bytes into a line buffer. It splits on newlines and parses event: / data: frames according to the SSE specification — each complete frame consists of an event: line, a data: line carrying a JSON payload, and a blank line delimiter. The dispatchEvent function JSON-parses the data and calls the corresponding typed handler from the SSEHandlers interface.
The node_transition events drive the usePipelineProgress hook, which maintains two Set<string> collections — activeNodes and completedNodes. An enter status adds the node to the active set; an exit status moves it from active to completed. Progress percentage is calculated as (completedNodes.size / PIPELINE_NODE_COUNT) * 100 where PIPELINE_NODE_COUNT is the constant 14, defined in pipeline-nodes.ts. The PipelineProgress component renders each node from PIPELINE_NODE_LABELS with its German label and current status.
The token events append the content string to the last assistant message, producing a real-time token-by-token streaming effect. The content_replace event overwrites the entire message buffer — the backend uses this to deliver the final formatted response after citation enforcement and post-processing. The sources event delivers an array of SourceItem objects containing document, document_id, section, page, and relevance_score.
When the backend requires additional information, it emits a clarification event containing text, thread_id, and requires_response. The useChat hook sets the clarification state, the UI presents a ClarificationPrompt component, and the user's reply triggers connectResume — which opens a new SSE connection to /api/diagnose/resume with the stored thread_id.
On done, the MarkdownContent component processes the final response through a three-stage rendering pipeline. First, marked parses the Markdown in GFM mode with breaks: true enabled. Second, DOMPurify.sanitize strips dangerous HTML while preserving allowlisted attributes including data-citation-index. Third, transformCitations applies a regex that matches [Quelle: document, section, S. page] patterns and rewrites each match into a clickable superscript span element with a data-citation-index attribute and a numbered badge.
The stripSourcesFooter function removes the AI-generated "Quellenangaben" section from the rendered HTML because the UI replaces it with a richer SourceCitations component. That component renders numbered badge buttons for each source document, deduplicated against any citations already referenced inline. Clicking any citation badge — whether inline in the text or in the footer component — opens the DocumentDrawer. The drawer calls getDocumentUrl to fetch a presigned URL from /api/documents/:id/url and renders the target PDF page using react-pdf with multi-page navigation support.
The useChat hook also performs safety-critical content detection by scanning the final response for keywords such as sicherheitshinweis and safety notice. When detected, the message is flagged with isSafetyCritical: true, and the SafetyBanner component renders an animated warning with the appropriate variant — f-system for functional safety systems, electrical for electrical hazards, or both when both patterns are present in the response.
- Node.js >= 20
- npm >= 10
- A running instance of the Sam backend API (default:
http://localhost:8000)
# Clone the repository
git clone <repo-url>
cd s7-1500-frontend
# Install dependencies
npm install
# Configure environment
cp .env.example .env
# Edit .env -- set VITE_API_BASE_URL to the backend address
# Start development server
npm run dev
# Build for production
npm run build
# Preview production build
npm run previewThe application was developed through incremental delivery in clearly scoped phases, each submitted as a single pull request against the main branch. This approach produced a traceable development history where each PR represents an atomic capability addition — from the initial project scaffold through domain features to cross-cutting concerns like authentication and UI refinement.
The layered architecture enforces unidirectional dependencies throughout: 05-pages may import from any lower layer, but 01-ui contains zero domain logic and no imports from higher layers. Each feature module in 03-features is self-contained with its own service layer, hooks, and components, communicating with other features only through shared types and context in 02-shared. This structure enables independent development and review of each feature without cross-contamination.
| PR / Phase | Scope |
|---|---|
| PR #1 — Project Scaffold | Vite + React + TypeScript project with Tailwind CSS v4 and DaisyUI 5 |
| PR #2 — Shared Layer | UI primitives (Button, Card, Modal, Spinner), type foundations, and 02-shared utilities |
| PR #3 — Navigation and Plant Selection | Landing page, MainLayout with Navbar, routing via React Router DOM, PlantContext |
| PR #4 — Chat and Diagnosis | SSE streaming via connectSSE, useChat hook, PipelineProgress, clarification flow |
| PR #5 — Incident Memory | Incident list and detail views, feedback controls (confirmed, corrected, rejected), filters |
| PR #6 — Pattern Analysis | PatternDashboard with aggregation by station, event ID, and module slot |
| PR #7 — API Integration | Frontend-backend communication fixes across all endpoints |
| PR #8 — Authentication | JWT Bearer token auth, AuthProvider, ProtectedRoute / GuestRoute, email verification |
| PR #9 — Auth Refinement | Token handling refactor, registration form simplification |
| PR #10 — Chat Sessions | ChatSidebar with session history, context retention across reloads, incident-to-chat navigation |
| PR #11 — Citation Overhaul | transformCitations with deduplication, inline superscript badges, DocumentDrawer with PDF preview |
| Variable | Description |
|---|---|
VITE_API_BASE_URL |
Base URL of the Sam backend API. Defaults to http://localhost:8000 when not set. Available to client-side code via import.meta.env. |