docs(06): research chat UI redesign phase

This commit is contained in:
Evandro Fonseca Junior committed 2026-04-02 08:38:37 -03:00
1 parent 9a093dca15
commit 3d57acbc7d
1 file changed
+462
@@ -0,0 +1,462 @@
# 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)
```js
// 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)
```rust
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):**
```tsx
{generating && !streamReasoning && !streamContent && !streamThinkingOpen && (
<div className="msg-wrapper assistant">
<div className="thinking">
<span /><span /><span />
</div>
</div>
)}
```
**Current CSS (ChatView.css lines 770–802):**
```css
.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:
```tsx
<div className="thinking-manus">
<span className="thinking-dot" />
<span className="thinking-label">Thinking</span>
</div>
```
```css
.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 <ArrowUp>" 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)
```tsx
// 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>
)}
```
```css
.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)
```tsx
// 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)
```rust
// 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)
```js
// 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)