Files
ForgeLM/.planning/phases/06-chat-ui-redesign/06-RESEARCH.md
T

463 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)