Files

21 KiB
Raw Permalink Blame History

Phase 6: Chat UI Redesign — Research

Researched: 2026-04-02 Domain: React + Tauri desktop UI, chat interface, Tailwind CSS Confidence: HIGH (all findings verified directly from source code)


Summary

The project has a mature Tauri + React desktop app (tauri-app/) with a well-factored component structure. The chat screen (ChatView.tsx, 918 lines) and the sidebar (Sidebar.tsx) are the primary targets. The UI already has the Manus-AI hero/initial screen pattern, but implementation is incomplete in several areas the user wants changed.

Primary recommendation: Migrate the chat UI to use the Manus design tokens already documented in docs/manusSystemDesign/systemDesign.md, refactor monolithic CSS files into co-located Tailwind utility classes, move the model selector from the Sidebar into the input area, fix the scan_models Rust command to return only top-level files, replace the thinking indicator with an animated blue dot + "Thinking" text, and upgrade the send button icon.

Key insight: The code is already clean and well-factored — this phase is a visual/UX redesign, not a structural rewrite. Most logic is preserved, only UI surface changes.


Standard Stack

Core (already installed — verified from package.json and imports)

Library Purpose Notes
React 18 UI framework Already in use
Tailwind CSS (with preflight disabled) Utility classes corePlugins: { preflight: false } — existing CSS vars preserved
@phosphor-icons/react Icons in ChatView ArrowUp, Image, Microphone, Paperclip, ChatCircleText
lucide-react Icons in Sidebar/Settings Plus, Settings, Trash2, etc.
@tauri-apps/api Tauri bridge invoke for Rust commands

Tailwind Config (current)

// tailwind.config.js — preflight DISABLED, CSS vars bridged
theme: {
  extend: {
    colors: {
      "c-bg": "var(--bg)",
      "c-bg2": "var(--bg2)",
      // ... etc
    }
  }
}

Key constraint: preflight: false means Tailwind utilities work but browser reset is NOT applied. Existing CSS-var-based hand-crafted styles still work. New Tailwind classes coexist with existing .css files.

Icon Strategy

  • ChatView uses @phosphor-icons/react — use this for new send button icon
  • Sidebar uses lucide-react — use this for sidebar icons
  • Send button change: Replace ArrowUp with PaperPlaneRight or ArrowCircleUp from @phosphor-icons/react

Architecture Patterns

Current Project Structure (verified)

tauri-app/src/
├── App.tsx                          # Root — owns chat state, model swap, view routing
├── App.css                          # App-level layout
├── index.css                        # :root CSS vars + @tailwind utilities
├── components/
│   ├── ChatView.tsx                 # 918 lines — MAIN TARGET
│   ├── ChatView.css                 # 1458 lines — MAIN TARGET CSS
│   ├── MessageBubble.tsx            # 237 lines — message rendering
│   ├── MessageBubble.css            # message styles
│   ├── Sidebar.tsx                  # 217 lines — MAIN TARGET
│   ├── Sidebar.css                  # sidebar styles
│   ├── Toast.tsx / Toast.css        # toast notifications
│   ├── chat-view-state.ts           # pure logic: composerNotice, suggestedPrompts
│   └── chat-view-state.test.ts      # tests
├── features/
│   ├── settings/
│   │   ├── SettingsPage.tsx         # 1200+ lines — SECONDARY TARGET
│   │   └── SettingsPage.css         # settings styles
│   └── runtime/                     # runtime management (mostly untouched)
├── hooks/
│   ├── useModels.ts                 # fetches /v1/models API
│   └── ...
└── lib/
    ├── modelAlias.ts                # Cooper/Iron/Steel alias system
    └── types.ts                    # type definitions

CSS Architecture (current pattern)

Each component has a co-located .css file using CSS custom properties from :root in index.css. Tailwind utilities (flex, truncate, w-full, etc.) are used selectively in settings but not yet in chat components. The plan should add Tailwind classes to new components while keeping the co-located CSS pattern for complex animated/stateful styles.


Component-by-Component Analysis

1. Model Listing Fix — WHERE the bug is

File: tauri-app/src-tauri/src/model_files.rs
Function: scan_model_dir_recursive (line 46–70)

fn scan_model_dir_recursive(dir: &Path, models: &mut Vec<ModelFile>) -> Result<(), String> {
    for entry in entries.flatten() {
        let path = entry.path();
        if path.is_dir() {
            // Recurses into ALL non-hidden subdirectories — THIS IS THE ISSUE
            scan_model_dir_recursive(&path, models)?;
        } else if path.is_file() && path.extension()... == "gguf" {
            models.push(...)
        }
    }
}

Problem confirmed: The Rust scanner recursively descends into all non-hidden subdirectories. When the user points models_dir at a HuggingFace cache (e.g., ~/.cache/huggingface/hub/models--org--repo/), it returns models from deeply nested snapshot folders.

Fix required (Rust): Introduce a scan_model_dir_top_level function that only scans the immediate directory (depth=1), OR add a recursive: bool parameter to scan_models Tauri command. The scan_models command should default to top-level only.

Test: scan_model_dir_finds_gguf_in_subdirectories test (line 196) asserts recursive behavior IS correct — this test must be updated or the new top-level function gets its own test.

2. Model Selector — Current Location

Current location: Sidebar.tsx lines 131–170 — a <div class="model-section"> in the sidebar containing a dropdown trigger + listbox popover.

Props flow: App.tsx → Sidebar via models, localModels, selectedModel, onModelChange, onModelSwap, isSwapping, status.

Target location (per requirements): Inside the chat input area (ChatView.tsx). The model selector should appear inside or just above the .input-box pill.

What moves:

  • The dropdown UI (trigger + popover) moves to ChatView.tsx
  • App.tsx must pass models, localModels, selectedModel, onModelChange, onModelSwap, isSwapping, status as new props to ChatView
  • The Sidebar removes the model-section block

Deduplication logic currently in Sidebar.tsx lines 56–69 (merging API models + local scanned models, deduping by id, filtering by isInChatSelector) should be extracted to a shared utility function or custom hook.

3. Hero Screen — Current State

Already implemented! ChatView.tsx has isInitialScreen = chatId === null && messages.length === 0 which conditionally renders a full hero section (lines 604–706) with:

  • Kicker pill ("ForgeLM workspace")
  • H1 "What can I do for you?" (serif font)
  • Hero composer block (large input box)
  • Starter chip rail
  • Hero card carousel

What's missing vs. Manus design:

  • Model selector not in the hero input area
  • The hero background is #252525 (custom) not the Manus #171717 — close but not aligned
  • "Thinking" indicator in hero is three dots, not Manus style

4. Thinking Indicator — Current vs. Target

Current (ChatView.tsx lines 763–769):

{generating && !streamReasoning && !streamContent && !streamThinkingOpen && (
  <div className="msg-wrapper assistant">
    <div className="thinking">
      <span /><span /><span />
    </div>
  </div>
)}

Current CSS (ChatView.css lines 770–802):

.thinking span {
  width: 8px; height: 8px;
  background: #9fd0ff;
  border-radius: 50%;
  animation: thinking-bounce 1.4s ease-in-out infinite;
}

→ Three bouncing light-blue dots.

Target (Manus style):

  • Single animated pulsing blue dot (#3B82F6)
  • "Thinking" text inline
  • Pattern: [● pulsing] Thinking

Implementation: Replace the three-span structure with a single dot + text:

<div className="thinking-manus">
  <span className="thinking-dot" />
  <span className="thinking-label">Thinking</span>
</div>
.thinking-dot {
  width: 8px; height: 8px;
  background: #3B82F6;
  border-radius: 50%;
  animation: thinking-pulse 1.5s ease-in-out infinite;
}
@keyframes thinking-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.4; transform: scale(0.75); }
}

5. Send Button — Current

Current: ArrowUp from @phosphor-icons/react, size={18}, weight="duotone" inside a round white button (.send-btn).

The button says "Send " in the non-hero input (line 871) and just <ArrowUp> in the hero input (line 657).

Target: A different icon. Best options from @phosphor-icons/react (already installed):

  • PaperPlaneRight (paper plane) — most chat-idiomatic
  • ArrowCircleRight
  • NavigationArrow

Recommendation: PaperPlaneRight from @phosphor-icons/react, size={18}, weight="fill". Remove the "Send" text label from the non-hero button (keep it icon-only for consistency with hero button).

6. Settings Screen — Current State

SettingsPage.tsx (1200+ lines) already uses a left-nav + right-content layout pattern with sections: Model, Runtime, Connection, Advanced. The CSS uses sp-* BEM-style classes. It already follows a clean pattern.

Manus design alignment needed:

  • sp-nav left sidebar should use Manus tokens: bg-[#1E1E1E], border-[#262626]
  • sp-card elements should use bg-[#1E1E1E] surface, border-[#262626]
  • Section titles should use text-[#F3F4F6], descriptions text-[#9CA3AF]
  • Currently SettingsPage.css uses Tailwind inline utility classes (verified in the model search area, lines 571–644)

7. Message Space Utilization

Current: messages-container has max-width: 1200px for the scroll area, but msg-wrapper.assistant is max-width: 100% and msg-wrapper.user is max-width: 800px. Input is constrained to max-width: 800px.

Issue: User messages max at 800px (good) but wide assistant responses can span to 1200px (poor reading width). Also the input area and messages aren't visually aligned.

Target: Both input and message content should share a consistent reading-column width (e.g., max-width: 768px or 800px). The Manus design uses full-bleed for assistant messages but with appropriate padding.


Don't Hand-Roll

Problem Don't Build Use Instead
Dropdown/popover positioning Custom CSS position math Use position: absolute + the existing pattern already in Sidebar's model-dropdown-menu
Icon set SVG sprites or custom SVGs @phosphor-icons/react (already installed)
Animation timing Custom JS timing CSS @keyframes with animation
Model deduplication New logic Extract Sidebar.tsx lines 56–69 into lib/modelUtils.ts

Runtime State Inventory

N/A — this is a UI redesign phase with no renames, no string replacements, and no data migrations. No runtime state is affected.

Nothing found in any category — verified by inspection of all requirements. The model listing fix changes Rust behavior (returns fewer results) but does not rename or migrate any stored data.


Common Pitfalls

Pitfall 1: Tailwind + CSS-var Conflict

What goes wrong: Writing bg-[#171717] in JSX when the existing .chat-view.initial-screen CSS rule sets background: #252525. The CSS rule wins due to specificity. Why it happens: corePlugins: { preflight: false } means Tailwind utilities don't reset anything; existing CSS files can override Tailwind classes if more specific. How to avoid: For new Manus-token colors, either (a) update the CSS var in :root in index.css and use bg-c-bg, or (b) use !bg-[#171717] (important modifier) carefully. Warning signs: Colors look wrong despite correct class on the element.

Pitfall 2: Model Selector Prop Drilling

What goes wrong: Moving the model selector from Sidebar to ChatView requires passing models, localModels, selectedModel, onModelChange, onModelSwap, isSwapping, status into ChatView — which becomes a massive props list. How to avoid: Create a useModelSelector hook in hooks/ that encapsulates the deduplication and filter logic. Pass only the minimal necessary props to ChatView.

Pitfall 3: Dual Textarea Refs

What goes wrong: ChatView has textareaRef used by both the hero input and the non-hero input. They are the same ref but only one is mounted at a time. If both render simultaneously (e.g., during state transitions), the ref points to the second one. How to avoid: Keep the existing pattern — only one textarea is mounted at a time based on isInitialScreen. Don't change this logic.

Pitfall 4: scan_models Test Break

What goes wrong: scan_model_dir_finds_gguf_in_subdirectories test (line 196, model_files.rs) explicitly asserts that nested models ARE found. Changing to top-level-only will break this test. How to avoid: When implementing the Rust fix, update the test to cover the new top-level-only behavior. Add a separate test for "nested models are NOT returned by default."

Pitfall 5: CSS Monolith Partial Migration

What goes wrong: Partially migrating ChatView.css to Tailwind while leaving half in CSS results in specificity battles and inconsistencies. How to avoid: Follow the project's existing pattern: keep co-located .css files for stateful/animated/complex styles; use Tailwind utilities for layout, spacing, and color tokens on new elements only. Don't wholesale migrate existing CSS.

Pitfall 6: Model Selector in Input vs. Hero

What goes wrong: The model selector needs to appear in BOTH the hero input AND the regular input area. These are two separate JSX blocks in ChatView. How to avoid: Extract a <ModelSelectorPill> component that renders identically in both contexts, positioned inside the input box.


Code Examples

Manus Thinking Indicator (target pattern)

// Source: systemDesign.md + verified against current thinking-bounce pattern
// Replace the 3-dot bounce with a single pulsing dot + text

{generating && !streamReasoning && !streamContent && !streamThinkingOpen && (
  <div className="msg-wrapper assistant">
    <div className="thinking-manus">
      <span className="thinking-dot" aria-hidden="true" />
      <span className="thinking-label">Thinking</span>
    </div>
  </div>
)}
.thinking-manus {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 0;
}
.thinking-dot {
  width: 8px;
  height: 8px;
  background: #3B82F6;
  border-radius: 50%;
  flex-shrink: 0;
  animation: thinking-pulse 1.5s ease-in-out infinite;
}
.thinking-label {
  font-size: 0.875rem;
  color: #9CA3AF;
  font-weight: 400;
}
@keyframes thinking-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.35; transform: scale(0.7); }
}

Model Selector in Input Area (sketch)

// New: <ModelSelectorPill> — inline in input box
// position: inside .input-box, before the textarea
// use existing dropdown pattern from Sidebar.tsx lines 136-168

interface ModelSelectorPillProps {
  models: ModelInfo[];
  localModels: RuntimeModelFile[];
  selectedModel: string;
  onModelChange: (id: string) => void;
  onModelSwap?: (path: string) => void;
  isSwapping?: boolean;
}

Rust Top-Level Scan (sketch)

// New function alongside existing scan_model_dir_recursive
fn scan_model_dir_top_level(path: &Path) -> Result<Vec<ModelFile>, String> {
    let mut models = Vec::new();
    let entries = std::fs::read_dir(path).map_err(|e| e.to_string())?;
    for entry in entries.flatten() {
        let p = entry.path();
        if p.is_file() 
            && p.extension().map(|e| e.eq_ignore_ascii_case("gguf")).unwrap_or(false) {
            models.push(model_file_from_path(&p));
        }
    }
    models.sort_by(|a, b| a.name.cmp(&b.name));
    Ok(models)
}

// Update scan_models command to use top-level scan:
#[tauri::command]
pub fn scan_models(dir: String) -> Result<Vec<ModelFile>, String> {
    let path = resolve_models_dir(if dir.trim().is_empty() { None } else { Some(&dir) });
    scan_model_dir_top_level(&path)
}

Manus Design Tokens — Tailwind Config Extension (proposed)

// tailwind.config.js — extend with Manus tokens as named colors
theme: {
  extend: {
    colors: {
      // Existing CSS-var bridges
      "c-bg": "var(--bg)",
      // ... existing entries ...
      
      // NEW: Manus design tokens (can use directly)
      "manus-base":     "#171717",
      "manus-surface":  "#1E1E1E",
      "manus-elevated": "#262626",
      "manus-hover":    "#333333",
      "manus-tx":       "#F3F4F6",
      "manus-tx2":      "#9CA3AF",
      "manus-tx3":      "#52525B",
      "manus-blue":     "#3B82F6",
    }
  }
}

State of the Art

Area Current Approach Target Approach Impact
Thinking indicator 3 bouncing dots (.thinking span) Single pulsing dot + "Thinking" text Matches Manus AI style
Model selector location Sidebar dropdown Input area pill User requirement
Manus tokens Custom CSS vars (--bg: #0d1115) Manus tokens (#171717, #1E1E1E) Visual alignment
Model scan depth Recursive (all subdirectories) Top-level only Bug fix
Send button ArrowUp + "Send" text PaperPlaneRight icon only Better UX
Input width 800px (input) / 1200px (messages) Aligned reading column Better space use

Environment Availability

Step 2.6: SKIPPED — this is a UI code change phase. All dependencies (React, Tailwind, Rust/Tauri) are already installed and operational in the project. No external services or new CLI tools required.


Validation Architecture

nyquist_validation is false in .planning/config.json — skipping this section.


Open Questions

  1. Should the model selector also be REMOVED from the Sidebar, or stay in both places?

    • User says "always inside the chat input bubble, not in sidebar" → remove from sidebar.
    • Implication: The sidebar model-section block is deleted entirely.
    • Recommendation: Remove from sidebar. The model-section CSS class and HTML block in Sidebar.tsx lines 131–170 can be deleted.
  2. Top-level scan vs. recursive scan for Settings page Model section

    • The Settings → Model section shows scanned models for swap selection. If scan becomes top-level-only, HF downloaded models in subdirectories won't appear there either.
    • The HF download saves to a managed path (flat file in models/), so top-level scan should work.
    • Recommendation: Make top-level the default for scan_models command. Keep recursive scan available as a separate function for future use.
  3. Manus #171717 vs. current #0d1115 base color

    • Current: --bg: #0d1115 (very dark navy-black)
    • Manus: #171717 (neutral dark gray)
    • Changing --bg in index.css would affect the ENTIRE app. The hero screen already uses #252525 as an override.
    • Recommendation: Update the CSS vars in index.css to align with Manus tokens as part of this phase. This is the right time — it's a design phase.
  4. Settings page redesign scope

    • User says "same Manus system design" for Settings.
    • Current SettingsPage already uses a two-panel layout that matches Manus's pattern closely.
    • Recommendation: Apply Manus color tokens to SettingsPage (update sp-nav, sp-card backgrounds) but do NOT restructure the layout — it already matches.

Sources

Primary (HIGH confidence)

  • Direct source read: tauri-app/src/components/ChatView.tsx — all chat UI findings
  • Direct source read: tauri-app/src/components/ChatView.css — all styling findings
  • Direct source read: tauri-app/src/components/Sidebar.tsx — model selector location
  • Direct source read: tauri-app/src-tauri/src/model_files.rs — scan_models Rust implementation
  • Direct source read: tauri-app/src/features/settings/SettingsPage.tsx — settings structure
  • Direct source read: tauri-app/tailwind.config.js — Tailwind configuration
  • Direct source read: tauri-app/src/index.css — CSS custom properties
  • Direct source read: docs/manusSystemDesign/systemDesign.md — Manus design tokens

Secondary (MEDIUM confidence)

  • @phosphor-icons/react icon names: verified from existing imports in ChatView.tsx

Metadata

Confidence breakdown:

  • Model listing bug: HIGH — read the Rust source directly; recursive call confirmed at line 58
  • Model selector location: HIGH — read Sidebar.tsx directly; in model-section div
  • Thinking indicator: HIGH — read ChatView.tsx and ChatView.css directly
  • Settings screen: HIGH — read SettingsPage.tsx directly
  • Component structure: HIGH — read all relevant files

Research date: 2026-04-02 Valid until: 2026-05-02 (stable codebase, no fast-moving deps)