Handle Streaming

Example of making a request to the Copilot API and handling the streaming of the messages in a React frontend.

// Handles incoming messages from the server
const handleIncomingMessage = ({ prevMessages, message }: { prevMessages: UserChatMessage[]; message: UserChatMessage }) => {
	const existingIndex = prevMessages.findIndex((m) => m.id === message.id)

	if (existingIndex === -1) {
		// If the message is not in the list, add it
		return [...prevMessages, message]
	} else {
		// Clone the array for immutability
		const updatedMessages = [...prevMessages]
		updatedMessages[existingIndex] = message
		return updatedMessages
	}
}

const handleUserQuery = async ({
	query,
	history,
	setLoading,
	setMessages,
	setChatTitle,
	setChatSamplePrompts,
  setAbortController
}: {
	query: string
	history?: UserChatMessage[]
	setLoading: (loading: boolean) => void
	setMessages: React.Dispatch<React.SetStateAction<UserChatMessage[]>>
	setChatTitle: React.Dispatch<React.SetStateAction<string>>
	setChatSamplePrompts: React.Dispatch<React.SetStateAction<string[]>>
  setAbortController: (value: AbortController | null) => void
}) => {
	try {
		setLoading(true)

		const res = await fetch('https://api.finchat.io/query', {
			method: 'POST',
			headers: { Accept: 'text/event-stream', 'Authorization': `Bearer ${process.env.FINCHAT_ENTERPRISE_API_KEY}` },
			body: JSON.stringify({
				query,
				history,
				inlineSourcing: true,
				generateTitle: !history || !history.length,
				generateSamplePrompts: true
			})
		})

		if (!res.ok) throw new Error('Failed to fetch')

		if (!res.body) throw new Error('No body')

      const abortController = new AbortController()
      setAbortController(abortController)

		const reader = res.body.getReader()
		const decoder = new TextDecoder()

    let newMessages: UserChatMessage[] = [];
		let done = false
    let accumulatedChunks = '';

    while (!done && !abortController.signal.aborted) {
      const { value, done: doneReading } = await reader.read()
      done = doneReading
    
      const chunkValue = decoder.decode(value, { stream: true })
      accumulatedChunks += chunkValue // Accumulate chunks
    
      let messageBoundaryIndex
      while ((messageBoundaryIndex = accumulatedChunks.indexOf('\n\n')) !== -1) {
        const completeMessage = accumulatedChunks.substring(0, messageBoundaryIndex)
        accumulatedChunks = accumulatedChunks.substring(messageBoundaryIndex + 2) // Remove the processed message from the accumulator
    
        if (!completeMessage) continue
        try {
          const messageJsonString = JSON.parse(
            completeMessage
              .split('\n')
              .map((line) => line.substring(6))
              .join('\n')
          ) as (UserChatMessage | GenerateTitleResponse | GenerateSamplePromptsResponse)

          if (messageJsonString.type === 'Title') {
            setChatTitle(messageJsonString.title)
          } else if (messageJsonString.type === 'Sample Prompts') {
            setChatSamplePrompts(messageJsonString.prompts)
          } else if (messageJsonString.type === 'Message') {
              // if the previous message id is the same as the current message id, then the bot is typing
          if (newMessages.length > 0 && newMessages[newMessages.length - 1].id === messageJsonString.id && !messageJsonString.hideContent) {
            setLoading(false)
          }
    
          setMessages((prevMessages: UserChatMessage[]) => handleIncomingMessage({ prevMessages, message: messageJsonString }))
          newMessages = handleIncomingMessage({ prevMessages: newMessages, message: messageJsonString })
          }
       
        } catch (e) {
          console.error('Failed to parse a chunk:', completeMessage)
        }
      }
    }
	} catch (e) {
		console.error(e)
	} finally {
		setLoading(false)
	}
}