Developer Guide · 05
Frontend Architecture
5.1 Directory Structure
text
frontend/src/
├── main.tsx # React 19 entry (Admin SPA)
├── App.tsx # ConfigProvider + AuthProvider + ThemeProvider + StreamProvider + Router
├── router/index.tsx # BrowserRouter + three-layer ErrorBoundary
├── layouts/AppLayout.tsx # Main layout (sidebar + content area + file panel trigger)
│
├── stores/
│ ├── authContext.tsx # Authentication state
│ ├── streamContext.tsx # SSE stream state (admin)
│ ├── fileWorkspaceContext.tsx # File panel state
│ └── themeContext.tsx # Multi-theme switching + Antd ThemeConfig
│
├── services/api.ts # Unified API client
│
├── types/index.ts # Shared types (Message / MessageBlock / Subagent / etc.)
│
├── styles/
│ ├── global.css # Global styles / scrollbars / markdown
│ ├── themes.css # Multi-theme CSS variable definitions
│ └── theme.ts # Fallback JS constants (CSS variables take precedence)
│
├── utils/
│ ├── csvParse.ts # CSV/TSV state machine parser
│ ├── fileKind.ts # Extension → kind classification
│ └── timezone.ts # Timezone utilities
│
├── pages/
│ ├── Login.tsx # Split-panel login/register page
│ ├── AdminServices/index.tsx # Service management (4 tabs)
│ ├── Scheduler/index.tsx # Scheduled tasks (Admin / Service dual tabs)
│ ├── WeChat/index.tsx # Admin WeChat onboarding
│ ├── Settings/
│ │ ├── index.tsx # SettingsLayout (sidebar menu)
│ │ ├── PromptPage.tsx # System Prompt + Memory & Soul + Capability Prompts
│ │ ├── SubagentPage.tsx
│ │ ├── PackagesPage.tsx # Per-user venv
│ │ ├── InboxPage.tsx
│ │ └── GeneralPage.tsx # API Keys + timezone + theme + Advanced switches + BatchRunner embedded
│ └── Chat/
│ ├── index.tsx # Main chat page
│ ├── chat.module.css # CSS Modules (--jf-* variables)
│ ├── markdown.ts # Sole rendering pipeline (including <<FILE:>> handling)
│ ├── useSmartScroll.ts # Smart scroll hook
│ ├── types.ts # StreamBlock union types
│ └── components/
│ ├── ThinkingBlock.tsx
│ ├── ToolIndicator.tsx
│ ├── SubagentCard.tsx
│ ├── StreamingMessage.tsx
│ ├── MessageBubble.tsx
│ ├── ApprovalCard.tsx
│ ├── ImageAttachment.tsx
│ ├── VoiceInput.tsx
│ └── PlanTracker.tsx
│
├── components/
│ ├── FilePanel.tsx
│ ├── FilePreview.tsx # Multi-type file viewer
│ ├── FileTreePicker.tsx # Visual file/script selector
│ ├── HeaderControls.tsx
│ ├── LogoLoading.tsx
│ ├── SplitToggle.tsx
│ ├── ApiKeyWarning.tsx # Guidance modal when no LLM key configured
│ ├── ErrorBoundary.tsx # The only class component in the entire codebase
│ └── modals/
│ ├── BatchRunner.tsx # Embedded in GeneralPage
│ ├── SoulSettings.tsx
│ ├── SubagentManager.tsx
│ ├── SystemPromptEditor.tsx
│ └── UserProfileEditor.tsx
│
└── service-chat/ # Vite second entry (Consumer side)
├── main.tsx
├── ServiceChatApp.tsx
├── ServiceToolBadge.tsx # Friendly tool status bar (replaces admin's ToolIndicator)
├── streamHandler.ts # Lightweight SSE handler (no HITL/subagent)
├── serviceApi.ts # Consumer API (decoupled from services/api.ts)
└── serviceChat.module.css5.2 Routing System
tsx
<BrowserRouter>
<Routes>
<Route path="/login" element={<PublicRoute><Login/></PublicRoute>} />
<Route element={
<ErrorBoundary scope="app-layout">
<ProtectedRoute><AppLayout/></ProtectedRoute>
</ErrorBoundary>
}>
<Route path="/" element={
<ErrorBoundary scope="chat"><ChatPage/></ErrorBoundary>
} />
<Route path="/settings" element={
<ErrorBoundary scope="settings"><SettingsLayout/></ErrorBoundary>
}>
<Route index element={<Navigate to="/settings/prompt" replace/>} />
<Route path="prompt" element={<PromptPage/>} />
<Route path="subagents" element={<SubagentPage/>} />
<Route path="packages" element={<PackagesPage/>} />
<Route path="batch" element={<Navigate to="/settings/general" replace/>} />
<Route path="services" element={<AdminServicesPage/>} />
<Route path="scheduler" element={<SchedulerPage/>} />
<Route path="wechat" element={<WeChatPage/>} />
<Route path="inbox" element={<InboxPage/>} />
<Route path="general" element={<GeneralPage/>} />
</Route>
</Route>
<Route path="*" element={<Navigate to="/" replace/>} />
</Routes>
</BrowserRouter>ProtectedRoute: redirects to/loginif unauthenticatedPublicRoute: redirects to/if already authenticatedAppLayoutprovides sidebar navigation +<Outlet/>+ Portal node#sider-slotChatPage/SettingsLayoutusecreatePortalto inject sidebar content into#sider-slot/settings/batchredirects to/settings/general(backward compatibility)
5.3 State Management
No global state library. Uses React Context + component-local state.
| Context | File | Purpose |
|---|---|---|
authContext | stores/authContext.tsx | user / loading / login / register / logout, Token in localStorage |
streamContext | stores/streamContext.tsx | SSE stream state, blocks buffer, HITL interrupt |
fileWorkspaceContext | stores/fileWorkspaceContext.tsx | File panel state, current editing file |
themeContext | stores/themeContext.tsx | Multi-theme switching + Antd ThemeConfig |
5.4 API Client
src/services/api.ts provides a typed API client.
typescript
async function request<T>(method: string, path: string, body?: unknown): Promise<T>Auto-handles:
- Bearer Token injection (from
localStorage) - Content-Type (JSON / FormData)
- 401 auto-clears token and refreshes
- Error extraction of
detailfield
Module breakdown:
| Module | Functions |
|---|---|
| Auth | login / register / getMe |
| Conversations | list/create/get/delete + attachmentUrl |
| Chat | streamChat / resumeChat / stopChat / abortStream / checkServerStreaming |
| Files | list/read/write/edit/delete/move + uploadFiles / downloadFile / mediaUrl |
| System Prompt | CRUD + version management |
| User Profile | CRUD + version management |
| Capability Prompts | getCapabilityPrompts / updateCapabilityPrompt / resetCapabilityPrompt |
| Soul Config | getSoulConfig / updateSoulConfig |
| Subagents | list/get/add/update/delete |
| Scripts | runScript |
| Audio | transcribeAudio |
| Models | getModels |
| Batch | uploadBatchExcel / startBatchRun / listBatchTasks / getBatchTask / cancelBatchTask / batchDownloadUrl |
| Scheduler | Admin + Service task CRUD + runNow |
| Services | CRUD + Keys + WeChat channel |
| Inbox | list/get/updateStatus/delete + getUnreadCount |
| Packages | venv status + install/uninstall |
| API Keys | getApiKeys / updateApiKeys / testApiKeys / getApiKeysStatus |
| WeChat (Admin) | adminWechatQrcode / adminWechatStatus / adminWechatSession / adminWechatMessages |
| WeChat (Service) | serviceWcQrcode / serviceWcSessions / serviceWcSessionMessages |
5.5 SSE Streaming
5.5.1 Event Types
typescript
type SSEEvent =
| { type: 'token'; content: string }
| { type: 'thinking'; content: string }
| { type: 'tool_call'; name: string; args: string }
| { type: 'tool_call_chunk'; args_delta: string }
| { type: 'tool_result'; name: string; content: string }
| { type: 'subagent_call'; name: string; task: string }
| { type: 'subagent_call_chunk'; args_delta: string }
| { type: 'subagent_start'; name: string }
| { type: 'subagent_token'; content: string; agent: string }
| { type: 'subagent_thinking'; content: string; agent: string }
| { type: 'subagent_tool_call'; name: string; args: string; agent: string }
| { type: 'subagent_tool_chunk'; args_delta: string }
| { type: 'subagent_tool_result'; name: string; content: string; agent: string }
| { type: 'subagent_end'; name: string; result: string }
| { type: 'interrupt'; actions: unknown[]; configs: unknown[] }
| { type: 'done' }
| { type: 'error'; content: string }5.5.2 Performance Optimization
text
SSE callback → useRef directly mutates blocks array (avoids setState per-token)
→ requestAnimationFrame throttled re-render (~60fps)
→ Batch update to React state
→ StreamingMessage (React.memo) avoids unnecessary re-renders5.5.3 Stream Recovery
- Backend
_stream_agent/_stream_consumerinfinallyblock detects unsaved partial replies, appends⚠️ [Connection interrupted — saved generated content]then persists (_savedflag prevents duplicate saves) _active_streamsdict tracks{thread_id → {user_id, conv_id}}GET /api/chat/streaming-statusreturns current user's active streaming conversations- Chat page on load calls
checkServerStreaming()— if background streaming detected, shows yellow banner with "Abort & Save" / "Refresh Status" buttons
5.5.4 Stop Button
- Send button turns red Stop (Phosphor
Stop) during streaming output POST /api/chat/stopsetsasyncio.Eventcancel flag,_stream_agentchecks it each iteration
5.6 Chat Component Tree & Shared Rendering
5.6.1 Component Tree
text
ChatPage (pages/Chat/index.tsx)
├── Conversation list (createPortal → #sider-slot)
├── Message area
│ ├── MessageBubble (history messages)
│ │ └── BlocksRenderer (msg.blocks priority → fallback to legacy logic)
│ ├── StreamingMessage (streaming container, React.memo)
│ │ ├── ThinkingBlock (collapsible)
│ │ ├── ToolIndicator (tool calls, expandable args/result)
│ │ └── SubagentCard (subagent timeline rendering)
│ └── ApprovalCard (HITL: file diff / Plan editing)
├── Input area
│ ├── ImageAttachment (paste/drag/file select)
│ ├── VoiceInput (toggle: click start → click stop → auto-transcribe)
│ ├── Capability switches / Plan Mode / Model selector
│ └── Send/Stop button
└── useSmartScroll (pause auto-scroll when user scrolls up, resume at bottom)5.6.2 Admin / Service Shared Rendering
Components shared across admin / service (change once, both sides update):
pages/Chat/markdown.ts— sole markdown rendering pipeline, includes<<FILE:>>handling / DOMPurify / hljspages/Chat/components/StreamingMessage.tsx— shared rendering component, acceptstoolRenderer/hideSubagents/avatarSrcpropspages/Chat/types.tsStreamBlockdata structure
Rendering differences:
| Dimension | Admin | Service |
|---|---|---|
| Tool blocks | Default ToolIndicator (real tool name + args/result) | Pass toolRenderer={ServiceToolBadge} (friendly text, non-whitelisted shows "Thinking…") |
| Subagent | Shows full SubagentCard | Pass hideSubagents to hide |
| Media URL | adminMediaUrl(path) | setMediaUrlBuilder(buildConsumerMediaUrl) to use query param with service API key |
5.6.3 Message Blocks Persistence
Streaming and history messages use unified interleaved rendering:
- Backend:
save_message(blocks=...),blocksis an ordered arraytext:{"type": "text", "content": "..."}thinking:{"type": "thinking", "content": "..."}tool:{"type": "tool", "name": "...", "args": "...", "result": "...", "done": true}subagent:{"type": "subagent", "name": "...", "task": "...", "status": "done", "content": "...", "tools": [...], "timeline": [...], "done": true}
- Frontend:
MessageBubbleusesBlocksRendererwhenmsg.blocksexists, otherwise falls back to legacy logic
5.7 File Preview Panel
- Kind classification:
utils/fileKind.tsby extension →image|audio|video|pdf|markdown|html|csv|json|text|binary - openFile optimization: media/binary skips
api.readFile, directlysetEditingFile(path) - Rendering strategy:
| Kind | Rendering |
|---|---|
| image / audio / video / pdf | Native <img>/<audio>/<video>/<iframe> via mediaUrl(path), toolbar hides "Save" |
| markdown | Reuses pages/Chat/markdown.ts::renderMarkdown, class .jf-file-md-preview |
| html | <iframe sandbox="allow-scripts"> (no same-origin — allows Plotly/ECharts but blocks parent page access) |
| csv / tsv | utils/csvParse.ts state machine → antd Table (max 2000 rows) |
| json / jsonl / ndjson | JSON.parse + hljs highlighting |
| text / code | textarea editing |
| binary | Empty placeholder + download button |
- Toolbar toggle: toggle types (md/html/csv/json) get antd
Segmented"Preview/Source" in header - Download button: all kinds get download button in toolbar
5.8 Error Boundary
- Component:
components/ErrorBoundary.tsx(the only class component in the codebase — React 19 still requires class form, intentionally exempted from "avoid classes" rule) - Three-layer deployment (
router/index.tsx):scope="app-layout"wraps<AppLayout/>— catches errors in the entire protected areascope="chat"wraps<ChatPage/>— chat page crash doesn't affect sidebarscope="settings"wraps<SettingsLayout/>— settings page crash doesn't affect chat
- Interaction: friendly message + expandable error details + copy error info button (includes scope/time/URL/UA/stack/componentStack)
- Styling: Antd
Result+Collapse, background uses--jf-bg-deep
5.9 Design System & Multi-Theme
5.9.1 Multi-Theme (themes.css + themeContext.tsx)
[data-theme]attribute on<html>, controlled by ThemeProvider- Persistence:
localStorage.jf-theme - Three built-in themes:
| Theme | Style | Primary Color | Special Rules |
|---|---|---|---|
dark (default) | Warm pink-purple dark | #E89FD9 | — |
cyber-ocean | Cyan-blue light | — | — |
terminal | Phosphor-green CRT terminal | #33ff00 | Global monospace font, border-radius: 0, phosphor text-shadow, CRT scanlines, button hover invert, text-transform: uppercase |
-
Adding a new theme:
- Copy a
[data-theme]block inthemes.cssand adjust values - Add
THEMESentry + Antd ThemeConfig inthemeContext.tsx
- Copy a
-
CSS variable naming:
- Brand colors:
--jf-primary/secondary/accent/highlight/legacy - RGB triplets:
--jf-primary-rgb(forrgba(var(--jf-primary-rgb), 0.12)) - Gradients:
--jf-gradient-from/to,--jf-user-bubble-bg/shadow - Backgrounds:
--jf-bg-deep/panel/raised/code/inset - Text:
--jf-text/text-muted/text-dim/text-quaternary - Borders:
--jf-border/border-rgb/border-strong - Semantic:
--jf-success/warning/error/info - Shadows:
--jf-shadow-float/hover/brand - Diff:
--jf-diff-add-bg/del-bg/eq-text - Antd:
--jf-menu-selected-bg/select-option-bg
- Brand colors:
5.9.2 Border Radius System
| Tier | CSS Variable | Usage |
|---|---|---|
| sm | var(--jf-radius-sm) | 4px, inset corners |
| md | var(--jf-radius-md) | 8px, buttons, panels |
| lg | var(--jf-radius-lg) | 12px, cards, modals |
| bubble | var(--jf-radius-bubble) | 16px, message bubbles |
- Circular elements still use
'50%' - Inline
borderRadiusmust use variable strings:'var(--jf-radius-md)'— no hardcoding
5.9.3 Other Standards
- Icons exclusively from
@phosphor-icons/react(replaces@ant-design/icons) - Style priority: antd components > inline style (with CSS variables) > CSS Modules
- Type definitions centralized in
src/types/index.ts - API calls unified through
src/services/api.ts - Logo:
/media_resources/jellyfishlogo.png - Fonts: body Segoe UI, code JetBrains Mono (Google Fonts CDN)
5.9.4 Advanced Tab Visibility
GeneralPage.tsx"Advanced Features" card: two Switches control Prompt page's Advanced Tab visibilitylocalStoragekeys:show_advanced_system(operation rules) /show_advanced_soul(Memory & Soul)- Off by default; custom event
advanced-settings-changedfor real-time response