TaskCommandBar.tsx32.9 KBView on GitHub 'use client';
import type { HydratedUserTask, TaskDateKey } from '@/modules/userTasks/slice/userTasksSlice';
import { generateSimpleDateSuggestions, type DateSuggestion } from '@/modules/crm/utils/time';
import Container3D from '@/modules/cedar-os/src/cedar-os-components/containers/Container3D';
import { Command, CommandGroup, CommandItem, CommandList } from '@/components/ui/command';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { Calendar, Plus, Building2, User } from 'lucide-react';
import { AnimatePresence, motion } from 'motion/react';
import { format, isToday, isTomorrow } from 'date-fns';
import { EnterKey } from '@/components/ui/enter-key';
import { useTRPC } from '@/providers/query-provider';
import { useCedarStore } from '@/modules/store';
import useMeasure from 'react-use-measure';
import { v4 as uuidv4 } from 'uuid';
import { cn } from '@/lib/utils';
import { toast } from 'sonner';
/**
* Get the date key for a given date relative to today
* Matches the logic in use-tasks-view.ts
*/
function getDateKeyForDate(date: Date): TaskDateKey {
const today = new Date();
today.setHours(0, 0, 0, 0);
const dateStart = new Date(date);
dateStart.setHours(0, 0, 0, 0);
const diffDays = Math.round((dateStart.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
if (diffDays < -3) return 'previous';
if (diffDays === -3) return '-3';
if (diffDays === -2) return '-2';
if (diffDays === -1) return '-1';
if (diffDays === 0) return 'today';
if (diffDays === 1) return '+1';
if (diffDays === 2) return '+2';
if (diffDays === 3) return '+3';
return 'future';
}
interface TaskCommandBarProps {
className?: string;
/**
* When provided, the command bar will be open by default with this conversation prefilled.
* After entering description, it will skip directly to the date step.
*/
prefilledConversation?: {
id: string;
name: string;
companyName: string | null;
};
/**
* Callback when the command bar is closed
*/
onClose?: () => void;
/**
* Whether to render as an inline component (no floating button)
*/
inline?: boolean;
}
/**
* Format date for display in the command bar
*/
function formatDateDisplay(date: Date): string {
if (isToday(date)) {
return 'Today';
}
if (isTomorrow(date)) {
return 'Tomorrow';
}
return format(date, 'EEE, MMM d');
}
/**
* Conversation search result type
*/
interface ConversationResult {
id: string;
name: string;
companyName: string | null;
}
/**
* Selected conversation for task
*/
interface SelectedConversation {
id: string;
name: string;
companyName: string | null;
}
/**
* Command bar step type
*/
type CommandBarStep = 'description' | 'conversation' | 'date';
export function TaskCommandBar({
className,
prefilledConversation,
onClose: onCloseProp,
inline = false,
}: TaskCommandBarProps) {
const trpc = useTRPC();
const queryClient = useQueryClient();
// State - open by default if prefilled conversation is provided
const [isOpen, setIsOpen] = useState(!!prefilledConversation);
const [currentStep, setCurrentStep] = useState<CommandBarStep>('description');
const [description, setDescription] = useState('');
const [conversationInput, setConversationInput] = useState(prefilledConversation?.name || '');
const [selectedConversation, setSelectedConversation] = useState<SelectedConversation | null>(
prefilledConversation || null,
);
const [selectedConversationIndex, setSelectedConversationIndex] = useState(0);
const [dateInput, setDateInput] = useState('');
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
const [suggestions, setSuggestions] = useState<DateSuggestion[]>([]);
const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(0);
// Track if we should skip conversation step (when prefilled)
const skipConversationStep = !!prefilledConversation;
// Refs
const descriptionInputRef = useRef<HTMLInputElement>(null);
const conversationInputRef = useRef<HTMLInputElement>(null);
const dateInputRef = useRef<HTMLInputElement>(null);
// Measure refs for smooth height animations
const [conversationMeasureRef, conversationBounds] = useMeasure();
const [dateMeasureRef, dateBounds] = useMeasure();
// Store actions
const setTasks = useCedarStore((state) => state.setTasks);
const removeTask = useCedarStore((state) => state.removeTask);
const taskIdsByDateKey=[redacted] => state.taskIdsByDateKey);
const setTaskIdsByDateKey=[redacted] => state.setTaskIdsByDateKey);
// Track optimistic task ID and date key for cleanup
const optimisticTaskIdRef = useRef<string | null>(null);
const optimisticDateKeyRef = useRef<TaskDateKey | null>(null);
// Debounced conversation search query
const [debouncedConversationQuery, setDebouncedConversationQuery] = useState('');
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedConversationQuery(conversationInput);
}, 200);
return () => clearTimeout(timer);
}, [conversationInput]);
// Which deal the task belongs to. `dealsOnly` for the same reason NewTaskDialog passes it: an
// unfiltered search answers with every auto-created correspondent conversation, which is not
// somewhere a task is ever filed.
const conversationSearchQuery = useQuery(
trpc.crm.searchConversationsMinimal.queryOptions(
{ query: debouncedConversationQuery, limit: 10, dealsOnly: true },
{
enabled: currentStep === 'conversation' && debouncedConversationQuery.length >= 1,
staleTime: 30000, // Cache for 30 seconds
},
),
);
const conversationResults = useMemo(() => {
return conversationSearchQuery.data?.conversations || [];
}, [conversationSearchQuery.data?.conversations]);
// Reset selected index when results change
useEffect(() => {
setSelectedConversationIndex(0);
}, [conversationResults]);
/**
* Add a task ID to the appropriate date bucket
*/
const addTaskToDateBucket = useCallback(
(taskId: string, dueDate: Date) => {
const dateKey=[redacted];
optimisticDateKeyRef.current = dateKey;
// Get current bucket and add the new task ID
const currentBucket = taskIdsByDateKey[dateKey] || [];
if (!currentBucket.includes(taskId)) {
setTaskIdsByDateKey({
...taskIdsByDateKey,
[dateKey]: [...currentBucket, taskId],
});
}
},
[taskIdsByDateKey, setTaskIdsByDateKey],
);
/**
* Remove a task ID from a date bucket
*/
const removeTaskFromDateBucket = useCallback(
(taskId: string, dateKey=[redacted] => {
const currentBucket = taskIdsByDateKey[dateKey] || [];
const updatedBucket = currentBucket.filter((id) => id !== taskId);
setTaskIdsByDateKey({
...taskIdsByDateKey,
[dateKey]: updatedBucket,
});
},
[taskIdsByDateKey, setTaskIdsByDateKey],
);
// Create task mutation with optimistic updates
const createTaskMutation = useMutation(
trpc.userTasks.createStandaloneTask.mutationOptions({
onSuccess: (data) => {
// Remove the optimistic task from store and date bucket
if (optimisticTaskIdRef.current && optimisticDateKeyRef.current) {
removeTask(optimisticTaskIdRef.current);
removeTaskFromDateBucket(optimisticTaskIdRef.current, optimisticDateKeyRef.current);
}
// Add the real task from server
if (data.task) {
const realTask: HydratedUserTask = {
...data.task,
} as HydratedUserTask;
// Add to tasks store
setTasks({ [data.task.id]: realTask });
// Add to the correct date bucket
addTaskToDateBucket(data.task.id, realTask.dueDate);
}
// Clear refs
optimisticTaskIdRef.current = null;
optimisticDateKeyRef.current = null;
// Invalidate queries to refetch and sync
queryClient.invalidateQueries({ queryKey: [['userTasks']] });
},
onError: (error) => {
toast.error('Failed to create task');
console.error('Failed to create task:', error);
// Remove the optimistic task on error
if (optimisticTaskIdRef.current && optimisticDateKeyRef.current) {
removeTask(optimisticTaskIdRef.current);
removeTaskFromDateBucket(optimisticTaskIdRef.current, optimisticDateKeyRef.current);
}
// Clear refs
optimisticTaskIdRef.current = null;
optimisticDateKeyRef.current = null;
},
}),
);
// Helper to normalize dates to 00:01 so "Today" shows up in current tasks
const normalizeDateSuggestions = useCallback((rawSuggestions: DateSuggestion[]) => {
return rawSuggestions.map((suggestion) => {
const normalizedDate = new Date(suggestion.date);
normalizedDate.setHours(0, 1, 0, 0); // 00:01:00.000
return { ...suggestion, date: normalizedDate };
});
}, []);
// Update date suggestions when input changes or when expanded
useEffect(() => {
if (currentStep === 'date') {
const rawSuggestions = generateSimpleDateSuggestions(dateInput);
const newSuggestions = normalizeDateSuggestions(rawSuggestions);
setSuggestions(newSuggestions);
setSelectedSuggestionIndex(0);
// Auto-select first parsed date if available
if (newSuggestions.length > 0 && dateInput.trim()) {
setSelectedDate(newSuggestions[0].date);
}
}
}, [dateInput, currentStep, normalizeDateSuggestions]);
// Initialize suggestions when entering date step
useEffect(() => {
if (currentStep === 'date') {
setSuggestions(normalizeDateSuggestions(generateSimpleDateSuggestions('')));
}
}, [currentStep, normalizeDateSuggestions]);
// Define handleClose before useEffect that depends on it
const handleClose = useCallback(() => {
setIsOpen(false);
setCurrentStep('description');
setDescription('');
setConversationInput(prefilledConversation?.name || '');
setSelectedConversation(prefilledConversation || null);
setDateInput('');
setSelectedDate(new Date());
onCloseProp?.();
}, [prefilledConversation, onCloseProp]);
// Handle Cmd+D to open/focus
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'd') {
// Don't intercept if user is typing in a TipTap/contenteditable editor
const activeElement = document.activeElement;
const isInEditor =
activeElement?.closest('.ProseMirror') ||
activeElement?.getAttribute('contenteditable') === 'true';
if (isInEditor) {
return; // Let the editor handle Cmd+K
}
e.preventDefault();
e.stopPropagation();
if (!isOpen) {
setIsOpen(true);
} else {
descriptionInputRef.current?.focus();
}
}
// Escape to close or go back
if (e.key === 'Escape' && isOpen) {
e.preventDefault();
if (currentStep === 'date') {
// Go back to conversation step
setCurrentStep('conversation');
setDateInput('');
setTimeout(() => conversationInputRef.current?.focus(), 100);
} else if (currentStep === 'conversation') {
// Go back to description step
setCurrentStep('description');
setConversationInput('');
setSelectedConversation(null);
setTimeout(() => descriptionInputRef.current?.focus(), 100);
} else {
handleClose();
}
}
};
window.addEventListener('keydown', handleKeyDown, { capture: true });
return () => {
window.removeEventListener('keydown', handleKeyDown, { capture: true });
};
}, [isOpen, currentStep, handleClose]);
// Focus input when step changes
useEffect(() => {
if (isOpen) {
setTimeout(() => {
if (currentStep === 'description') {
descriptionInputRef.current?.focus();
} else if (currentStep === 'conversation') {
conversationInputRef.current?.focus();
} else if (currentStep === 'date') {
dateInputRef.current?.focus();
}
}, 100);
}
}, [isOpen, currentStep]);
const handleDescriptionKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && description.trim()) {
e.preventDefault();
// Skip to date step if conversation is prefilled, otherwise go to conversation step
if (skipConversationStep) {
setCurrentStep('date');
} else {
setCurrentStep('conversation');
}
}
},
[description, skipConversationStep],
);
const handleConversationSelect = useCallback((conversation: ConversationResult) => {
setSelectedConversation(conversation);
setConversationInput(conversation.name);
// Move to date step
setCurrentStep('date');
}, []);
const handleSkipConversation = useCallback(() => {
setSelectedConversation(null);
setCurrentStep('date');
}, []);
const handleConversationKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
if (conversationResults.length > 0) {
// Select the highlighted conversation
handleConversationSelect(conversationResults[selectedConversationIndex]);
} else if (conversationInput.trim() === '') {
// Skip if empty
handleSkipConversation();
}
} else if (e.key === 'ArrowDown') {
e.preventDefault();
setSelectedConversationIndex((prev) => Math.min(prev + 1, conversationResults.length - 1));
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setSelectedConversationIndex((prev) => Math.max(prev - 1, 0));
} else if (e.key === 'Backspace' && !conversationInput) {
// Go back to description input
e.preventDefault();
setCurrentStep('description');
setTimeout(() => descriptionInputRef.current?.focus(), 100);
} else if (e.key === 'Tab' && !e.shiftKey) {
// Tab to skip conversation and go to date
e.preventDefault();
handleSkipConversation();
}
},
[
conversationResults,
selectedConversationIndex,
conversationInput,
handleConversationSelect,
handleSkipConversation,
],
);
const handleCreateTask = useCallback(
(overrideDate?: Date) => {
if (!description.trim()) {
toast.error('Please enter a task description');
return;
}
// Use override date if provided (for immediate selection), otherwise use state
const taskDueDate = overrideDate || selectedDate;
// Create optimistic task with a proper structure (using new schema fields)
const optimisticTaskId = uuidv4();
const now = new Date();
const optimisticTask: HydratedUserTask = {
id: optimisticTaskId,
userId: 'optimistic', // Placeholder - will be replaced by server
conversationId: selectedConversation?.id || 'optimistic', // Use selected or placeholder
creationRunId: null, // Reference to execution that CREATED this task
executionRunId: null, // Reference to execution that EXECUTED this task
// A user typed this task; it did not come from a note marker.
sourceDocumentId: null,
sourceMarkerId: null,
// Status
status: 'todo',
// Obligation tags are agent-written at create time; a user-created task carries none.
tags: [],
isRead: true, // Mark as read since user just created it
// Core fields
description: description.trim(),
notes: null,
dueDate: taskDueDate,
// New schema fields
taskChannel: 'email', // Default channel for standalone tasks
taskType: 'manual',
taskCreatedBy: 'user', // User-created task
taskActionData: null,
// Output axis — bare intent; nothing is produced until the task is worked.
taskOutput: { kind: 'email' },
// Agent execution
agentExecutionEnabled: false,
// Timestamps
createdAt: now,
updatedAt: now,
completedAt: null,
// Board position. The server places a new task by its due date (see
// user_tasks_sort_order.sql); 0 is a placeholder that lives only until the real row
// arrives, and never pins — the user has not dragged this anywhere.
sortOrder: 0,
sortOrderPinned: false,
// Hydrated fields
taskGroupId: null,
chatThreadId: null,
// The email thread the task is about — an optimistic row created from the
// command bar has no thread in hand.
sourceThreadId: null,
conversation: selectedConversation ? { name: selectedConversation.name } : null,
};
// Store the optimistic task ID for cleanup in mutation callbacks
optimisticTaskIdRef.current = optimisticTaskId;
// Add optimistic task to the tasks store
setTasks({ [optimisticTaskId]: optimisticTask });
// Add to the appropriate date bucket for immediate UI update
addTaskToDateBucket(optimisticTaskId, taskDueDate);
// Call mutation
createTaskMutation.mutate({
description: description.trim(),
dueDate: taskDueDate.toISOString(),
conversationId: selectedConversation?.id,
});
// Close and reset
handleClose();
},
[
description,
selectedDate,
selectedConversation,
setTasks,
addTaskToDateBucket,
createTaskMutation,
handleClose,
],
);
const handleDateKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
// Create the task with currently selected date
handleCreateTask(selectedDate);
} else if (e.key === 'ArrowDown') {
e.preventDefault();
setSelectedSuggestionIndex((prev) => Math.min(prev + 1, suggestions.length - 1));
if (suggestions[Math.min(selectedSuggestionIndex + 1, suggestions.length - 1)]) {
setSelectedDate(
suggestions[Math.min(selectedSuggestionIndex + 1, suggestions.length - 1)].date,
);
}
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setSelectedSuggestionIndex((prev) => Math.max(prev - 1, 0));
if (suggestions[Math.max(selectedSuggestionIndex - 1, 0)]) {
setSelectedDate(suggestions[Math.max(selectedSuggestionIndex - 1, 0)].date);
}
} else if (e.key === 'Backspace' && !dateInput) {
// Go back to conversation input (or description if skipping conversation)
e.preventDefault();
if (skipConversationStep) {
setCurrentStep('description');
setTimeout(() => descriptionInputRef.current?.focus(), 100);
} else {
setCurrentStep('conversation');
setTimeout(() => conversationInputRef.current?.focus(), 100);
}
}
},
[
suggestions,
selectedSuggestionIndex,
dateInput,
selectedDate,
handleCreateTask,
skipConversationStep,
],
);
const handleSuggestionSelect = useCallback(
(suggestion: DateSuggestion) => {
// Create task immediately with the selected date
handleCreateTask(suggestion.date);
},
[handleCreateTask],
);
const handleConversationButtonClick = useCallback(() => {
// Allow clicking to switch to conversation step from any step (not just description)
if (description.trim()) {
setCurrentStep('conversation');
}
}, [description]);
const handleDescriptionButtonClick = useCallback(() => {
// Allow clicking to switch back to description step from any step
setCurrentStep('description');
}, []);
const handleDateSectionClick = useCallback(() => {
// Allow clicking to switch to date step if description is filled
if (description.trim()) {
setCurrentStep('date');
}
}, [description]);
// Don't render if not open
if (!isOpen) {
// If inline mode, don't render anything when closed
if (inline) {
return null;
}
return (
<div className={cn('absolute bottom-4 left-1/2 z-50 -translate-x-1/2', className)}>
<motion.button
layoutId="task-command-bar-container"
onClick={() => setIsOpen(true)}
className="bg-background/80 hover:bg-background border-border flex items-center gap-2 rounded-full border px-4 py-2 text-sm shadow-lg backdrop-blur-sm transition-all hover:shadow-xl"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Plus className="h-4 w-4" />
<span>Add task</span>
<kbd className="bg-muted text-muted-foreground ml-2 rounded px-1.5 py-0.5 text-xs">
⌘D
</kbd>
</motion.button>
</div>
);
}
const isDescriptionStep = currentStep === 'description';
const isConversationExpanded = currentStep === 'conversation';
const isDateExpanded = currentStep === 'date';
return (
<Container3D
className={cn(
'w-full overflow-hidden shadow-xl',
inline
? 'relative max-w-full'
: 'absolute bottom-4 left-1/2 z-50 max-w-lg -translate-x-1/2',
className,
)}
motionProps={{
layoutId: inline ? undefined : 'task-command-bar-container',
transition: {
type: 'spring',
stiffness: 400,
damping: 30,
},
}}
>
<Command className="rounded-xl" shouldFilter={false}>
{/* Conversation suggestions - shown when in conversation step */}
<motion.div
initial={false}
animate={{
height: isConversationExpanded ? conversationBounds.height : 0,
opacity: isConversationExpanded ? 1 : 0,
}}
transition={{ duration: 0.2, ease: 'easeOut' }}
className="overflow-hidden"
>
<div ref={conversationMeasureRef}>
<CommandList className="max-h-[200px] overflow-y-auto">
<CommandGroup
heading={
conversationInput.trim()
? 'Matching conversations'
: 'Link to a conversation (optional)'
}
>
{/* Skip option */}
{conversationInput.trim() === '' && (
<CommandItem
value="skip"
onSelect={handleSkipConversation}
className="text-muted-foreground flex cursor-pointer items-center gap-2 italic"
>
<User className="h-4 w-4" />
<span className="flex-1 text-sm">Personal task (no conversation)</span>
<span className="text-xs">Tab to skip</span>
</CommandItem>
)}
{/* Loading state */}
{conversationSearchQuery.isLoading && debouncedConversationQuery && (
<CommandItem disabled className="text-muted-foreground">
<span className="text-sm">Searching...</span>
</CommandItem>
)}
{/* Results */}
{conversationResults.map((conversation: ConversationResult, index: number) => (
<CommandItem
key=[redacted]
value={conversation.id}
onSelect={() => handleConversationSelect(conversation)}
className={cn(
'flex cursor-pointer items-center gap-2',
index === selectedConversationIndex && 'bg-accent',
)}
>
<Building2 className="text-muted-foreground h-4 w-4 flex-shrink-0" />
<div className="flex min-w-0 flex-1 flex-col">
<span className="truncate text-sm">{conversation.name}</span>
{conversation.companyName && (
<span className="text-muted-foreground truncate text-xs">
{conversation.companyName}
</span>
)}
</div>
</CommandItem>
))}
{/* No results */}
{!conversationSearchQuery.isLoading &&
conversationResults.length === 0 &&
debouncedConversationQuery && (
<CommandItem
value="no-results"
onSelect={handleSkipConversation}
className="text-muted-foreground flex cursor-pointer items-center gap-2"
>
<User className="h-4 w-4" />
<span className="text-sm">
No conversations found. Press Enter to continue without linking.
</span>
</CommandItem>
)}
</CommandGroup>
</CommandList>
</div>
</motion.div>
{/* Date suggestions - shown when in date step */}
<motion.div
initial={false}
animate={{
height: isDateExpanded ? dateBounds.height : 0,
opacity: isDateExpanded ? 1 : 0,
}}
transition={{ duration: 0.2, ease: 'easeOut' }}
className="overflow-hidden"
>
<div ref={dateMeasureRef}>
<CommandList className="max-h-[200px] overflow-y-auto">
<CommandGroup heading="When is this due?">
{suggestions.map((suggestion, index) => (
<CommandItem
key=[redacted]
value={suggestion.id}
onSelect={() => handleSuggestionSelect(suggestion)}
className={cn(
'flex cursor-pointer items-center gap-2',
index === selectedSuggestionIndex && 'bg-accent',
)}
>
<Calendar className="text-muted-foreground h-4 w-4" />
<span className="flex-1 text-sm">{suggestion.label}</span>
{suggestion.shortLabel && (
<span className="text-muted-foreground text-xs">{suggestion.shortLabel}</span>
)}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</div>
</motion.div>
{/* Main input row - always visible */}
<div
className={cn(
'p-3',
(isConversationExpanded || isDateExpanded) && 'border-border border-t',
)}
>
<div className="flex items-center gap-2">
{/* Description input */}
<motion.div
layout
layoutId="task-description-section"
className="flex min-w-0 flex-1 items-center gap-2"
>
{isDescriptionStep ? (
<>
<motion.div layoutId="task-plus-icon">
<Plus className="text-muted-foreground h-4 w-4 flex-shrink-0" />
</motion.div>
<input
ref={descriptionInputRef}
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
onKeyDown={handleDescriptionKeyDown}
placeholder="What do you need to do?"
className="placeholder:text-muted-foreground min-w-0 flex-1 bg-transparent text-sm outline-none"
autoFocus
/>
{/* Show enter hint when description has content */}
<AnimatePresence>
{description.trim() && (
<motion.kbd
initial={{ opacity: 0, filter: 'blur(4px)' }}
animate={{ opacity: 1, filter: 'blur(0px)' }}
exit={{ opacity: 0, filter: 'blur(4px)' }}
transition={{ duration: 0.2 }}
className="bg-muted text-muted-foreground flex-shrink-0 rounded px-1.5 py-0.5 text-xs"
>
<EnterKey />
</motion.kbd>
)}
</AnimatePresence>
</>
) : (
<button
onClick={handleDescriptionButtonClick}
className="hover:bg-accent flex min-w-0 flex-1 cursor-pointer items-center gap-2 rounded-md px-2 py-1 transition-colors"
>
<motion.div layoutId="task-plus-icon">
<Plus className="text-muted-foreground h-4 w-4 flex-shrink-0" />
</motion.div>
<span className="min-w-0 flex-1 truncate text-left text-sm">
{description || 'What do you need to do?'}
</span>
</button>
)}
</motion.div>
{/* Separator */}
<motion.div layoutId="task-separator-1" className="bg-border h-5 w-px flex-shrink-0" />
{/* Conversation section */}
<motion.div
layout
layoutId="task-conversation-section"
className="flex flex-shrink-0 items-center gap-2"
>
{isConversationExpanded ? (
<>
<motion.div layoutId="task-building-icon">
<Building2 className="text-muted-foreground h-4 w-4 flex-shrink-0" />
</motion.div>
<input
ref={conversationInputRef}
type="text"
value={conversationInput}
onChange={(e) => setConversationInput(e.target.value)}
onKeyDown={handleConversationKeyDown}
placeholder="Search a deal…"
className="w-36 bg-transparent text-sm outline-none"
autoFocus
/>
<kbd className="bg-muted text-muted-foreground flex-shrink-0 rounded px-1.5 py-0.5 text-xs">
<EnterKey />
</kbd>
</>
) : (
<button
onClick={handleConversationButtonClick}
disabled={!description.trim()}
className={cn(
'flex items-center gap-1.5 rounded-md px-2 py-1 text-sm transition-colors',
description.trim()
? 'hover:bg-accent cursor-pointer'
: 'cursor-not-allowed opacity-50',
)}
>
<motion.div layoutId="task-building-icon">
<Building2 className="text-muted-foreground h-4 w-4" />
</motion.div>
<span className="text-muted-foreground max-w-24 truncate">
{selectedConversation ? selectedConversation.name : 'Personal'}
</span>
</button>
)}
</motion.div>
{/* Separator */}
<motion.div layoutId="task-separator-2" className="bg-border h-5 w-px flex-shrink-0" />
{/* Date section */}
<motion.div
layout
layoutId="task-date-section"
className="flex flex-shrink-0 items-center gap-2"
>
{isDateExpanded ? (
<>
<motion.div layoutId="task-calendar-icon">
<Calendar className="text-muted-foreground h-4 w-4 flex-shrink-0" />
</motion.div>
<input
ref={dateInputRef}
type="text"
value={dateInput}
onChange={(e) => setDateInput(e.target.value)}
onKeyDown={handleDateKeyDown}
placeholder={'next tuesday at 9:30am'}
className="w-40 bg-transparent text-sm outline-none"
autoFocus
/>
<kbd className="bg-muted text-muted-foreground flex-shrink-0 rounded px-1.5 py-0.5 text-xs">
<EnterKey />
</kbd>
</>
) : (
<button
onClick={handleDateSectionClick}
disabled={!description.trim()}
className={cn(
'flex items-center gap-1.5 rounded-md px-2 py-1 text-sm transition-colors',
description.trim()
? 'hover:bg-accent cursor-pointer'
: 'cursor-not-allowed opacity-50',
)}
>
<motion.div layoutId="task-calendar-icon">
<Calendar className="text-muted-foreground h-4 w-4" />
</motion.div>
<span className="text-muted-foreground">{formatDateDisplay(selectedDate)}</span>
</button>
)}
</motion.div>
</div>
</div>
</Command>
</Container3D>
);
}