fix: clear TaskLane on chat switch and remove double border
- Reset TaskLane state in handleNewChat and selectChat - Conditionally remove chat input top border when TaskLane is open Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,12 +4,16 @@ import { Sparkles, Send, Loader2, Flag, MessageSquare, Paperclip, Terminal, X, R
|
|||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { uploadsApi } from '@/api/uploads'
|
import { uploadsApi } from '@/api/uploads'
|
||||||
import type { PendingUpload } from '@/types/upload'
|
import type { PendingUpload } from '@/types/upload'
|
||||||
|
import type { ForkMetadata, ActionItem, QuestionItem } from '@/types/ai-session'
|
||||||
import { PageMeta } from '@/components/common/PageMeta'
|
import { PageMeta } from '@/components/common/PageMeta'
|
||||||
import { aiSessionsApi } from '@/api/aiSessions'
|
import { aiSessionsApi } from '@/api/aiSessions'
|
||||||
|
import { useBranching } from '@/hooks/useBranching'
|
||||||
|
import { BranchMap } from '@/components/session/BranchMap'
|
||||||
import { analytics } from '@/lib/analytics'
|
import { analytics } from '@/lib/analytics'
|
||||||
import { toast } from '@/lib/toast'
|
import { toast } from '@/lib/toast'
|
||||||
import { ChatSidebar } from '@/components/assistant/ChatSidebar'
|
import { ChatSidebar } from '@/components/assistant/ChatSidebar'
|
||||||
import { ChatMessage } from '@/components/assistant/ChatMessage'
|
import { ChatMessage } from '@/components/assistant/ChatMessage'
|
||||||
|
import { TaskLane } from '@/components/assistant/TaskLane'
|
||||||
import { ConcludeSessionModal } from '@/components/assistant/ConcludeSessionModal'
|
import { ConcludeSessionModal } from '@/components/assistant/ConcludeSessionModal'
|
||||||
import type { ChatListItem, ConclusionOutcome } from '@/types/assistant-chat'
|
import type { ChatListItem, ConclusionOutcome } from '@/types/assistant-chat'
|
||||||
import type { SuggestedFlow } from '@/types/copilot'
|
import type { SuggestedFlow } from '@/types/copilot'
|
||||||
@@ -18,6 +22,9 @@ interface MessageWithMeta {
|
|||||||
role: 'user' | 'assistant'
|
role: 'user' | 'assistant'
|
||||||
content: string
|
content: string
|
||||||
suggestedFlows?: SuggestedFlow[]
|
suggestedFlows?: SuggestedFlow[]
|
||||||
|
fork?: ForkMetadata | null
|
||||||
|
actions?: ActionItem[] | null
|
||||||
|
questions?: QuestionItem[] | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AssistantChatPage() {
|
export default function AssistantChatPage() {
|
||||||
@@ -30,11 +37,15 @@ export default function AssistantChatPage() {
|
|||||||
const [input, setInput] = useState('')
|
const [input, setInput] = useState('')
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [showConclude, setShowConclude] = useState(false)
|
const [showConclude, setShowConclude] = useState(false)
|
||||||
|
const branching = useBranching()
|
||||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false)
|
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false)
|
||||||
const [showLogs, setShowLogs] = useState(false)
|
const [showLogs, setShowLogs] = useState(false)
|
||||||
const [logContent, setLogContent] = useState('')
|
const [logContent, setLogContent] = useState('')
|
||||||
const [pendingUploads, setPendingUploads] = useState<PendingUpload[]>([])
|
const [pendingUploads, setPendingUploads] = useState<PendingUpload[]>([])
|
||||||
const [isDragOver, setIsDragOver] = useState(false)
|
const [isDragOver, setIsDragOver] = useState(false)
|
||||||
|
const [activeQuestions, setActiveQuestions] = useState<QuestionItem[]>([])
|
||||||
|
const [activeActions, setActiveActions] = useState<ActionItem[]>([])
|
||||||
|
const [showTaskLane, setShowTaskLane] = useState(false)
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||||
const inputRef = useRef<HTMLTextAreaElement>(null)
|
const inputRef = useRef<HTMLTextAreaElement>(null)
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
@@ -88,7 +99,7 @@ export default function AssistantChatPage() {
|
|||||||
})
|
})
|
||||||
setMessages(prev => [
|
setMessages(prev => [
|
||||||
...prev,
|
...prev,
|
||||||
{ role: 'assistant', content: response.content, suggestedFlows: response.suggested_flows },
|
{ role: 'assistant', content: response.content, suggestedFlows: response.suggested_flows, fork: response.fork, actions: response.actions, questions: response.questions },
|
||||||
])
|
])
|
||||||
setChats(prev =>
|
setChats(prev =>
|
||||||
prev.map(c =>
|
prev.map(c =>
|
||||||
@@ -97,6 +108,17 @@ export default function AssistantChatPage() {
|
|||||||
: c
|
: c
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
// Show task lane if AI sent questions or actions
|
||||||
|
if (response.fork && session.session_id) {
|
||||||
|
branching.loadBranches(session.session_id)
|
||||||
|
}
|
||||||
|
const hasQuestions = response.questions && response.questions.length > 0
|
||||||
|
const hasActions = response.actions && response.actions.length > 0
|
||||||
|
if (hasQuestions || hasActions) {
|
||||||
|
setActiveQuestions(response.questions || [])
|
||||||
|
setActiveActions(response.actions || [])
|
||||||
|
setShowTaskLane(true)
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('Failed to start AI conversation')
|
toast.error('Failed to start AI conversation')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -131,6 +153,10 @@ export default function AssistantChatPage() {
|
|||||||
|
|
||||||
const selectChat = useCallback(async (chatId: string) => {
|
const selectChat = useCallback(async (chatId: string) => {
|
||||||
setActiveChatId(chatId)
|
setActiveChatId(chatId)
|
||||||
|
// Clear TaskLane when switching chats
|
||||||
|
setShowTaskLane(false)
|
||||||
|
setActiveQuestions([])
|
||||||
|
setActiveActions([])
|
||||||
try {
|
try {
|
||||||
const detail = await aiSessionsApi.getSession(chatId)
|
const detail = await aiSessionsApi.getSession(chatId)
|
||||||
setMessages(
|
setMessages(
|
||||||
@@ -161,6 +187,10 @@ export default function AssistantChatPage() {
|
|||||||
setChats(prev => [chatItem, ...prev])
|
setChats(prev => [chatItem, ...prev])
|
||||||
setActiveChatId(session.session_id)
|
setActiveChatId(session.session_id)
|
||||||
setMessages([])
|
setMessages([])
|
||||||
|
// Clear TaskLane from previous session
|
||||||
|
setShowTaskLane(false)
|
||||||
|
setActiveQuestions([])
|
||||||
|
setActiveActions([])
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('Failed to create chat')
|
toast.error('Failed to create chat')
|
||||||
}
|
}
|
||||||
@@ -184,6 +214,19 @@ export default function AssistantChatPage() {
|
|||||||
toast.info('Pin feature coming soon')
|
toast.info('Pin feature coming soon')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleBranchSwitch = async (branchId: string) => {
|
||||||
|
if (!activeChatId || loading) return
|
||||||
|
const result = await branching.switchBranch(activeChatId, branchId)
|
||||||
|
if (!result) return
|
||||||
|
|
||||||
|
// Load the branch's conversation into the chat
|
||||||
|
const branchMessages: MessageWithMeta[] = (result.conversation_messages || []).map(m => ({
|
||||||
|
role: m.role as 'user' | 'assistant',
|
||||||
|
content: m.content,
|
||||||
|
}))
|
||||||
|
setMessages(branchMessages)
|
||||||
|
}
|
||||||
|
|
||||||
const handleSend = async () => {
|
const handleSend = async () => {
|
||||||
if (!input.trim() || !activeChatId || loading) return
|
if (!input.trim() || !activeChatId || loading) return
|
||||||
|
|
||||||
@@ -204,7 +247,7 @@ export default function AssistantChatPage() {
|
|||||||
analytics.aiFeatureUsed({ feature: 'assistant_chat' })
|
analytics.aiFeatureUsed({ feature: 'assistant_chat' })
|
||||||
setMessages(prev => [
|
setMessages(prev => [
|
||||||
...prev,
|
...prev,
|
||||||
{ role: 'assistant', content: response.content, suggestedFlows: response.suggested_flows },
|
{ role: 'assistant', content: response.content, suggestedFlows: response.suggested_flows, fork: response.fork, actions: response.actions, questions: response.questions },
|
||||||
])
|
])
|
||||||
setChats(prev =>
|
setChats(prev =>
|
||||||
prev.map(c =>
|
prev.map(c =>
|
||||||
@@ -213,6 +256,18 @@ export default function AssistantChatPage() {
|
|||||||
: c
|
: c
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
// Load branches if fork was created
|
||||||
|
if (response.fork && activeChatId) {
|
||||||
|
branching.loadBranches(activeChatId)
|
||||||
|
}
|
||||||
|
// Show task lane if AI sent questions or actions
|
||||||
|
const hasQuestions = response.questions && response.questions.length > 0
|
||||||
|
const hasActions = response.actions && response.actions.length > 0
|
||||||
|
if (hasQuestions || hasActions) {
|
||||||
|
setActiveQuestions(response.questions || [])
|
||||||
|
setActiveActions(response.actions || [])
|
||||||
|
setShowTaskLane(true)
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setMessages(prev => [
|
setMessages(prev => [
|
||||||
...prev,
|
...prev,
|
||||||
@@ -224,6 +279,56 @@ export default function AssistantChatPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleTaskSubmit = async (responses: Array<{ type: string; state: string; value: string; text?: string; label?: string }>) => {
|
||||||
|
if (!activeChatId || loading) return
|
||||||
|
|
||||||
|
// Format task responses into a structured message for the AI
|
||||||
|
const parts: string[] = []
|
||||||
|
for (const r of responses) {
|
||||||
|
const name = r.type === 'question' ? `Q: ${r.text}` : r.label || 'Check'
|
||||||
|
if (r.state === 'done' && r.value.trim()) {
|
||||||
|
parts.push(`**${name}:**\n\`\`\`\n${r.value.trim()}\n\`\`\``)
|
||||||
|
} else if (r.state === 'skipped') {
|
||||||
|
parts.push(`**${name}:** _(skipped)_`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const userMessage = parts.join('\n\n')
|
||||||
|
|
||||||
|
// Close the task lane
|
||||||
|
setShowTaskLane(false)
|
||||||
|
setActiveQuestions([])
|
||||||
|
setActiveActions([])
|
||||||
|
|
||||||
|
setMessages(prev => [...prev, { role: 'user', content: userMessage }])
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await aiSessionsApi.sendChatMessage(activeChatId, { message: userMessage })
|
||||||
|
setMessages(prev => [
|
||||||
|
...prev,
|
||||||
|
{ role: 'assistant', content: response.content, suggestedFlows: response.suggested_flows, fork: response.fork, actions: response.actions, questions: response.questions },
|
||||||
|
])
|
||||||
|
if (response.fork && activeChatId) {
|
||||||
|
branching.loadBranches(activeChatId)
|
||||||
|
}
|
||||||
|
// Show task lane again if AI sends more tasks
|
||||||
|
const hasQuestions = response.questions && response.questions.length > 0
|
||||||
|
const hasActions = response.actions && response.actions.length > 0
|
||||||
|
if (hasQuestions || hasActions) {
|
||||||
|
setActiveQuestions(response.questions || [])
|
||||||
|
setActiveActions(response.actions || [])
|
||||||
|
setShowTaskLane(true)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setMessages(prev => [
|
||||||
|
...prev,
|
||||||
|
{ role: 'assistant', content: 'Sorry, something went wrong processing your responses. Please try again.' },
|
||||||
|
])
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleConclude = async (outcome: ConclusionOutcome, _notes: string): Promise<string> => {
|
const handleConclude = async (outcome: ConclusionOutcome, _notes: string): Promise<string> => {
|
||||||
if (!activeChatId) throw new Error('No active chat')
|
if (!activeChatId) throw new Error('No active chat')
|
||||||
|
|
||||||
@@ -268,7 +373,7 @@ export default function AssistantChatPage() {
|
|||||||
const response = await aiSessionsApi.sendChatMessage(session.session_id, { message: resumePrompt })
|
const response = await aiSessionsApi.sendChatMessage(session.session_id, { message: resumePrompt })
|
||||||
setMessages(prev => [
|
setMessages(prev => [
|
||||||
...prev,
|
...prev,
|
||||||
{ role: 'assistant', content: response.content, suggestedFlows: response.suggested_flows },
|
{ role: 'assistant', content: response.content, suggestedFlows: response.suggested_flows, fork: response.fork, actions: response.actions, questions: response.questions },
|
||||||
])
|
])
|
||||||
setChats(prev =>
|
setChats(prev =>
|
||||||
prev.map(c =>
|
prev.map(c =>
|
||||||
@@ -277,6 +382,17 @@ export default function AssistantChatPage() {
|
|||||||
: c
|
: c
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
// Show task lane if AI sent questions or actions
|
||||||
|
if (response.fork && session.session_id) {
|
||||||
|
branching.loadBranches(session.session_id)
|
||||||
|
}
|
||||||
|
const hasQuestions = response.questions && response.questions.length > 0
|
||||||
|
const hasActions = response.actions && response.actions.length > 0
|
||||||
|
if (hasQuestions || hasActions) {
|
||||||
|
setActiveQuestions(response.questions || [])
|
||||||
|
setActiveActions(response.actions || [])
|
||||||
|
setShowTaskLane(true)
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('Failed to create resume chat')
|
toast.error('Failed to create resume chat')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -392,7 +508,8 @@ export default function AssistantChatPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main chat area */}
|
{/* Main chat area + optional branch sidebar */}
|
||||||
|
<div className="flex-1 flex min-w-0">
|
||||||
<div className="flex-1 flex flex-col min-w-0">
|
<div className="flex-1 flex flex-col min-w-0">
|
||||||
{/* Mobile header with chat history toggle */}
|
{/* Mobile header with chat history toggle */}
|
||||||
<div className="sm:hidden flex items-center gap-2 px-3 py-2 border-b border-border shrink-0">
|
<div className="sm:hidden flex items-center gap-2 px-3 py-2 border-b border-border shrink-0">
|
||||||
@@ -452,7 +569,7 @@ export default function AssistantChatPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Rich Input */}
|
{/* Rich Input */}
|
||||||
<div className="px-3 sm:px-6 py-3 shrink-0 border-t border-border">
|
<div className={cn("px-3 sm:px-6 py-3 shrink-0", !showTaskLane && "border-t border-border")}>
|
||||||
<div
|
<div
|
||||||
className="max-w-3xl mx-auto"
|
className="max-w-3xl mx-auto"
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
@@ -593,6 +710,22 @@ export default function AssistantChatPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Task lane — slides in when AI sends questions or actions */}
|
||||||
|
{showTaskLane && (activeQuestions.length > 0 || activeActions.length > 0) && (
|
||||||
|
<TaskLane
|
||||||
|
questions={activeQuestions}
|
||||||
|
actions={activeActions}
|
||||||
|
onSubmit={handleTaskSubmit}
|
||||||
|
onClose={() => setShowTaskLane(false)}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Branch map hidden — branching is now silent/background only.
|
||||||
|
Branches are tracked in the DB but not shown to the user.
|
||||||
|
The AI manages branch context internally. */}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Conclude Session Modal */}
|
{/* Conclude Session Modal */}
|
||||||
<ConcludeSessionModal
|
<ConcludeSessionModal
|
||||||
isOpen={showConclude}
|
isOpen={showConclude}
|
||||||
|
|||||||
Reference in New Issue
Block a user