diff --git a/.planning/phases/06-chat-ui-redesign/06-RESEARCH.md b/.planning/phases/06-chat-ui-redesign/06-RESEARCH.md new file mode 100644 index 0000000..add8244 --- /dev/null +++ b/.planning/phases/06-chat-ui-redesign/06-RESEARCH.md @@ -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) -> 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 `
` 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 && ( +
+
+ +
+
+)} +``` + +**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 +
+ + Thinking +
+``` +```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 " in the non-hero input (line 871) and just `` 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 `` 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 && ( +
+
+
+
+)} +``` + +```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: — 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, 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, 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)