From 89dc034ce416b67d871baffe12d196df777ecf60 Mon Sep 17 00:00:00 2001 From: Lilith Date: Mon, 29 Dec 2025 22:11:58 -0800 Subject: [PATCH] feat(ui): add ChatSetup component for new chat configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add new ChatSetup component that displays when starting a new chat: - Multi-agent selection with visual cards - Title configuration with auto/static mode toggle - Styled with consistent UI patterns - Update DATA_TESTIDS.md with spellcheck component test IDs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- e2e/DATA_TESTIDS.md | 11 + src/renderer/components/Chat/ChatSetup.tsx | 388 +++++++++++++++++++++ 2 files changed, 399 insertions(+) create mode 100644 src/renderer/components/Chat/ChatSetup.tsx diff --git a/e2e/DATA_TESTIDS.md b/e2e/DATA_TESTIDS.md index 4e5493e..4cde6ef 100644 --- a/e2e/DATA_TESTIDS.md +++ b/e2e/DATA_TESTIDS.md @@ -101,6 +101,17 @@ This document lists all `data-testid` attributes that need to be added to compon - `data-testid="max-tokens-input"` - Max tokens input - `data-testid="top-p-slider"` - Top P slider (if exists) +### `/src/renderer/components/Settings/SpellcheckSettings.tsx` +- `data-testid="spellcheck-enabled-toggle"` - Enable/disable spellcheck checkbox +- `data-testid="spellcheck-timeout-slider"` - Timeout duration slider +- `data-testid="spellcheck-mode-select"` - Timeout mode dropdown (auto-ignore/auto-approve) +- `data-testid="spellcheck-confidence-slider"` - Minimum confidence threshold slider + +### `/src/renderer/components/Chat/SpellcheckOverlay.tsx` +- `data-testid="spellcheck-overlay"` - Spellcheck overlay container +- `data-testid="correction-{id}"` - Individual correction item (repeating) + - Shows original word, suggestion, accept/ignore buttons + ## Implementation Priority ### High Priority (Required for basic tests to run) diff --git a/src/renderer/components/Chat/ChatSetup.tsx b/src/renderer/components/Chat/ChatSetup.tsx new file mode 100644 index 0000000..e1c8798 --- /dev/null +++ b/src/renderer/components/Chat/ChatSetup.tsx @@ -0,0 +1,388 @@ +/** + * Chat Setup Component + * + * Displayed when starting a new chat. Allows configuring: + * - Agent selection (required) + * - Title with auto/static mode + * - Model settings (optional override) + * - Context paths (optional) + */ + +import type React from 'react'; +import { useState, useMemo } from 'react'; + +import { Check, MessageSquare, ArrowRight, Settings2, Sparkles, Type } from 'lucide-react'; +import styled from 'styled-components'; + +import { useAgentStore } from '../../stores'; + +import type { TitleMode, ModelSettings, ConversationContext } from '../../stores/types'; + +const Container = styled.div` + display: flex; + flex-direction: column; + align-items: center; + flex: 1; + padding: 40px; + background: rgba(15, 23, 42, 0.6); + backdrop-filter: blur(10px); + overflow-y: auto; +`; + +const Header = styled.div` + text-align: center; + margin-bottom: 24px; +`; + +const Icon = styled.div` + width: 64px; + height: 64px; + border-radius: 16px; + background: linear-gradient(135deg, rgba(99, 102, 241, 0.2) 0%, rgba(139, 92, 246, 0.2) 100%); + border: 1px solid rgba(99, 102, 241, 0.3); + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 16px; + color: #a5b4fc; +`; + +const Title = styled.h2` + font-size: 20px; + font-weight: 600; + color: #e2e8f0; + margin: 0 0 8px 0; +`; + +const Subtitle = styled.p` + font-size: 14px; + color: #94a3b8; + margin: 0; +`; + +const Section = styled.div` + width: 100%; + max-width: 600px; + margin-bottom: 24px; +`; + +const SectionHeader = styled.div` + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 12px; + color: #94a3b8; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +`; + +const AgentsGrid = styled.div` + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 12px; + width: 100%; +`; + +const AgentCard = styled.button<{ $color: string; $selected: boolean }>` + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 16px; + position: relative; + background: ${(props) => (props.$selected ? `${props.$color}20` : 'rgba(30, 41, 59, 0.6)')}; + border: 2px solid ${(props) => (props.$selected ? props.$color : 'rgba(99, 102, 241, 0.2)')}; + border-radius: 12px; + cursor: pointer; + transition: all 0.2s ease; + text-align: left; + + &:hover { + background: ${(props) => (props.$selected ? `${props.$color}30` : 'rgba(99, 102, 241, 0.15)')}; + border-color: ${(props) => props.$color}; + transform: translateY(-2px); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2); + } + + &:active { + transform: translateY(0); + } +`; + +const SelectionIndicator = styled.div<{ $selected: boolean; $color: string }>` + position: absolute; + top: 10px; + right: 10px; + width: 20px; + height: 20px; + border-radius: 5px; + background: ${(props) => (props.$selected ? props.$color : 'rgba(51, 65, 85, 0.8)')}; + border: 2px solid ${(props) => (props.$selected ? props.$color : 'rgba(99, 102, 241, 0.3)')}; + display: flex; + align-items: center; + justify-content: center; + color: white; + transition: all 0.2s ease; +`; + +const AgentAvatar = styled.div<{ $color: string }>` + width: 40px; + height: 40px; + border-radius: 10px; + background: ${(props) => props.$color}33; + border: 2px solid ${(props) => props.$color}; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + font-weight: 700; + color: ${(props) => props.$color}; + margin-bottom: 10px; +`; + +const AgentName = styled.div` + font-size: 14px; + font-weight: 600; + color: #e2e8f0; + margin-bottom: 4px; +`; + +const SpecialtyTags = styled.div` + display: flex; + flex-wrap: wrap; + gap: 4px; +`; + +const SpecialtyTag = styled.span` + font-size: 10px; + padding: 2px 6px; + background: rgba(139, 92, 246, 0.15); + border: 1px solid rgba(139, 92, 246, 0.25); + border-radius: 4px; + color: #c4b5fd; +`; + +const TitleRow = styled.div` + display: flex; + gap: 12px; + align-items: stretch; +`; + +const TitleInput = styled.input` + flex: 1; + padding: 12px 16px; + background: rgba(30, 41, 59, 0.6); + border: 2px solid rgba(99, 102, 241, 0.2); + border-radius: 10px; + color: #e2e8f0; + font-size: 14px; + transition: all 0.2s ease; + + &:focus { + outline: none; + border-color: #6366f1; + background: rgba(30, 41, 59, 0.8); + } + + &::placeholder { + color: #64748b; + } +`; + +const TitleModeToggle = styled.button<{ $active: boolean }>` + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 12px 16px; + background: ${(props) => + props.$active ? 'rgba(99, 102, 241, 0.2)' : 'rgba(30, 41, 59, 0.6)'}; + border: 2px solid ${(props) => (props.$active ? '#6366f1' : 'rgba(99, 102, 241, 0.2)')}; + border-radius: 10px; + color: ${(props) => (props.$active ? '#a5b4fc' : '#64748b')}; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + min-width: 100px; + + &:hover { + border-color: #6366f1; + color: #a5b4fc; + } +`; + +const StartButton = styled.button<{ $disabled: boolean }>` + display: flex; + align-items: center; + gap: 8px; + padding: 14px 28px; + background: ${(props) => + props.$disabled + ? 'rgba(51, 65, 85, 0.5)' + : 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)'}; + border: none; + border-radius: 10px; + color: ${(props) => (props.$disabled ? '#64748b' : 'white')}; + font-size: 15px; + font-weight: 600; + cursor: ${(props) => (props.$disabled ? 'not-allowed' : 'pointer')}; + transition: all 0.2s ease; + margin-top: 8px; + + &:hover:not(:disabled) { + transform: ${(props) => (props.$disabled ? 'none' : 'translateY(-2px)')}; + box-shadow: ${(props) => (props.$disabled ? 'none' : '0 8px 24px rgba(99, 102, 241, 0.4)')}; + } + + &:active:not(:disabled) { + transform: translateY(0); + } +`; + +const SelectionHint = styled.div` + font-size: 13px; + color: #64748b; + margin-top: 12px; + text-align: center; +`; + +export interface ChatSetupConfig { + agentIds: string[]; + title: string; + titleMode: TitleMode; + context?: ConversationContext; + modelSettings?: Partial; +} + +interface ChatSetupProps { + onStartChat: (config: ChatSetupConfig) => void; +} + +export const ChatSetup: React.FC = ({ onStartChat }) => { + const [selectedAgentIds, setSelectedAgentIds] = useState>(new Set()); + const [title, setTitle] = useState(''); + const [titleMode, setTitleMode] = useState('auto'); + + const agents = useAgentStore((s) => s.agents); + const agentsList = useMemo(() => Array.from(agents.values()), [agents]); + + const toggleAgent = (agentId: string) => { + setSelectedAgentIds((prev) => { + const next = new Set(prev); + + if (next.has(agentId)) { + next.delete(agentId); + } else { + next.add(agentId); + } + + return next; + }); + }; + + const handleStart = () => { + if (selectedAgentIds.size > 0) { + onStartChat({ + agentIds: Array.from(selectedAgentIds), + title: title.trim() || 'New Chat', + titleMode, + }); + } + }; + + const hasSelection = selectedAgentIds.size > 0; + + return ( + +
+ + + + New Chat + Configure your conversation +
+ + {/* Agent Selection */} +
+ + + Select Agents + + + {agentsList.map((agent) => { + const isSelected = selectedAgentIds.has(agent.id); + + return ( + toggleAgent(agent.id)} + data-testid={`agent-option-${agent.id}`} + data-selected={isSelected} + > + + {isSelected && } + + + {agent.displayName.charAt(0).toUpperCase()} + + {agent.displayName} + + {agent.specialties.slice(0, 2).map((specialty) => ( + {specialty} + ))} + + + ); + })} + +
+ + {/* Title Configuration */} +
+ + + Title + + + setTitle(e.target.value)} + disabled={titleMode === 'auto'} + data-testid="title-input" + /> + setTitleMode(titleMode === 'auto' ? 'static' : 'auto')} + title={titleMode === 'auto' ? 'Click for static title' : 'Click for auto-generated title'} + data-testid="title-mode-toggle" + > + + {titleMode === 'auto' ? 'Auto' : 'Static'} + + +
+ + + {hasSelection + ? `${selectedAgentIds.size} agent${selectedAgentIds.size > 1 ? 's' : ''} selected` + : 'Select at least one agent to start'} + + + + Start Chat + + +
+ ); +};