feat: conversational branching, AI markers, TaskLane improvements, collapsible sidebar #120

Merged
chihlasm merged 58 commits from feat/conversational-branching into main 2026-03-27 13:16:44 +00:00
2 changed files with 108 additions and 18 deletions
Showing only changes of commit ca60b77d9a - Show all commits

View File

@@ -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,

View File

@@ -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