Spaces:
Sleeping
Sleeping
| import { useState } from 'react'; | |
| export const useChunkNavigation = (documentData) => { | |
| const [chunkStates, setChunkStates] = useState({}); | |
| const [currentChunkIndex, setCurrentChunkIndex] = useState(0); | |
| const [chunkExpanded, setChunkExpanded] = useState(true); | |
| const [globalChatHistory, setGlobalChatHistory] = useState([]); | |
| const [showChat, setShowChat] = useState(true); | |
| const [loadingChunkIndex, setLoadingChunkIndex] = useState(null); | |
| const streamResponse = async (requestBody, isAutomated, nextChunkIndex) => { | |
| const targetChunkIndex = nextChunkIndex || currentChunkIndex; | |
| setLoadingChunkIndex(targetChunkIndex); | |
| try { | |
| const response = await fetch('/api/chat/stream', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: requestBody | |
| }); | |
| const reader = await response.body.getReader() | |
| let shouldStop = false; | |
| const parsedBody = JSON.parse(requestBody); | |
| let localMessages = [...parsedBody.messages]; | |
| const createTempId = () => `assistant_${Date.now()}_${Math.random().toString(36).slice(2)}`; | |
| let assistantId = null; | |
| // SSE read buffer | |
| let sseBuffer = ''; | |
| // Streaming smoothness buffer | |
| let textBuffer = ''; | |
| let frameScheduled = false; | |
| const flushBuffer = (isFinal = false) => { | |
| if (!assistantId) return; | |
| const lastMsg = localMessages[localMessages.length - 1]; | |
| if (lastMsg.id === assistantId) { | |
| // Append buffered text | |
| lastMsg.content += textBuffer; | |
| textBuffer = ''; | |
| } | |
| updateGlobalChatHistory([...localMessages]); | |
| }; | |
| const scheduleFlush = () => { | |
| if (!frameScheduled) { | |
| frameScheduled = true; | |
| requestAnimationFrame(() => { | |
| flushBuffer(); | |
| frameScheduled = false; | |
| }); | |
| } | |
| }; | |
| while (!shouldStop) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| sseBuffer += new TextDecoder().decode(value); | |
| const parts = sseBuffer.split('\n\n'); | |
| sseBuffer = parts.pop(); // keep last partial | |
| for (const part of parts) { | |
| if (!part.startsWith('data:')) continue; | |
| const jsonStr = part.slice(5).trim(); | |
| if (!jsonStr) continue; | |
| let parsed; | |
| try { | |
| parsed = JSON.parse(jsonStr); | |
| } catch (err) { | |
| console.warn('Could not JSON.parse stream chunk', jsonStr); | |
| continue; | |
| } | |
| if (parsed.error) { | |
| console.error('streaming error', parsed.error); | |
| shouldStop = true; | |
| break; | |
| } | |
| if (parsed.done) { | |
| shouldStop = true; | |
| flushBuffer(true); // final flush, remove cursor | |
| break; | |
| } | |
| const delta = typeof parsed === 'string' ? parsed : parsed?.content ?? ''; | |
| if (!assistantId) { | |
| assistantId = createTempId(); | |
| localMessages.push({ | |
| id: assistantId, | |
| role: 'assistant', | |
| content: delta, | |
| chunkIndex: nextChunkIndex ? nextChunkIndex : currentChunkIndex | |
| }); | |
| } else { | |
| textBuffer += delta; | |
| } | |
| // Schedule smooth UI update | |
| scheduleFlush(); | |
| } | |
| } | |
| } catch (error) { | |
| console.error(error); | |
| addMessageToChunk( | |
| { role: 'assistant', content: 'Sorry, something went wrong. Please try again.' }, | |
| currentChunkIndex | |
| ); | |
| } finally { | |
| setLoadingChunkIndex(null); | |
| } | |
| }; | |
| const goToNextChunk = () => { | |
| if (documentData && currentChunkIndex < documentData.chunks.length - 1) { | |
| setCurrentChunkIndex(currentChunkIndex + 1); | |
| setChunkExpanded(true); | |
| } | |
| }; | |
| const goToPrevChunk = () => { | |
| if (currentChunkIndex > 0) { | |
| setCurrentChunkIndex(currentChunkIndex - 1); | |
| setChunkExpanded(true); | |
| } | |
| }; | |
| const sendAutomatedMessage = async (action) => { | |
| if (!documentData || currentChunkIndex >= documentData.chunks.length - 1) return; | |
| const nextChunkIndex = currentChunkIndex + 1; | |
| setLoadingChunkIndex(nextChunkIndex); | |
| const nextChunk = documentData.chunks[nextChunkIndex]; | |
| // Update chunk index immediately for UI feedback | |
| setCurrentChunkIndex(nextChunkIndex); | |
| // Check if we already have messages for this chunk | |
| if (hasChunkMessages(nextChunkIndex)) { | |
| // Don't generate new response, just navigate | |
| setLoadingChunkIndex(null); | |
| return; | |
| } | |
| const requestBody = JSON.stringify({ | |
| messages: globalChatHistory, | |
| currentChunk: documentData.chunks[currentChunkIndex]?.text || '', | |
| nextChunk: nextChunk.text, | |
| action: action, | |
| document: documentData ? JSON.stringify(documentData) : '' | |
| }) | |
| streamResponse(requestBody, true, nextChunkIndex); | |
| }; | |
| const skipChunk = () => { | |
| return sendAutomatedMessage('skip'); | |
| }; | |
| const markChunkUnderstood = () => { | |
| return sendAutomatedMessage('understood'); | |
| }; | |
| const startInteractiveLesson = (startChunkLessonFn) => { | |
| setChunkStates(prev => ({ | |
| ...prev, | |
| [currentChunkIndex]: 'interactive' | |
| })); | |
| startChunkLessonFn(currentChunkIndex); | |
| }; | |
| const setChunkAsInteractive = () => { | |
| // No longer tracking status - this is just for compatibility | |
| }; | |
| const updateGlobalChatHistory = (messages) => { | |
| setGlobalChatHistory(messages); | |
| }; | |
| const getGlobalChatHistory = () => { | |
| return globalChatHistory; | |
| }; | |
| const addMessageToChunk = (message, chunkIndex) => { | |
| const messageWithChunk = { ...message, chunkIndex }; | |
| setGlobalChatHistory(prev => [...prev, messageWithChunk]); | |
| }; | |
| const getCurrentChunkMessages = () => { | |
| return globalChatHistory.filter(msg => msg.chunkIndex === currentChunkIndex); | |
| }; | |
| const hasChunkMessages = (chunkIndex) => { | |
| return globalChatHistory.some(msg => msg.chunkIndex === chunkIndex); | |
| }; | |
| const isChunkLoading = (chunkIndex) => { | |
| return loadingChunkIndex === chunkIndex; | |
| }; | |
| return { | |
| chunkStates, | |
| currentChunkIndex, | |
| chunkExpanded, | |
| showChat, | |
| goToNextChunk, | |
| goToPrevChunk, | |
| skipChunk, | |
| markChunkUnderstood, | |
| startInteractiveLesson, | |
| setChunkExpanded, | |
| setShowChat, | |
| setChunkAsInteractive, | |
| updateGlobalChatHistory, | |
| getGlobalChatHistory, | |
| addMessageToChunk, | |
| getCurrentChunkMessages, | |
| hasChunkMessages, | |
| isChunkLoading, | |
| streamResponse | |
| }; | |
| }; |