Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions GUI/src/components/MessageContent/MessageContent.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
.message-content-wrapper {
width: 100%;

.message-text {
margin-bottom: 12px;
line-height: 1.6;
white-space: pre-wrap;
word-wrap: break-word;
}

.message-references {
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid rgba(0, 0, 0, 0.1);

.references-title {
display: block;
font-weight: 600;
margin-bottom: 8px;
font-size: 14px;
}

.references-list {
margin: 0;
padding-left: 20px;
list-style-type: decimal;

li {
margin-bottom: 6px;
line-height: 1.5;

&:last-child {
margin-bottom: 0;
}
}

.reference-link {
color: #0066cc;
text-decoration: none;
word-break: break-all;
transition: color 0.2s ease;

&:hover {
color: #0052a3;
text-decoration: underline;
}

&:visited {
color: #551a8b;
}
}
}
}
}

// Dark mode support
.test-production-llm__message--bot {
.message-references {
border-top-color: rgba(255, 255, 255, 0.1);
}
}
90 changes: 90 additions & 0 deletions GUI/src/components/MessageContent/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { FC } from 'react';
import './MessageContent.scss';

interface MessageContentProps {
content: string;
}

const MessageContent: FC<MessageContentProps> = ({ content }) => {
// Function to parse and render message content with proper formatting
const renderContent = () => {
// Split by **References:** pattern
const referencesMatch = content.match(/\*\*References:\*\*([\s\S]*)/);

if (!referencesMatch) {
// No references, return plain content with line breaks
return (
<div className="message-text">
{content.split('\n').map((line, index) => (
<span key={index}>
{line}
{index < content.split('\n').length - 1 && <br />}
</span>
))}
</div>
);
}

// Split content into main text and references
const mainText = content.substring(0, referencesMatch.index);
const referencesText = referencesMatch[1].trim();

// Parse numbered references with URLs
const referenceLines = referencesText
.split('\n')
.filter(line => line.trim())
.map(line => {
// Match pattern: "1. https://url" or "1. url"
const match = line.match(/^(\d+)\.\s+(https?:\/\/[^\s]+)/);
if (match) {
return {
number: match[1],
url: match[2],
};
}
return null;
})
.filter(Boolean);

return (
<div className="message-content-wrapper">
{/* Main text */}
{mainText && (
<div className="message-text">
{mainText.split('\n').map((line, index) => (
<span key={index}>
{line}
{index < mainText.split('\n').length - 1 && <br />}
</span>
))}
</div>
)}

{/* References section */}
{referenceLines.length > 0 && (
<div className="message-references">
<strong className="references-title">References:</strong>
<ol className="references-list">
{referenceLines.map((ref, index) => (
<li key={index}>
<a
href={ref!.url}
target="_blank"
rel="noopener noreferrer"
className="reference-link"
>
{ref!.url}
</a>
</li>
))}
</ol>
</div>
)}
</div>
);
};

return <>{renderContent()}</>;
};

export default MessageContent;
130 changes: 130 additions & 0 deletions GUI/src/hooks/useStreamingResponse.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { useState, useRef, useCallback, useEffect } from 'react';
import axios from 'axios';

interface StreamingOptions {
authorId: string;
conversationHistory: Array<{ authorRole: string; message: string; timestamp: string }>;
url: string;
}

interface UseStreamingResponseReturn {
startStreaming: (message: string, options: StreamingOptions, onToken: (token: string) => void, onComplete: () => void, onError: (error: string) => void) => Promise<void>;
stopStreaming: () => void;
isStreaming: boolean;
}

export const useStreamingResponse = (channelId: string): UseStreamingResponseReturn => {
const [isStreaming, setIsStreaming] = useState(false);
const eventSourceRef = useRef<EventSource | null>(null);

const stopStreaming = useCallback(() => {
if (eventSourceRef.current) {
console.log('[SSE] Closing connection');
eventSourceRef.current.close();
eventSourceRef.current = null;
}
setIsStreaming(false);
}, []);

// Cleanup on unmount
useEffect(() => {
return () => {
if (eventSourceRef.current) {
eventSourceRef.current.close();
}
};
}, []);

const startStreaming = useCallback(
async (
message: string,
options: StreamingOptions,
onToken: (token: string) => void,
onComplete: () => void,
onError: (error: string) => void
) => {
console.log('[SSE] Starting streaming for channel:', channelId);

// Close any existing connection
stopStreaming();

try {
// Step 1: Open SSE connection FIRST
const sseUrl = `https://est-rag-rtc.rootcode.software/notifications-server/sse/stream/${channelId}`;
console.log('[SSE] Connecting to:', sseUrl);

const eventSource = new EventSource(sseUrl);
eventSourceRef.current = eventSource;

eventSource.onopen = () => {
console.log('[SSE] Connection opened');
};

eventSource.onmessage = (event) => {
console.log('[SSE] Message received:', event.data);

try {
const data = JSON.parse(event.data);

if (data.type === 'stream_start') {
console.log('[SSE] Stream started');
setIsStreaming(true);
} else if (data.type === 'stream_chunk' && data.content) {
console.log('[SSE] Token:', data.content);
onToken(data.content);
} else if (data.type === 'stream_end') {
console.log('[SSE] Stream ended');
setIsStreaming(false);
eventSource.close();
eventSourceRef.current = null;
onComplete();
} else if (data.type === 'stream_error') {
console.error('[SSE] Stream error:', data.error);
setIsStreaming(false);
eventSource.close();
eventSourceRef.current = null;
onError(data.error || 'Stream error occurred');
}
} catch (e) {
console.error('[SSE] Failed to parse message:', e);
}
};

eventSource.onerror = (err) => {
console.error('[SSE] Connection error:', err);
setIsStreaming(false);
eventSource.close();
eventSourceRef.current = null;
onError('Connection error');
};

// Step 2: Wait a moment for SSE connection to establish, then trigger the stream
await new Promise(resolve => setTimeout(resolve, 500));

// Step 3: POST to trigger streaming
const postUrl = `https://est-rag-rtc.rootcode.software/notifications-server/channels/${channelId}/orchestrate/stream`;
console.log('[API] Triggering stream:', postUrl);

await axios.post(postUrl, {
message,
options,
});

console.log('[API] Stream triggered successfully');

} catch (err) {
console.error('[SSE] Error starting stream:', err);
stopStreaming();
onError(err instanceof Error ? err.message : 'Failed to start streaming');
}
},
[channelId, stopStreaming]
);

return {
startStreaming,
stopStreaming,
isStreaming,
};
};

Loading
Loading