feat(ui): add ChatSetup component for new chat configuration
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 <noreply@anthropic.com>
This commit is contained in:
parent
6be14c5eb6
commit
89dc034ce4
2 changed files with 399 additions and 0 deletions
|
|
@ -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)
|
||||
|
|
|
|||
388
src/renderer/components/Chat/ChatSetup.tsx
Normal file
388
src/renderer/components/Chat/ChatSetup.tsx
Normal file
|
|
@ -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<ModelSettings>;
|
||||
}
|
||||
|
||||
interface ChatSetupProps {
|
||||
onStartChat: (config: ChatSetupConfig) => void;
|
||||
}
|
||||
|
||||
export const ChatSetup: React.FC<ChatSetupProps> = ({ onStartChat }) => {
|
||||
const [selectedAgentIds, setSelectedAgentIds] = useState<Set<string>>(new Set());
|
||||
const [title, setTitle] = useState('');
|
||||
const [titleMode, setTitleMode] = useState<TitleMode>('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 (
|
||||
<Container data-testid="chat-setup">
|
||||
<Header>
|
||||
<Icon>
|
||||
<MessageSquare size={32} />
|
||||
</Icon>
|
||||
<Title>New Chat</Title>
|
||||
<Subtitle>Configure your conversation</Subtitle>
|
||||
</Header>
|
||||
|
||||
{/* Agent Selection */}
|
||||
<Section>
|
||||
<SectionHeader>
|
||||
<Settings2 size={14} />
|
||||
Select Agents
|
||||
</SectionHeader>
|
||||
<AgentsGrid>
|
||||
{agentsList.map((agent) => {
|
||||
const isSelected = selectedAgentIds.has(agent.id);
|
||||
|
||||
return (
|
||||
<AgentCard
|
||||
key={agent.id}
|
||||
$color={agent.color}
|
||||
$selected={isSelected}
|
||||
onClick={() => toggleAgent(agent.id)}
|
||||
data-testid={`agent-option-${agent.id}`}
|
||||
data-selected={isSelected}
|
||||
>
|
||||
<SelectionIndicator $selected={isSelected} $color={agent.color}>
|
||||
{isSelected && <Check size={12} strokeWidth={3} />}
|
||||
</SelectionIndicator>
|
||||
<AgentAvatar $color={agent.color}>
|
||||
{agent.displayName.charAt(0).toUpperCase()}
|
||||
</AgentAvatar>
|
||||
<AgentName>{agent.displayName}</AgentName>
|
||||
<SpecialtyTags>
|
||||
{agent.specialties.slice(0, 2).map((specialty) => (
|
||||
<SpecialtyTag key={specialty}>{specialty}</SpecialtyTag>
|
||||
))}
|
||||
</SpecialtyTags>
|
||||
</AgentCard>
|
||||
);
|
||||
})}
|
||||
</AgentsGrid>
|
||||
</Section>
|
||||
|
||||
{/* Title Configuration */}
|
||||
<Section>
|
||||
<SectionHeader>
|
||||
<Type size={14} />
|
||||
Title
|
||||
</SectionHeader>
|
||||
<TitleRow>
|
||||
<TitleInput
|
||||
type="text"
|
||||
placeholder={titleMode === 'auto' ? 'Auto-generated from conversation...' : 'Enter chat title...'}
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
disabled={titleMode === 'auto'}
|
||||
data-testid="title-input"
|
||||
/>
|
||||
<TitleModeToggle
|
||||
$active={titleMode === 'auto'}
|
||||
onClick={() => setTitleMode(titleMode === 'auto' ? 'static' : 'auto')}
|
||||
title={titleMode === 'auto' ? 'Click for static title' : 'Click for auto-generated title'}
|
||||
data-testid="title-mode-toggle"
|
||||
>
|
||||
<Sparkles size={14} />
|
||||
{titleMode === 'auto' ? 'Auto' : 'Static'}
|
||||
</TitleModeToggle>
|
||||
</TitleRow>
|
||||
</Section>
|
||||
|
||||
<SelectionHint>
|
||||
{hasSelection
|
||||
? `${selectedAgentIds.size} agent${selectedAgentIds.size > 1 ? 's' : ''} selected`
|
||||
: 'Select at least one agent to start'}
|
||||
</SelectionHint>
|
||||
|
||||
<StartButton
|
||||
$disabled={!hasSelection}
|
||||
onClick={handleStart}
|
||||
disabled={!hasSelection}
|
||||
data-testid="start-chat-button"
|
||||
>
|
||||
Start Chat
|
||||
<ArrowRight size={18} />
|
||||
</StartButton>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue