import type { Message } from 'ai'; import { useCallback, useState } from 'react'; import { createScopedLogger } from '../../utils/logger'; import { StreamingMessageParser } from '../runtime/message-parser'; import { workbenchStore } from '../stores/workbench'; const logger = createScopedLogger('useMessageParser'); const messageParser = new StreamingMessageParser({ callbacks: { onArtifactOpen: (data) => { logger.trace('onArtifactOpen', data); workbenchStore.showWorkbench.set(true); workbenchStore.addArtifact(data); }, onArtifactClose: (data) => { logger.trace('onArtifactClose'); workbenchStore.updateArtifact(data, { closed: true }); }, onActionOpen: (data) => { logger.trace('onActionOpen', data.action); // we only add shell actions when when the close tag got parsed because only then we have the content if (data.action.type !== 'shell') { workbenchStore.addAction(data); } }, onActionClose: (data) => { logger.trace('onActionClose', data.action); if (data.action.type === 'shell') { workbenchStore.addAction(data); } workbenchStore.runAction(data); }, }, }); export function useMessageParser() { const [parsedMessages, setParsedMessages] = useState<{ [key: number]: string }>({}); const parseMessages = useCallback((messages: Message[], isLoading: boolean) => { let reset = false; if (import.meta.env.DEV && !isLoading) { reset = true; messageParser.reset(); } for (const [index, message] of messages.entries()) { if (message.role === 'assistant') { /** * In production, we only parse the last assistant message since previous messages can't change. * During development they can change, e.g., if the parser gets modified. */ if (import.meta.env.PROD && index < messages.length - 1) { continue; } const newParsedContent = messageParser.parse(message.id, message.content); setParsedMessages((prevParsed) => ({ ...prevParsed, [index]: !reset ? (prevParsed[index] || '') + newParsedContent : newParsedContent, })); } } }, []); return { parsedMessages, parseMessages }; }