feat: conversational branching, AI markers, TaskLane improvements, collapsible sidebar #120
161
frontend/src/hooks/useBranching.ts
Normal file
161
frontend/src/hooks/useBranching.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
import { branchesApi } from '@/api/branches'
|
||||
import { toast } from '@/lib/toast'
|
||||
import type {
|
||||
BranchResponse,
|
||||
ForkCreateRequest,
|
||||
ForkPointResponse,
|
||||
BranchSwitchResponse,
|
||||
ReviveRequest,
|
||||
BranchMessageRequest,
|
||||
BranchMessageResponse,
|
||||
} from '@/types/branching'
|
||||
|
||||
export interface UseBranching {
|
||||
// State
|
||||
branches: BranchResponse[]
|
||||
activeBranchId: string | null
|
||||
isLoading: boolean
|
||||
isProcessing: boolean
|
||||
|
||||
// Actions
|
||||
loadBranches: (sessionId: string) => Promise<void>
|
||||
createFork: (sessionId: string, data: ForkCreateRequest) => Promise<ForkPointResponse | null>
|
||||
switchBranch: (sessionId: string, branchId: string) => Promise<BranchSwitchResponse | null>
|
||||
updateStatus: (sessionId: string, branchId: string, status: string, reason?: string) => Promise<void>
|
||||
reviveBranch: (sessionId: string, branchId: string, data: ReviveRequest) => Promise<void>
|
||||
sendMessage: (sessionId: string, branchId: string, data: BranchMessageRequest) => Promise<BranchMessageResponse | null>
|
||||
}
|
||||
|
||||
export function useBranching(): UseBranching {
|
||||
const [branches, setBranches] = useState<BranchResponse[]>([])
|
||||
const [activeBranchId, setActiveBranchId] = useState<string | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isProcessing, setIsProcessing] = useState(false)
|
||||
|
||||
const loadBranches = useCallback(async (sessionId: string) => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const result = await branchesApi.getBranches(sessionId)
|
||||
setBranches(result.branches)
|
||||
setActiveBranchId(result.active_branch_id)
|
||||
} catch {
|
||||
toast.error('Failed to load branches')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const createFork = useCallback(async (
|
||||
sessionId: string,
|
||||
data: ForkCreateRequest
|
||||
): Promise<ForkPointResponse | null> => {
|
||||
setIsProcessing(true)
|
||||
try {
|
||||
const result = await branchesApi.createFork(sessionId, data)
|
||||
// Reload branches to reflect the new fork
|
||||
const updated = await branchesApi.getBranches(sessionId)
|
||||
setBranches(updated.branches)
|
||||
setActiveBranchId(updated.active_branch_id)
|
||||
return result
|
||||
} catch {
|
||||
toast.error('Failed to create fork')
|
||||
return null
|
||||
} finally {
|
||||
setIsProcessing(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const switchBranch = useCallback(async (
|
||||
sessionId: string,
|
||||
branchId: string
|
||||
): Promise<BranchSwitchResponse | null> => {
|
||||
setIsProcessing(true)
|
||||
try {
|
||||
const result = await branchesApi.switchBranch(sessionId, branchId)
|
||||
setActiveBranchId(result.active_branch_id)
|
||||
setBranches(prev =>
|
||||
prev.map(b => b.id === result.active_branch_id ? result.branch : b)
|
||||
)
|
||||
return result
|
||||
} catch {
|
||||
toast.error('Failed to switch branch')
|
||||
return null
|
||||
} finally {
|
||||
setIsProcessing(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const updateStatus = useCallback(async (
|
||||
sessionId: string,
|
||||
branchId: string,
|
||||
status: string,
|
||||
reason?: string
|
||||
) => {
|
||||
setIsProcessing(true)
|
||||
try {
|
||||
await branchesApi.updateBranchStatus(sessionId, branchId, status, reason)
|
||||
setBranches(prev =>
|
||||
prev.map(b =>
|
||||
b.id === branchId
|
||||
? { ...b, status: status as BranchResponse['status'], status_reason: reason ?? b.status_reason }
|
||||
: b
|
||||
)
|
||||
)
|
||||
} catch {
|
||||
toast.error('Failed to update branch status')
|
||||
} finally {
|
||||
setIsProcessing(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const reviveBranch = useCallback(async (
|
||||
sessionId: string,
|
||||
branchId: string,
|
||||
data: ReviveRequest
|
||||
) => {
|
||||
setIsProcessing(true)
|
||||
try {
|
||||
await branchesApi.reviveBranch(sessionId, branchId, data)
|
||||
// Reload to get updated statuses
|
||||
const updated = await branchesApi.getBranches(sessionId)
|
||||
setBranches(updated.branches)
|
||||
setActiveBranchId(updated.active_branch_id)
|
||||
toast.success('Branch revived')
|
||||
} catch {
|
||||
toast.error('Failed to revive branch')
|
||||
} finally {
|
||||
setIsProcessing(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const sendMessage = useCallback(async (
|
||||
sessionId: string,
|
||||
branchId: string,
|
||||
data: BranchMessageRequest
|
||||
): Promise<BranchMessageResponse | null> => {
|
||||
setIsProcessing(true)
|
||||
try {
|
||||
const result = await branchesApi.sendBranchMessage(sessionId, branchId, data)
|
||||
return result
|
||||
} catch {
|
||||
toast.error('Failed to send message')
|
||||
return null
|
||||
} finally {
|
||||
setIsProcessing(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return {
|
||||
branches,
|
||||
activeBranchId,
|
||||
isLoading,
|
||||
isProcessing,
|
||||
loadBranches,
|
||||
createFork,
|
||||
switchBranch,
|
||||
updateStatus,
|
||||
reviveBranch,
|
||||
sendMessage,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user