feat: conversational branching, AI markers, TaskLane improvements, collapsible sidebar #120
@@ -1,4 +1,4 @@
|
||||
import { Plus, Pin, Trash2, MessageSquare } from 'lucide-react'
|
||||
import { Plus, Pin, Trash2, MessageSquare, History, X } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { ChatListItem } from '@/types/assistant-chat'
|
||||
|
||||
@@ -11,6 +11,8 @@ interface ChatSidebarProps {
|
||||
onTogglePin: (id: string, pinned: boolean) => void
|
||||
mobileOpen?: boolean
|
||||
onMobileClose?: () => void
|
||||
collapsed?: boolean
|
||||
onToggleCollapse?: () => void
|
||||
}
|
||||
|
||||
export function ChatSidebar({
|
||||
@@ -22,6 +24,8 @@ export function ChatSidebar({
|
||||
onTogglePin,
|
||||
mobileOpen = false,
|
||||
onMobileClose,
|
||||
collapsed = false,
|
||||
onToggleCollapse,
|
||||
}: ChatSidebarProps) {
|
||||
const pinnedChats = chats.filter(c => c.pinned)
|
||||
const unpinnedChats = chats.filter(c => !c.pinned)
|
||||
@@ -36,6 +40,11 @@ export function ChatSidebar({
|
||||
onMobileClose?.()
|
||||
}
|
||||
|
||||
// When collapsed on desktop, render nothing — parent renders the top bar
|
||||
if (collapsed && !mobileOpen) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile overlay */}
|
||||
@@ -52,14 +61,23 @@ export function ChatSidebar({
|
||||
style={{ background: 'var(--color-bg-sidebar)' }}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="px-4 py-3 border-b shrink-0" style={{ borderColor: 'var(--color-border-default)' }}>
|
||||
<div className="px-3 py-3 border-b shrink-0 flex items-center gap-2" style={{ borderColor: 'var(--color-border-default)' }}>
|
||||
<button
|
||||
onClick={handleNewChat}
|
||||
className="w-full flex items-center justify-center gap-2 bg-primary text-white font-semibold text-sm rounded-lg px-4 py-2.5 hover:brightness-110 active:scale-[0.98] transition-all"
|
||||
className="flex-1 flex items-center justify-center gap-2 bg-primary text-white font-semibold text-sm rounded-lg px-4 py-2.5 hover:brightness-110 active:scale-[0.98] transition-all"
|
||||
>
|
||||
<Plus size={16} />
|
||||
New Chat
|
||||
</button>
|
||||
{onToggleCollapse && (
|
||||
<button
|
||||
onClick={onToggleCollapse}
|
||||
className="hidden sm:flex p-1.5 rounded-lg text-muted-foreground hover:text-heading hover:bg-elevated transition-colors"
|
||||
title="Collapse to top bar"
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Chat list */}
|
||||
@@ -108,6 +126,51 @@ export function ChatSidebar({
|
||||
)
|
||||
}
|
||||
|
||||
/** Collapsed top bar — rendered by the parent page above the chat area */
|
||||
export function ChatSidebarCollapsedBar({
|
||||
chats,
|
||||
activeChatId,
|
||||
onNewChat,
|
||||
onExpand,
|
||||
}: {
|
||||
chats: ChatListItem[]
|
||||
activeChatId: string | null
|
||||
onNewChat: () => void
|
||||
onExpand: () => void
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-2 px-3 py-2 border-b shrink-0"
|
||||
style={{ background: 'var(--color-bg-sidebar)', borderColor: 'var(--color-border-default)' }}
|
||||
>
|
||||
<button
|
||||
onClick={onNewChat}
|
||||
className="flex items-center gap-1.5 bg-primary text-white font-semibold text-xs rounded-md px-3 py-1.5 hover:brightness-110 active:scale-[0.98] transition-all"
|
||||
>
|
||||
<Plus size={14} />
|
||||
New
|
||||
</button>
|
||||
<button
|
||||
onClick={onExpand}
|
||||
className="flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs text-muted-foreground hover:text-heading hover:bg-elevated transition-colors"
|
||||
title="Show chat history"
|
||||
>
|
||||
<History size={14} />
|
||||
<span>History</span>
|
||||
{chats.length > 0 && (
|
||||
<span className="text-[10px] bg-elevated rounded-full px-1.5 py-0.5 font-medium">{chats.length}</span>
|
||||
)}
|
||||
</button>
|
||||
<div className="flex-1" />
|
||||
{activeChatId && (
|
||||
<span className="text-xs text-muted-foreground truncate max-w-[300px]">
|
||||
{chats.find(c => c.id === activeChatId)?.title}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ChatItem({
|
||||
chat,
|
||||
isActive,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { aiSessionsApi } from '@/api/aiSessions'
|
||||
import { useBranching } from '@/hooks/useBranching'
|
||||
import { analytics } from '@/lib/analytics'
|
||||
import { toast } from '@/lib/toast'
|
||||
import { ChatSidebar } from '@/components/assistant/ChatSidebar'
|
||||
import { ChatSidebar, ChatSidebarCollapsedBar } from '@/components/assistant/ChatSidebar'
|
||||
import { ChatMessage } from '@/components/assistant/ChatMessage'
|
||||
import { TaskLane } from '@/components/assistant/TaskLane'
|
||||
import { ConcludeSessionModal } from '@/components/assistant/ConcludeSessionModal'
|
||||
@@ -45,6 +45,14 @@ export default function AssistantChatPage() {
|
||||
const [activeQuestions, setActiveQuestions] = useState<QuestionItem[]>([])
|
||||
const [activeActions, setActiveActions] = useState<ActionItem[]>([])
|
||||
const [showTaskLane, setShowTaskLane] = useState(false)
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(() =>
|
||||
localStorage.getItem('rf-chat-sidebar-collapsed') === 'true'
|
||||
)
|
||||
const toggleSidebarCollapse = () => {
|
||||
const next = !sidebarCollapsed
|
||||
setSidebarCollapsed(next)
|
||||
localStorage.setItem('rf-chat-sidebar-collapsed', String(next))
|
||||
}
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
@@ -470,17 +478,20 @@ export default function AssistantChatPage() {
|
||||
<>
|
||||
<PageMeta title="AI Assistant" />
|
||||
<div className="flex h-[calc(100vh-3.5rem)]">
|
||||
{/* Sidebar — hidden on mobile, slide-out via toggle */}
|
||||
<div className="hidden sm:block">
|
||||
<ChatSidebar
|
||||
chats={chats}
|
||||
activeChatId={activeChatId}
|
||||
onSelectChat={selectChat}
|
||||
onNewChat={handleNewChat}
|
||||
onDeleteChat={handleDeleteChat}
|
||||
onTogglePin={handleTogglePin}
|
||||
/>
|
||||
</div>
|
||||
{/* Sidebar — hidden on mobile, collapsed to top bar or full sidebar on desktop */}
|
||||
{!sidebarCollapsed && (
|
||||
<div className="hidden sm:block">
|
||||
<ChatSidebar
|
||||
chats={chats}
|
||||
activeChatId={activeChatId}
|
||||
onSelectChat={selectChat}
|
||||
onNewChat={handleNewChat}
|
||||
onDeleteChat={handleDeleteChat}
|
||||
onTogglePin={handleTogglePin}
|
||||
onToggleCollapse={toggleSidebarCollapse}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="sm:hidden">
|
||||
<ChatSidebar
|
||||
chats={chats}
|
||||
@@ -495,7 +506,22 @@ export default function AssistantChatPage() {
|
||||
</div>
|
||||
|
||||
{/* Main chat area + optional branch sidebar */}
|
||||
<div className="flex-1 flex min-w-0">
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
|
||||
{/* Collapsed sidebar top bar — desktop only */}
|
||||
{sidebarCollapsed && (
|
||||
<div className="hidden sm:block">
|
||||
<ChatSidebarCollapsedBar
|
||||
chats={chats}
|
||||
activeChatId={activeChatId}
|
||||
onNewChat={handleNewChat}
|
||||
onExpand={toggleSidebarCollapse}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Chat content row: chat column + TaskLane side by side */}
|
||||
<div className="flex-1 flex min-w-0 min-h-0">
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
{/* 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">
|
||||
@@ -555,7 +581,7 @@ export default function AssistantChatPage() {
|
||||
</div>
|
||||
|
||||
{/* Rich Input */}
|
||||
<div className={cn("px-3 sm:px-6 py-3 shrink-0", !showTaskLane && "border-t border-border")}>
|
||||
<div className="px-3 sm:px-6 py-3 shrink-0">
|
||||
<div
|
||||
className="max-w-3xl mx-auto"
|
||||
onDragOver={handleDragOver}
|
||||
@@ -710,7 +736,8 @@ export default function AssistantChatPage() {
|
||||
{/* 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>
|
||||
</div>{/* close chat content row */}
|
||||
</div>{/* close outer flex-col */}
|
||||
|
||||
{/* Conclude Session Modal */}
|
||||
<ConcludeSessionModal
|
||||
|
||||
Reference in New Issue
Block a user