diff --git a/DATA_STREAMING.md b/DATA_STREAMING.md new file mode 100644 index 0000000..f6a8faa --- /dev/null +++ b/DATA_STREAMING.md @@ -0,0 +1,31 @@ +# Live Data Streaming + +The live data streaming feature allows visualization of real-time text updates across multiple streams. This is useful for monitoring ongoing processes or displaying live transcription or streaming data. + +## API + +The `/api/update-data-stream` endpoint provides functionality for managing live text streams and finalized data entries: + +- **POST**: Submit live text updates or finalized entries + - `text`: The text content to stream + - `stream_id`: Identifier for the data stream (defaults to 'default') + - `timestamp`: Unix timestamp (defaults to current time) + - `finalized`: Boolean flag to mark entry as finalized + - `uuid`: Backend UUID for database tracking + +- **GET**: Retrieve live or finalized data + - Query `?type=finalized` for processed entries + - Query `?stream=` for specific stream data + - No query parameters returns all live streams + +- **PATCH**: Update entry processing status using UUID + +## Data Stream Display + +The chat interface includes a "Data Stream Display" toggle in the header menu that enables real-time visualization of streaming data alongside chat conversations. This feature is particularly useful for monitoring live transcription feeds or processing status updates. + +## Database Watcher + +Database entries are created when data is submitted to the `/api/update-data-stream` endpoint with the `finalized` field set to `true`. These entries represent completed or processed data that should be persisted to a database system. + +NOTE: This API just provides visualization and does not manage a database itself. The assumption is that the user is using this API when manually updating a database. \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 9f5796c..b8a45af 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,6 +18,23 @@ RUN npm i FROM base AS builder WORKDIR /app +# Accept build arguments for NEXT_PUBLIC_ environment variables +ARG NEXT_PUBLIC_HTTP_CHAT_COMPLETION_URL +ARG NEXT_PUBLIC_WEBSOCKET_CHAT_COMPLETION_URL +ARG NEXT_PUBLIC_SHOW_DATA_STREAM_DEFAULT_ON +ARG NEXT_PUBLIC_WORKFLOW +ARG NEXT_PUBLIC_CHAT_HISTORY_DEFAULT_ON +ARG NEXT_PUBLIC_WEB_SOCKET_DEFAULT_ON +ARG NEXT_PUBLIC_ENABLE_INTERMEDIATE_STEPS + +# Set them as environment variables for the build +ENV NEXT_PUBLIC_HTTP_CHAT_COMPLETION_URL=$NEXT_PUBLIC_HTTP_CHAT_COMPLETION_URL +ENV NEXT_PUBLIC_WEBSOCKET_CHAT_COMPLETION_URL=$NEXT_PUBLIC_WEBSOCKET_CHAT_COMPLETION_URL +ENV NEXT_PUBLIC_SHOW_DATA_STREAM_DEFAULT_ON=$NEXT_PUBLIC_SHOW_DATA_STREAM_DEFAULT_ON +ENV NEXT_PUBLIC_WORKFLOW=$NEXT_PUBLIC_WORKFLOW +ENV NEXT_PUBLIC_CHAT_HISTORY_DEFAULT_ON=$NEXT_PUBLIC_CHAT_HISTORY_DEFAULT_ON +ENV NEXT_PUBLIC_WEB_SOCKET_DEFAULT_ON=$NEXT_PUBLIC_WEB_SOCKET_DEFAULT_ON +ENV NEXT_PUBLIC_ENABLE_INTERMEDIATE_STEPS=$NEXT_PUBLIC_ENABLE_INTERMEDIATE_STEPS COPY --from=deps /app/node_modules ./node_modules COPY . . diff --git a/README.md b/README.md index ce4eaf1..60ca4bc 100644 --- a/README.md +++ b/README.md @@ -85,9 +85,16 @@ NOTE: Most of the time, you will want to select /chat/stream for intermediate re - /generate/stream - Streaming response generation - /chat - Single response chat completion - /chat/stream - Streaming chat completion + - /chat/ca-rag - Single response chat completion when using [Context-Aware RAG](https://github.com/NVIDIA/context-aware-rag) backend - `WebSocket URL for Completion`: WebSocket URL to connect to running NeMo Agent Toolkit server - `WebSocket Schema`: Workflow schema type over WebSocket connection +### Live Data Streaming + +The live data streaming feature allows visualization of real-time text updates across multiple streams. This is useful for monitoring ongoing processes or displaying live transcription or streaming data. + +For more detail, see the [README for live data streaming](DATA_STREAMING.md). + ## Usage Examples ### Getting Started Example diff --git a/components/Avatar/SystemAvatar.tsx b/components/Avatar/SystemAvatar.tsx index 06ecd8a..b3f64f6 100644 --- a/components/Avatar/SystemAvatar.tsx +++ b/components/Avatar/SystemAvatar.tsx @@ -1,4 +1,4 @@ -import { IconPasswordUser, IconUserPentagon } from '@tabler/icons-react'; +import { IconPasswordUser } from '@tabler/icons-react'; import React from 'react'; export const SystemAgentAvatar = ({ height = 7, width = 7 }) => { diff --git a/components/Avatar/UserAvatar.tsx b/components/Avatar/UserAvatar.tsx index aa8acdb..1855f10 100644 --- a/components/Avatar/UserAvatar.tsx +++ b/components/Avatar/UserAvatar.tsx @@ -1,7 +1,5 @@ import React from 'react'; -import { getInitials } from '@/utils/app/helper'; - export const UserAvatar = ({ src = '', height = 30, width = 30 }) => { const profilePicUrl = src || ``; diff --git a/components/Chat/Chat.tsx b/components/Chat/Chat.tsx index 8d471dd..13c32c5 100644 --- a/components/Chat/Chat.tsx +++ b/components/Chat/Chat.tsx @@ -1,39 +1,32 @@ 'use client'; -import { ChatHeader } from './ChatHeader'; -import { ChatInput } from './ChatInput'; -import { ChatLoader } from './ChatLoader'; -import { MemoizedChatMessage } from './MemoizedChatMessage'; +import { v4 as uuidv4 } from 'uuid'; +import toast from 'react-hot-toast'; +import { useCallback, useContext, useEffect, useRef, useState } from 'react'; + import { InteractionModal } from '@/components/Chat/ChatInteractionMessage'; import HomeContext from '@/pages/api/home/home.context'; import { ChatBody, Conversation, Message } from '@/types/chat'; import { WebSocketInbound, - validateWebSocketMessage, validateWebSocketMessageWithConversationId, - validateConversationId, isSystemResponseMessage, isSystemIntermediateMessage, isSystemInteractionMessage, isErrorMessage, - isSystemResponseInProgress, isSystemResponseComplete, - isOAuthConsentMessage, extractOAuthUrl, - shouldAppendResponseContent, } from '@/types/websocket'; import { getEndpoint } from '@/utils/app/api'; import { webSocketMessageTypes } from '@/utils/app/const'; import { saveConversation, saveConversations, - updateConversation, } from '@/utils/app/conversation'; import { fetchLastMessage, processIntermediateMessage, - updateConversationTitle, } from '@/utils/app/helper'; import { shouldAppendResponse, @@ -45,14 +38,11 @@ import { shouldRenderAssistantMessage, } from '@/utils/chatTransform'; import { throttle } from '@/utils/data/throttle'; -import { useTranslation } from 'next-i18next'; -import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; -import toast from 'react-hot-toast'; -import { v4 as uuidv4 } from 'uuid'; - import { SESSION_COOKIE_NAME } from '@/constants/constants'; - +import { ChatInput } from './ChatInput'; +import { ChatLoader } from './ChatLoader'; +import { MemoizedChatMessage } from './MemoizedChatMessage'; // Streaming utilities for handling SSE and NDJSON safely function normalizeNewlines(s: string): string { @@ -145,7 +135,6 @@ function parsePossiblyConcatenatedJson(payload: string): any[] { // }; export const Chat = () => { - const { t } = useTranslation('chat'); const { state: { selectedConversation, @@ -162,13 +151,11 @@ export const Chat = () => { intermediateStepOverride, enableIntermediateSteps, }, - handleUpdateConversation, dispatch: homeDispatch, } = useContext(HomeContext); const [currentMessage, setCurrentMessage] = useState(); const [autoScrollEnabled, setAutoScrollEnabled] = useState(true); - const [showSettings, setShowSettings] = useState(false); const [showScrollDownButton, setShowScrollDownButton] = useState(false); @@ -321,7 +308,9 @@ export const Chat = () => { 'ws://127.0.0.1:8000/websocket'; // Determine if this is a cross-origin connection + // eslint-disable-next-line no-unused-vars const wsUrlObj = new URL(wsUrl); + // eslint-disable-next-line no-unused-vars const isCrossOrigin = wsUrlObj.origin !== window.location.origin; // Always add session cookie as query parameter for reliability @@ -389,7 +378,7 @@ export const Chat = () => { } }; - ws.onerror = error => { + ws.onerror = _error => { homeDispatch({ field: 'webSocketConnected', value: false }); webSocketConnectedRef.current = false; homeDispatch({ field: 'loading', value: false }); @@ -412,7 +401,8 @@ export const Chat = () => { /** * Handles OAuth consent flow by opening popup window */ - const handleOAuthConsent = (message: WebSocketInbound) => { + // eslint-disable-next-line no-unused-vars + const _handleOAuthConsent = (message: WebSocketInbound) => { if (!isSystemInteractionMessage(message)) return false; if (message.content?.input_type === 'oauth_consent') { @@ -423,7 +413,7 @@ export const Chat = () => { 'oauth-popup', 'width=600,height=700,scrollbars=yes,resizable=yes' ); - const handleOAuthComplete = (event: MessageEvent) => { + const handleOAuthComplete = (_event: MessageEvent) => { if (popup && !popup.closed) popup.close(); window.removeEventListener('message', handleOAuthComplete); }; @@ -703,7 +693,7 @@ export const Chat = () => { }; const handleSend = useCallback( - async (message: Message, deleteCount = 0, retry = false) => { + async (message: Message, deleteCount = 0, _retry = false) => { message.id = uuidv4(); // Set the active user message ID for WebSocket message tracking @@ -863,8 +853,7 @@ export const Chat = () => { }; const endpoint = getEndpoint({ service: 'chat' }); - let body; - body = JSON.stringify({ + const body = JSON.stringify({ ...chatBody, }); @@ -982,6 +971,7 @@ export const Chat = () => { chunkValue = String(chunkValue ?? ''); } + // eslint-disable-next-line no-unused-vars counter++; // First, handle any partial chunk from previous iteration @@ -1006,8 +996,8 @@ export const Chat = () => { } // Process complete intermediate steps - let rawIntermediateSteps: any[] = []; - let stepMatches = + const rawIntermediateSteps: any[] = []; + const stepMatches = chunkValue.match( /([\s\S]*?)<\/intermediatestep>/g ) || []; @@ -1017,7 +1007,7 @@ export const Chat = () => { .replace('', '') .replace('', '') .trim(); - let rawIntermediateMessage = tryParseJson(jsonString); + const rawIntermediateMessage = tryParseJson(jsonString); // handle intermediate data if (rawIntermediateMessage?.type === 'system_intermediate') { rawIntermediateSteps.push(rawIntermediateMessage); @@ -1343,7 +1333,6 @@ export const Chat = () => { ref={chatContainerRef} onScroll={handleScroll} > - {selectedConversation?.messages.map((message, index) => { if (!shouldRenderAssistantMessage(message)) { return null; // Hide empty assistant messages diff --git a/components/Chat/ChatHeader.tsx b/components/Chat/ChatHeader.tsx index ff187b2..60db137 100644 --- a/components/Chat/ChatHeader.tsx +++ b/components/Chat/ChatHeader.tsx @@ -10,13 +10,14 @@ import { IconChevronRight, } from '@tabler/icons-react'; import React, { useContext, useState, useRef, useEffect } from 'react'; - import { env } from 'next-runtime-env'; import { getWorkflowName } from '@/utils/app/helper'; - +import { useTheme } from '@/contexts/ThemeContext'; import HomeContext from '@/pages/api/home/home.context'; +import { DataStreamControls } from './DataStreamControls'; + export const ChatHeader = ({ webSocketModeRef = {} }) => { const [isMenuOpen, setIsMenuOpen] = useState(false); const [isExpanded, setIsExpanded] = useState( @@ -34,12 +35,13 @@ export const ChatHeader = ({ webSocketModeRef = {} }) => { chatHistory, webSocketMode, webSocketConnected, - lightMode, selectedConversation, }, dispatch: homeDispatch, } = useContext(HomeContext); + const { lightMode, setLightMode } = useTheme(); + const handleLogin = () => { console.log('Login clicked'); setIsMenuOpen(false); @@ -73,7 +75,7 @@ export const ChatHeader = ({ webSocketModeRef = {} }) => { ) : (
- Hi, I'm {workflow} + Hi, I'm {workflow}
How can I assist you today? @@ -85,6 +87,10 @@ export const ChatHeader = ({ webSocketModeRef = {} }) => {
+ {/* Data Stream Controls - Manages data stream display toggle and database updates button */} + + {/* Theme Toggle Button */}
+
+ + ); +}; + diff --git a/components/Chat/MemoizedChatMessage.tsx b/components/Chat/MemoizedChatMessage.tsx index 4270ee7..6ee65d6 100644 --- a/components/Chat/MemoizedChatMessage.tsx +++ b/components/Chat/MemoizedChatMessage.tsx @@ -1,9 +1,8 @@ import { FC, memo } from 'react'; +import isEqual from 'lodash/isEqual'; import { ChatMessage, Props } from './ChatMessage'; -import isEqual from 'lodash/isEqual'; - export const MemoizedChatMessage: FC = memo( ChatMessage, (prevProps, nextProps) => { diff --git a/components/Chat/Regenerate.tsx b/components/Chat/Regenerate.tsx index d36c3e4..618b019 100644 --- a/components/Chat/Regenerate.tsx +++ b/components/Chat/Regenerate.tsx @@ -1,6 +1,5 @@ import { IconRefresh } from '@tabler/icons-react'; import { FC } from 'react'; - import { useTranslation } from 'next-i18next'; interface Props { diff --git a/components/Chatbar/Chatbar.context.tsx b/components/Chatbar/Chatbar.context.tsx index af43d3a..0acdf72 100644 --- a/components/Chatbar/Chatbar.context.tsx +++ b/components/Chatbar/Chatbar.context.tsx @@ -1,7 +1,6 @@ import { Dispatch, createContext } from 'react'; import { ActionType } from '@/hooks/useCreateReducer'; - import { Conversation } from '@/types/chat'; import { SupportedExportFormats } from '@/types/export'; @@ -10,10 +9,10 @@ import { ChatbarInitialState } from './Chatbar.state'; export interface ChatbarContextProps { state: ChatbarInitialState; dispatch: Dispatch>; - handleDeleteConversation: (conversation: Conversation) => void; + handleDeleteConversation: (_conversation: Conversation) => void; handleClearConversations: () => void; handleExportData: () => void; - handleImportConversations: (data: SupportedExportFormats) => void; + handleImportConversations: (_data: SupportedExportFormats) => void; } const ChatbarContext = createContext(undefined!); diff --git a/components/Chatbar/Chatbar.tsx b/components/Chatbar/Chatbar.tsx index c7c29ba..86d6871 100644 --- a/components/Chatbar/Chatbar.tsx +++ b/components/Chatbar/Chatbar.tsx @@ -1,28 +1,23 @@ -import { useCallback, useContext, useEffect } from 'react'; - +import { useContext, useEffect } from 'react'; import { useTranslation } from 'next-i18next'; +import { v4 as uuidv4 } from 'uuid'; import { useCreateReducer } from '@/hooks/useCreateReducer'; - import { saveConversation, saveConversations } from '@/utils/app/conversation'; import { saveFolders } from '@/utils/app/folders'; import { exportData, importData } from '@/utils/app/importExport'; - import { Conversation } from '@/types/chat'; import { LatestExportFormat, SupportedExportFormats } from '@/types/export'; - import HomeContext from '@/pages/api/home/home.context'; +import Sidebar from '../Sidebar'; + import { ChatFolders } from './components/ChatFolders'; import { ChatbarSettings } from './components/ChatbarSettings'; import { Conversations } from './components/Conversations'; - -import Sidebar from '../Sidebar'; import ChatbarContext from './Chatbar.context'; import { ChatbarInitialState, initialState } from './Chatbar.state'; -import { v4 as uuidv4 } from 'uuid'; - export const Chatbar = () => { const { t } = useTranslation('sidebar'); diff --git a/components/Chatbar/components/ChatFolders.tsx b/components/Chatbar/components/ChatFolders.tsx index faae6e2..d9e7ea4 100644 --- a/components/Chatbar/components/ChatFolders.tsx +++ b/components/Chatbar/components/ChatFolders.tsx @@ -1,9 +1,7 @@ import { useContext } from 'react'; import { FolderInterface } from '@/types/folder'; - import HomeContext from '@/pages/api/home/home.context'; - import Folder from '@/components/Folder'; import { ConversationComponent } from './Conversation'; diff --git a/components/Chatbar/components/ChatbarSettings.tsx b/components/Chatbar/components/ChatbarSettings.tsx index ba2931e..7660bc2 100644 --- a/components/Chatbar/components/ChatbarSettings.tsx +++ b/components/Chatbar/components/ChatbarSettings.tsx @@ -1,15 +1,14 @@ -import { IconFileExport, IconSettings } from '@tabler/icons-react'; import { useContext, useState } from 'react'; - import { useTranslation } from 'next-i18next'; +import { IconFileExport, IconSettings } from '@tabler/icons-react'; import HomeContext from '@/pages/api/home/home.context'; - import { SettingDialog } from '@/components/Settings/SettingDialog'; import { Import } from '../../Settings/Import'; import { SidebarButton } from '../../Sidebar/SidebarButton'; import ChatbarContext from '../Chatbar.context'; + import { ClearConversations } from './ClearConversations'; export const ChatbarSettings = () => { @@ -17,8 +16,7 @@ export const ChatbarSettings = () => { const [isSettingDialogOpen, setIsSettingDialog] = useState(false); const { - state: { lightMode, conversations }, - dispatch: homeDispatch, + state: { conversations } } = useContext(HomeContext); const { diff --git a/components/Chatbar/components/ClearConversations.tsx b/components/Chatbar/components/ClearConversations.tsx index 5ac218c..70a297d 100644 --- a/components/Chatbar/components/ClearConversations.tsx +++ b/components/Chatbar/components/ClearConversations.tsx @@ -1,6 +1,5 @@ import { IconCheck, IconTrash, IconX } from '@tabler/icons-react'; import { FC, useState } from 'react'; - import { useTranslation } from 'next-i18next'; import { SidebarButton } from '@/components/Sidebar/SidebarButton'; diff --git a/components/Chatbar/components/Conversation.tsx b/components/Chatbar/components/Conversation.tsx index b9c5739..b52ff48 100644 --- a/components/Chatbar/components/Conversation.tsx +++ b/components/Chatbar/components/Conversation.tsx @@ -15,9 +15,7 @@ import { } from 'react'; import { Conversation } from '@/types/chat'; - import HomeContext from '@/pages/api/home/home.context'; - import SidebarActionButton from '@/components/Buttons/SidebarActionButton'; import ChatbarContext from '@/components/Chatbar/Chatbar.context'; diff --git a/components/DataStreamDisplay/DataStreamDisplay.tsx b/components/DataStreamDisplay/DataStreamDisplay.tsx new file mode 100644 index 0000000..f158d14 --- /dev/null +++ b/components/DataStreamDisplay/DataStreamDisplay.tsx @@ -0,0 +1,170 @@ + +import React, { useEffect, useRef, useState } from 'react'; + +interface DataStreamDisplayProps { + dataStreams: string[]; + selectedStream: string; + onStreamChange: (_stream: string) => void; +} + +interface FinalizedDataEntry { + text: string; + stream_id: string; + timestamp: number; + id: string; + uuid?: string; + pending?: boolean; +} + +const DataStreamDisplayComponent: React.FC = React.memo(({ + dataStreams, + selectedStream, + onStreamChange +}) => { + const scrollRef = useRef(null); + const [text, setText] = useState(''); + const [lastDbUpdate, setLastDbUpdate] = useState(null); + + // Move polling logic into this component to isolate updates + useEffect(() => { + const interval = setInterval(async () => { + try { + // Get text for selected stream + const textRes = await fetch(`/api/update-data-stream?stream=${selectedStream}`); + if (textRes.ok) { + const textData = await textRes.json(); + if (typeof textData.text === 'string') { + setText(textData.text); + } + } + } catch (err) { + // Optionally handle error + } + }, 100); + return () => clearInterval(interval); + }, [selectedStream]); + + // Fetch last database update time for the selected stream + useEffect(() => { + const fetchLastDbUpdate = async () => { + try { + const response = await fetch(`/api/update-data-stream?type=finalized&stream=${selectedStream}`); + if (response.ok) { + const data = await response.json(); + const entries: FinalizedDataEntry[] = data.entries || []; + + if (entries.length > 0) { + // Find the most recent entry for this stream + const sortedEntries = entries.sort((a, b) => { + const timestampA = parseTimestampAsUTC(a.timestamp); + const timestampB = parseTimestampAsUTC(b.timestamp); + return timestampB - timestampA; // Most recent first + }); + + const latestTimestamp = parseTimestampAsUTC(sortedEntries[0].timestamp); + setLastDbUpdate(latestTimestamp); + } else { + setLastDbUpdate(null); + } + } + } catch (err) { + // Handle error silently + setLastDbUpdate(null); + } + }; + + if (selectedStream) { + fetchLastDbUpdate(); + // Check for updates every 5 seconds + const interval = setInterval(fetchLastDbUpdate, 5000); + return () => clearInterval(interval); + } + }, [selectedStream]); + + useEffect(() => { + if (scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }, [text]); + + const formatStreamName = (streamId: string) => { + return streamId || 'Default Stream'; + }; + + const parseTimestampAsUTC = (timestamp: string | number): number => { + if (typeof timestamp === 'number') return timestamp; + // Only add 'Z' if not already present + const utcString = timestamp.endsWith('Z') ? timestamp : timestamp + 'Z'; + console.log(`[SERVER] Timestamp debug ${utcString}`); + return new Date(utcString).getTime(); + }; + + const formatLastUpdateTime = (timestamp: number) => { + const now = Date.now(); + const diff = now - timestamp; + + // Less than 1 minute + if (diff < 60000) { + const seconds = Math.floor(diff / 1000); + return `${seconds}s ago`; + } + + // Less than 1 hour + if (diff < 3600000) { + const minutes = Math.floor(diff / 60000); + return `${minutes}m ago`; + } + + // Less than 1 day + if (diff < 86400000) { + const hours = Math.floor(diff / 3600000); + return `${hours}h ago`; + } + + // More than 1 day - show date + return new Date(timestamp).toLocaleDateString(); + }; + + return ( +
+
+
+

+ Live Data Streams +

+ + {lastDbUpdate ? `Last DB update: ${formatLastUpdateTime(lastDbUpdate)}` : 'No database updates yet'} + +
+ {dataStreams.length > 1 && ( +
+ + +
+ )} +
+
+

+ {text || 'Waiting for data...'} +

+
+
+ ); +}); + +DataStreamDisplayComponent.displayName = 'DataStreamDisplay'; + +export const DataStreamDisplay = DataStreamDisplayComponent; \ No newline at end of file diff --git a/components/DataStreamDisplay/DataStreamManager.tsx b/components/DataStreamDisplay/DataStreamManager.tsx new file mode 100644 index 0000000..eda1113 --- /dev/null +++ b/components/DataStreamDisplay/DataStreamManager.tsx @@ -0,0 +1,71 @@ +'use client'; + +import { useEffect, useContext } from 'react'; + +import HomeContext from '@/pages/api/home/home.context'; +import { Conversation } from '@/types/chat'; +import { saveConversation } from '@/utils/app/conversation'; + +import { DataStreamDisplay } from './DataStreamDisplay'; + +interface DataStreamManagerProps { + selectedConversation: Conversation | undefined; + dispatch: any; +} + +export const DataStreamManager = ({ + selectedConversation, + dispatch, +}: DataStreamManagerProps) => { + const { + state: { showDataStreamDisplay, dataStreams }, + } = useContext(HomeContext); + + const handleDataStreamChange = (stream: string) => { + if (selectedConversation) { + const updatedConversation = { + ...selectedConversation, + selectedStream: stream, + }; + dispatch({ field: 'selectedConversation', value: updatedConversation }); + saveConversation(updatedConversation); + } + }; + + // Poll /api/update-data-stream every 2 seconds to discover available streams + useEffect(() => { + const interval = setInterval(async () => { + try { + // Get available streams + const streamsRes = await fetch('/api/update-data-stream'); + if (streamsRes.ok) { + const streamsData = await streamsRes.json(); + if (streamsData.streams && Array.isArray(streamsData.streams)) { + // Only update if streams actually changed + const currentStreams = dataStreams || []; + const newStreams = streamsData.streams; + if (JSON.stringify(currentStreams.sort()) !== JSON.stringify(newStreams.sort())) { + dispatch({ field: 'dataStreams', value: newStreams }); + } + } + } + } catch (err) { + // Optionally handle error + } + }, 2000); // Less frequent polling for stream discovery + return () => clearInterval(interval); + }, [dispatch, dataStreams]); + + if (!showDataStreamDisplay || !selectedConversation) { + return null; + } + + return ( + 0 ? dataStreams[0] : 'default')} + onStreamChange={handleDataStreamChange} + /> + ); +}; + diff --git a/components/Folder/Folder.tsx b/components/Folder/Folder.tsx index 183261e..a623640 100644 --- a/components/Folder/Folder.tsx +++ b/components/Folder/Folder.tsx @@ -15,15 +15,13 @@ import { } from 'react'; import { FolderInterface } from '@/types/folder'; - import HomeContext from '@/pages/api/home/home.context'; - import SidebarActionButton from '@/components/Buttons/SidebarActionButton'; interface Props { currentFolder: FolderInterface; searchTerm: string; - handleDrop: (e: any, folder: FolderInterface) => void; + handleDrop: (_e: any, _folder: FolderInterface) => void; folderComponent: (ReactElement | undefined)[]; } diff --git a/components/Markdown/Chart.tsx b/components/Markdown/Chart.tsx index 1472b17..2206584 100644 --- a/components/Markdown/Chart.tsx +++ b/components/Markdown/Chart.tsx @@ -1,13 +1,8 @@ // Import html-to-image for generating images import { IconDownload } from '@tabler/icons-react'; -import React, { useContext } from 'react'; +import React from 'react'; import toast from 'react-hot-toast'; - import dynamic from 'next/dynamic'; - -// Import dynamic from Next.js -import HomeContext from '@/pages/api/home/home.context'; - import * as htmlToImage from 'html-to-image'; import { BarChart, @@ -68,11 +63,6 @@ const Chart = (props: any) => { Links = [], } = data; - const { - state: { selectedConversation, conversations }, - dispatch, - } = useContext(HomeContext); - const colors = { fill: '#76b900', stroke: 'black', diff --git a/components/Markdown/CodeBlock.tsx b/components/Markdown/CodeBlock.tsx index b71dfc7..c57bc77 100644 --- a/components/Markdown/CodeBlock.tsx +++ b/components/Markdown/CodeBlock.tsx @@ -1,8 +1,7 @@ import { IconCheck, IconClipboard, IconDownload } from '@tabler/icons-react'; -import { FC, memo, MouseEvent, useState } from 'react'; +import { FC, memo, useState } from 'react'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'; - import { useTranslation } from 'next-i18next'; import { diff --git a/components/Markdown/CustomComponents.tsx b/components/Markdown/CustomComponents.tsx index f5a458d..68e3d82 100644 --- a/components/Markdown/CustomComponents.tsx +++ b/components/Markdown/CustomComponents.tsx @@ -1,4 +1,5 @@ import { memo } from 'react'; +import { isEqual } from 'lodash'; import Chart from '@/components/Markdown/Chart'; import { CodeBlock } from '@/components/Markdown/CodeBlock'; @@ -7,180 +8,208 @@ import { CustomSummary } from '@/components/Markdown/CustomSummary'; import { Image } from '@/components/Markdown/Image'; import { Video } from '@/components/Markdown/Video'; -import { isEqual } from 'lodash'; +const CodeComponent = memo( + ({ + _node, + _inline, + className, + children, + ...props + }: { + children: React.ReactNode; + [key: string]: any; + }) => { + // if (children?.length) { + // if (children[0] === '▍') { + // return ; + // } + // children[0] = children.length > 0 ? (children[0] as string)?.replace("`▍`", "▍") : ''; + // } + + const match = /language-(\w+)/.exec(className || ''); + + return ( + 1 && match[1]) || ''} + value={String(children).replace(/\n$/, '')} + {...props} + /> + ); + }, + (prevProps, nextProps) => { + return isEqual(prevProps.children, nextProps.children); + }, +); +CodeComponent.displayName = 'CodeComponent'; + +const ChartComponent = memo( + ({ children }) => { + try { + const payload = JSON.parse(children[0].replaceAll('\n', '')); + return payload ? : null; + } catch (error) { + return null; + } + }, + (prevProps, nextProps) => + isEqual(prevProps.children, nextProps.children), +); +ChartComponent.displayName = 'ChartComponent'; + +const TableComponent = memo( + ({ children }) => ( + + {children} +
+ ), + (prevProps, nextProps) => + isEqual(prevProps.children, nextProps.children), +); +TableComponent.displayName = 'TableComponent'; + +const ThComponent = memo( + ({ children }) => ( + + {children} + + ), + (prevProps, nextProps) => + isEqual(prevProps.children, nextProps.children), +); +ThComponent.displayName = 'ThComponent'; + +const TdComponent = memo( + ({ children }) => ( + + {children} + + ), + (prevProps, nextProps) => + isEqual(prevProps.children, nextProps.children), +); +TdComponent.displayName = 'TdComponent'; + +const AComponent = memo( + ({ href, children, ...props }) => ( + + {children} + + ), + (prevProps, nextProps) => + isEqual(prevProps.children, nextProps.children), +); +AComponent.displayName = 'AComponent'; + +const LiComponent = memo( + ({ children, ...props }) => ( +
  • + {children} +
  • + ), + (prevProps, nextProps) => + isEqual(prevProps.children, nextProps.children), +); +LiComponent.displayName = 'LiComponent'; + +const SupComponent = memo( + ({ children, ...props }) => { + const validContent = Array.isArray(children) + ? children + .filter( + (child) => + typeof child === 'string' && + child.trim() && + child.trim() !== ',', + ) + .join('') + : typeof children === 'string' && + children.trim() && + children.trim() !== ',' + ? children + : null; + + return validContent ? ( + + {validContent} + + ) : null; + }, + (prevProps, nextProps) => + isEqual(prevProps.children, nextProps.children), +); +SupComponent.displayName = 'SupComponent'; + +const PComponent = memo( + ({ + children, + ...props + }: { + children: React.ReactNode; + [key: string]: any; + }) => { + return

    {children}

    ; + }, + (prevProps, nextProps) => { + return isEqual(prevProps.children, nextProps.children); + }, +); +PComponent.displayName = 'PComponent'; + +const ImgComponent = memo( + (props) => , + (prevProps, nextProps) => isEqual(prevProps, nextProps), +); +ImgComponent.displayName = 'ImgComponent'; + +const VideoComponent = memo( + (props) =>
    ); diff --git a/pages/_document.tsx b/pages/_document.tsx index 753f5c8..a6c71aa 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -1,8 +1,10 @@ import { DocumentProps, Head, Html, Main, NextScript } from 'next/document'; +import Script from 'next/script'; -import i18nextConfig from '../next-i18next.config'; import { APPLICATION_UI_NAME } from '@/constants/constants'; +import i18nextConfig from '../next-i18next.config'; + type Props = DocumentProps & { // add custom document props }; @@ -18,9 +20,9 @@ export default function Document(props: Props) { name="apple-mobile-web-app-title" content={APPLICATION_UI_NAME} > -