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.css

5.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 /login if unauthenticated
  • PublicRoute: redirects to / if already authenticated
  • AppLayout provides sidebar navigation + <Outlet/> + Portal node #sider-slot
  • ChatPage / SettingsLayout use createPortal to inject sidebar content into #sider-slot
  • /settings/batch redirects to /settings/general (backward compatibility)

5.3 State Management

No global state library. Uses React Context + component-local state.

ContextFilePurpose
authContextstores/authContext.tsxuser / loading / login / register / logout, Token in localStorage
streamContextstores/streamContext.tsxSSE stream state, blocks buffer, HITL interrupt
fileWorkspaceContextstores/fileWorkspaceContext.tsxFile panel state, current editing file
themeContextstores/themeContext.tsxMulti-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 detail field

Module breakdown:

ModuleFunctions
Authlogin / register / getMe
Conversationslist/create/get/delete + attachmentUrl
ChatstreamChat / resumeChat / stopChat / abortStream / checkServerStreaming
Fileslist/read/write/edit/delete/move + uploadFiles / downloadFile / mediaUrl
System PromptCRUD + version management
User ProfileCRUD + version management
Capability PromptsgetCapabilityPrompts / updateCapabilityPrompt / resetCapabilityPrompt
Soul ConfiggetSoulConfig / updateSoulConfig
Subagentslist/get/add/update/delete
ScriptsrunScript
AudiotranscribeAudio
ModelsgetModels
BatchuploadBatchExcel / startBatchRun / listBatchTasks / getBatchTask / cancelBatchTask / batchDownloadUrl
SchedulerAdmin + Service task CRUD + runNow
ServicesCRUD + Keys + WeChat channel
Inboxlist/get/updateStatus/delete + getUnreadCount
Packagesvenv status + install/uninstall
API KeysgetApiKeys / 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-renders

5.5.3 Stream Recovery

  • Backend _stream_agent / _stream_consumer in finally block detects unsaved partial replies, appends ⚠️ [Connection interrupted — saved generated content] then persists (_saved flag prevents duplicate saves)
  • _active_streams dict tracks {thread_id → {user_id, conv_id}}
  • GET /api/chat/streaming-status returns 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/stop sets asyncio.Event cancel flag, _stream_agent checks 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 / hljs
  • pages/Chat/components/StreamingMessage.tsx — shared rendering component, accepts toolRenderer / hideSubagents / avatarSrc props
  • pages/Chat/types.ts StreamBlock data structure

Rendering differences:

DimensionAdminService
Tool blocksDefault ToolIndicator (real tool name + args/result)Pass toolRenderer={ServiceToolBadge} (friendly text, non-whitelisted shows "Thinking…")
SubagentShows full SubagentCardPass hideSubagents to hide
Media URLadminMediaUrl(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=...), blocks is an ordered array
    • text: {"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: MessageBubble uses BlocksRenderer when msg.blocks exists, otherwise falls back to legacy logic

5.7 File Preview Panel

  • Kind classification: utils/fileKind.ts by extension → image|audio|video|pdf|markdown|html|csv|json|text|binary
  • openFile optimization: media/binary skips api.readFile, directly setEditingFile(path)
  • Rendering strategy:
KindRendering
image / audio / video / pdfNative <img>/<audio>/<video>/<iframe> via mediaUrl(path), toolbar hides "Save"
markdownReuses 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 / tsvutils/csvParse.ts state machine → antd Table (max 2000 rows)
json / jsonl / ndjsonJSON.parse + hljs highlighting
text / codetextarea editing
binaryEmpty 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 area
    • scope="chat" wraps <ChatPage/> — chat page crash doesn't affect sidebar
    • scope="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:
ThemeStylePrimary ColorSpecial Rules
dark (default)Warm pink-purple dark#E89FD9
cyber-oceanCyan-blue light
terminalPhosphor-green CRT terminal#33ff00Global monospace font, border-radius: 0, phosphor text-shadow, CRT scanlines, button hover invert, text-transform: uppercase
  • Adding a new theme:

    1. Copy a [data-theme] block in themes.css and adjust values
    2. Add THEMES entry + Antd ThemeConfig in themeContext.tsx
  • CSS variable naming:

    • Brand colors: --jf-primary/secondary/accent/highlight/legacy
    • RGB triplets: --jf-primary-rgb (for rgba(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

5.9.2 Border Radius System

TierCSS VariableUsage
smvar(--jf-radius-sm)4px, inset corners
mdvar(--jf-radius-md)8px, buttons, panels
lgvar(--jf-radius-lg)12px, cards, modals
bubblevar(--jf-radius-bubble)16px, message bubbles
  • Circular elements still use '50%'
  • Inline borderRadius must 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 visibility
  • localStorage keys: show_advanced_system (operation rules) / show_advanced_soul (Memory & Soul)
  • Off by default; custom event advanced-settings-changed for real-time response