Feat/mobile quick actions (#282)
* feat(mobile): add quick actions button and improve mobile UX Add mobile-optimized quick action buttons and improve autocomplete UX. - Add quick command button (/) in mobile chat footer - Add tabbed autocomplete interface for Commands/Agents/Files - Fix agent selector conflict with model selector - Improve TerminalViewport Android IME support - Add reusable Slider component for settings - Update StatusChip styling for mobile layout Files changed: - ChatInput.tsx: Mobile quick actions, command menu, autocomplete tabs - AgentMentionAutocomplete.tsx: Tab support for mobile - CommandAutocomplete.tsx: Tab support and touch handling - FileMentionAutocomplete.tsx: Tab support for mobile - ModelControls.tsx: Agent selector fix - StatusChip.tsx: Mobile layout improvements - TerminalViewport.tsx: Android IME improvements - ui/slider.tsx: New reusable slider component * feat(chat): remove unused BrowserVoiceButton component from ChatInput * fix: Resolve linter errors
This commit is contained in:
@@ -3,6 +3,7 @@ import type { ComponentType } from 'react';
|
||||
import {
|
||||
RiAiAgentLine,
|
||||
RiArrowDownSLine,
|
||||
RiArrowGoBackLine,
|
||||
RiArrowRightSLine,
|
||||
RiBrainAi3Line,
|
||||
RiCheckLine,
|
||||
@@ -270,6 +271,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
currentVariant,
|
||||
currentAgentName,
|
||||
settingsDefaultVariant,
|
||||
settingsDefaultAgent,
|
||||
setProvider,
|
||||
setModel,
|
||||
setCurrentVariant,
|
||||
@@ -336,6 +338,9 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
isModelSelectorOpen,
|
||||
setModelSelectorOpen,
|
||||
} = useUIStore();
|
||||
|
||||
// Separate state for agent selector to avoid conflict with model selector
|
||||
const [isAgentSelectorOpen, setIsAgentSelectorOpen] = React.useState(false);
|
||||
const { favoriteModelsList, recentModelsList } = useModelLists();
|
||||
|
||||
const { isMobile } = useDeviceInfo();
|
||||
@@ -385,12 +390,13 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
}
|
||||
}, [activeMobilePanel]);
|
||||
|
||||
const prevAgentMenuOpenRef = React.useRef(agentMenuOpen);
|
||||
// Handle model selector close behavior (separate from agent selector)
|
||||
const prevModelSelectorOpenRef = React.useRef(isModelSelectorOpen);
|
||||
React.useEffect(() => {
|
||||
const wasOpen = prevAgentMenuOpenRef.current;
|
||||
prevAgentMenuOpenRef.current = agentMenuOpen;
|
||||
const wasOpen = prevModelSelectorOpenRef.current;
|
||||
prevModelSelectorOpenRef.current = isModelSelectorOpen;
|
||||
|
||||
if (!agentMenuOpen) {
|
||||
if (!isModelSelectorOpen) {
|
||||
setDesktopModelQuery('');
|
||||
setModelSelectedIndex(0);
|
||||
|
||||
@@ -402,13 +408,48 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [agentMenuOpen, isCompact]);
|
||||
}, [isModelSelectorOpen, isCompact]);
|
||||
|
||||
// Handle agent selector close behavior
|
||||
const [agentSearchQuery, setAgentSearchQuery] = React.useState('');
|
||||
React.useEffect(() => {
|
||||
if (!isAgentSelectorOpen) {
|
||||
setAgentSearchQuery('');
|
||||
if (!isCompact) {
|
||||
requestAnimationFrame(() => {
|
||||
const textarea = document.querySelector<HTMLTextAreaElement>('textarea[data-chat-input="true"]');
|
||||
textarea?.focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [isAgentSelectorOpen, isCompact]);
|
||||
|
||||
// Reset selected index when search query changes
|
||||
React.useEffect(() => {
|
||||
setModelSelectedIndex(0);
|
||||
}, [desktopModelQuery]);
|
||||
|
||||
const sortedAndFilteredAgents = React.useMemo(() => {
|
||||
const sorted = [...agents].sort((a, b) => a.name.localeCompare(b.name));
|
||||
if (!agentSearchQuery.trim()) {
|
||||
return sorted;
|
||||
}
|
||||
return sorted.filter((agent) =>
|
||||
fuzzyMatch(agentSearchQuery, agent.name) ||
|
||||
(agent.description && fuzzyMatch(agentSearchQuery, agent.description))
|
||||
);
|
||||
}, [agents, agentSearchQuery]);
|
||||
|
||||
const defaultAgentName = React.useMemo(() => {
|
||||
if (settingsDefaultAgent) {
|
||||
const found = agents.find(a => a.name === settingsDefaultAgent);
|
||||
if (found) return found.name;
|
||||
}
|
||||
const buildAgent = agents.find(a => a.name === 'build');
|
||||
if (buildAgent) return buildAgent.name;
|
||||
return agents[0]?.name;
|
||||
}, [settingsDefaultAgent, agents]);
|
||||
|
||||
const currentAgent = React.useMemo(() => {
|
||||
if (uiAgentName) {
|
||||
return agents.find((agent) => agent.name === uiAgentName);
|
||||
@@ -2408,7 +2449,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
return (
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Tooltip delayDuration={1000}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenu open={isAgentSelectorOpen} onOpenChange={setIsAgentSelectorOpen}>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className={cn(
|
||||
@@ -2437,29 +2478,66 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<DropdownMenuContent align="end" alignOffset={-40} className="w-[min(280px,calc(100vw-2rem))]">
|
||||
{agents.filter(agent => isPrimaryMode(agent.mode)).map((agent) => (
|
||||
<DropdownMenuItem
|
||||
key={agent.name}
|
||||
className="typography-meta"
|
||||
onSelect={() => handleAgentChange(agent.name)}
|
||||
>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className={cn(
|
||||
'h-1 w-1 rounded-full agent-dot',
|
||||
getAgentColor(agent.name).class
|
||||
)} />
|
||||
<span className="font-medium">{capitalizeAgentName(agent.name)}</span>
|
||||
<DropdownMenuContent align="end" alignOffset={-40} className="w-[min(280px,calc(100vw-2rem))] p-0 flex flex-col">
|
||||
<div className="p-2 border-b border-border/40">
|
||||
<div className="relative">
|
||||
<RiSearchLine className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search agents"
|
||||
value={agentSearchQuery}
|
||||
onChange={(e) => setAgentSearchQuery(e.target.value)}
|
||||
className="pl-8 h-8 typography-meta"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ScrollableOverlay outerClassName="max-h-[min(400px,calc(100dvh-12rem))] flex-1">
|
||||
<div className="p-1">
|
||||
{!agentSearchQuery.trim() && defaultAgentName && (
|
||||
<>
|
||||
<DropdownMenuItem
|
||||
className="typography-meta"
|
||||
onSelect={() => handleAgentChange(defaultAgentName)}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<RiArrowGoBackLine className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span className="font-medium">Reset to default</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
{sortedAndFilteredAgents.length === 0 ? (
|
||||
<div className="px-2 py-4 text-center typography-meta text-muted-foreground">
|
||||
No agents found
|
||||
</div>
|
||||
{agent.description && (
|
||||
<span className="typography-meta text-muted-foreground max-w-[200px] ml-2.5 break-words">
|
||||
{agent.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
) : (
|
||||
sortedAndFilteredAgents.map((agent) => (
|
||||
<DropdownMenuItem
|
||||
key={agent.name}
|
||||
className="typography-meta"
|
||||
onSelect={() => handleAgentChange(agent.name)}
|
||||
>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className={cn(
|
||||
'h-1 w-1 rounded-full agent-dot',
|
||||
getAgentColor(agent.name).class
|
||||
)} />
|
||||
<span className="font-medium">{capitalizeAgentName(agent.name)}</span>
|
||||
</div>
|
||||
{agent.description && (
|
||||
<span className="typography-meta text-muted-foreground max-w-[200px] ml-2.5 break-words">
|
||||
{agent.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</ScrollableOverlay>
|
||||
<DropdownMenuSeparator />
|
||||
<div className="flex flex-col gap-1 px-1 py-0.5">
|
||||
<div className="rounded-xl bg-transparent">
|
||||
|
||||
Reference in New Issue
Block a user