271 lines
6.4 KiB
TypeScript
271 lines
6.4 KiB
TypeScript
"use client"
|
|
|
|
import {
|
|
createContext,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useState,
|
|
useCallback,
|
|
useRef,
|
|
type ReactNode,
|
|
} from "react"
|
|
import { toErrorMessage } from "@/lib/app-error"
|
|
import { getFolder, listFolderConversations } from "@/lib/tauri"
|
|
import type {
|
|
AgentType,
|
|
AgentStats,
|
|
DbConversationSummary,
|
|
FolderDetail,
|
|
} from "@/lib/types"
|
|
|
|
interface SelectedConversation {
|
|
id: number
|
|
agentType: AgentType
|
|
}
|
|
|
|
interface NewConversationState {
|
|
agentType: AgentType
|
|
workingDir: string
|
|
}
|
|
|
|
interface FolderContextValue {
|
|
folder: FolderDetail | null
|
|
folderId: number
|
|
folderLoading: boolean
|
|
|
|
conversations: DbConversationSummary[]
|
|
loading: boolean
|
|
refreshing: boolean
|
|
error: string | null
|
|
|
|
selectedConversation: SelectedConversation | null
|
|
selectConversation: (id: number, agentType: AgentType) => void
|
|
clearSelection: () => void
|
|
|
|
newConversation: NewConversationState | null
|
|
startNewConversation: (agentType: AgentType, workingDir: string) => void
|
|
cancelNewConversation: () => void
|
|
|
|
stats: AgentStats | null
|
|
|
|
refreshConversations: () => void
|
|
}
|
|
|
|
const FolderContext = createContext<FolderContextValue | null>(null)
|
|
|
|
export function useFolderContext() {
|
|
const ctx = useContext(FolderContext)
|
|
if (!ctx) {
|
|
throw new Error("useFolderContext must be used within FolderProvider")
|
|
}
|
|
return ctx
|
|
}
|
|
|
|
function computeStats(conversations: DbConversationSummary[]): AgentStats {
|
|
const byAgent = new Map<AgentType, number>()
|
|
let totalMessages = 0
|
|
|
|
for (const s of conversations) {
|
|
byAgent.set(s.agent_type, (byAgent.get(s.agent_type) ?? 0) + 1)
|
|
totalMessages += s.message_count
|
|
}
|
|
|
|
return {
|
|
total_conversations: conversations.length,
|
|
total_messages: totalMessages,
|
|
by_agent: Array.from(byAgent.entries()).map(([agent_type, count]) => ({
|
|
agent_type,
|
|
conversation_count: count,
|
|
})),
|
|
}
|
|
}
|
|
|
|
/** Module-level cache — survives component unmounts / page navigations. */
|
|
const cache = new Map<string, DbConversationSummary[]>()
|
|
|
|
interface FolderProviderProps {
|
|
children: ReactNode
|
|
folderId: number
|
|
initialConversationId?: number | null
|
|
initialAgentType?: AgentType | null
|
|
}
|
|
|
|
export function FolderProvider({
|
|
children,
|
|
folderId,
|
|
initialConversationId,
|
|
initialAgentType,
|
|
}: FolderProviderProps) {
|
|
// Folder info
|
|
const [folder, setFolder] = useState<FolderDetail | null>(null)
|
|
const [folderLoading, setFolderLoading] = useState(true)
|
|
|
|
// Conversations
|
|
const cacheKey = String(folderId)
|
|
const [conversations, setConversations] = useState<DbConversationSummary[]>(
|
|
() => cache.get(cacheKey) ?? []
|
|
)
|
|
const [loading, setLoading] = useState(() => !cache.has(cacheKey))
|
|
const [refreshing, setRefreshing] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
const [selectedConversation, setSelectedConversation] =
|
|
useState<SelectedConversation | null>(() => {
|
|
if (initialConversationId != null && initialAgentType) {
|
|
return { id: initialConversationId, agentType: initialAgentType }
|
|
}
|
|
return null
|
|
})
|
|
|
|
// Sync selection when URL params change (e.g. navigation)
|
|
useEffect(() => {
|
|
if (initialConversationId != null && initialAgentType) {
|
|
setSelectedConversation({
|
|
id: initialConversationId,
|
|
agentType: initialAgentType,
|
|
})
|
|
}
|
|
}, [initialConversationId, initialAgentType])
|
|
const [newConversation, setNewConversation] =
|
|
useState<NewConversationState | null>(null)
|
|
|
|
const mountedRef = useRef(true)
|
|
|
|
// Fetch folder info
|
|
useEffect(() => {
|
|
let cancelled = false
|
|
setFolderLoading(true)
|
|
getFolder(folderId)
|
|
.then((f) => {
|
|
if (!cancelled) {
|
|
setFolder(f)
|
|
setFolderLoading(false)
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
console.error("[FolderProvider] getFolder failed:", err)
|
|
if (!cancelled) setFolderLoading(false)
|
|
})
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
}, [folderId])
|
|
|
|
const fetchConversations = useCallback(async () => {
|
|
const cached = cache.get(cacheKey)
|
|
|
|
if (cached) {
|
|
setConversations(cached)
|
|
setLoading(false)
|
|
setRefreshing(true)
|
|
} else {
|
|
setLoading(true)
|
|
}
|
|
|
|
try {
|
|
setError(null)
|
|
const data = await listFolderConversations({
|
|
folder_id: folderId,
|
|
status: null,
|
|
})
|
|
if (!mountedRef.current) return
|
|
cache.set(cacheKey, data)
|
|
setConversations(data)
|
|
} catch (e) {
|
|
if (!mountedRef.current) return
|
|
if (!cached) {
|
|
setError(toErrorMessage(e))
|
|
}
|
|
} finally {
|
|
if (mountedRef.current) {
|
|
setLoading(false)
|
|
setRefreshing(false)
|
|
}
|
|
}
|
|
}, [folderId, cacheKey])
|
|
|
|
useEffect(() => {
|
|
fetchConversations()
|
|
}, [fetchConversations])
|
|
|
|
useEffect(() => {
|
|
mountedRef.current = true
|
|
return () => {
|
|
mountedRef.current = false
|
|
}
|
|
}, [])
|
|
|
|
const selectConversation = useCallback((id: number, agentType: AgentType) => {
|
|
setSelectedConversation({ id, agentType })
|
|
setNewConversation(null)
|
|
}, [])
|
|
|
|
const clearSelection = useCallback(() => {
|
|
setSelectedConversation(null)
|
|
}, [])
|
|
|
|
const startNewConversation = useCallback(
|
|
(agentType: AgentType, workingDir: string) => {
|
|
setNewConversation({ agentType, workingDir })
|
|
setSelectedConversation(null)
|
|
},
|
|
[]
|
|
)
|
|
|
|
const cancelNewConversation = useCallback(() => {
|
|
setNewConversation(null)
|
|
}, [])
|
|
|
|
const refreshConversations = useCallback(() => {
|
|
cache.delete(cacheKey)
|
|
fetchConversations()
|
|
}, [cacheKey, fetchConversations])
|
|
|
|
const stats = useMemo(
|
|
() => (conversations.length > 0 ? computeStats(conversations) : null),
|
|
[conversations]
|
|
)
|
|
|
|
const value = useMemo<FolderContextValue>(
|
|
() => ({
|
|
folder,
|
|
folderId,
|
|
folderLoading,
|
|
conversations,
|
|
loading,
|
|
refreshing,
|
|
error,
|
|
selectedConversation,
|
|
selectConversation,
|
|
clearSelection,
|
|
newConversation,
|
|
startNewConversation,
|
|
cancelNewConversation,
|
|
stats,
|
|
refreshConversations,
|
|
}),
|
|
[
|
|
folder,
|
|
folderId,
|
|
folderLoading,
|
|
conversations,
|
|
loading,
|
|
refreshing,
|
|
error,
|
|
selectedConversation,
|
|
selectConversation,
|
|
clearSelection,
|
|
newConversation,
|
|
startNewConversation,
|
|
cancelNewConversation,
|
|
stats,
|
|
refreshConversations,
|
|
]
|
|
)
|
|
|
|
return (
|
|
<FolderContext.Provider value={value}>{children}</FolderContext.Provider>
|
|
)
|
|
}
|