Skip to content

Commit 7b1c830

Browse files
authored
Merge branch 'RAG-206' into encrypt-llm-keys
2 parents 30f05bb + 8b54764 commit 7b1c830

5 files changed

Lines changed: 370 additions & 110 deletions

File tree

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
.message-content-wrapper {
2+
width: 100%;
3+
4+
.message-text {
5+
margin-bottom: 12px;
6+
line-height: 1.6;
7+
white-space: pre-wrap;
8+
word-wrap: break-word;
9+
}
10+
11+
.message-references {
12+
margin-top: 16px;
13+
padding-top: 12px;
14+
border-top: 1px solid rgba(0, 0, 0, 0.1);
15+
16+
.references-title {
17+
display: block;
18+
font-weight: 600;
19+
margin-bottom: 8px;
20+
font-size: 14px;
21+
}
22+
23+
.references-list {
24+
margin: 0;
25+
padding-left: 20px;
26+
list-style-type: decimal;
27+
28+
li {
29+
margin-bottom: 6px;
30+
line-height: 1.5;
31+
32+
&:last-child {
33+
margin-bottom: 0;
34+
}
35+
}
36+
37+
.reference-link {
38+
color: #0066cc;
39+
text-decoration: none;
40+
word-break: break-all;
41+
transition: color 0.2s ease;
42+
43+
&:hover {
44+
color: #0052a3;
45+
text-decoration: underline;
46+
}
47+
48+
&:visited {
49+
color: #551a8b;
50+
}
51+
}
52+
}
53+
}
54+
}
55+
56+
// Dark mode support
57+
.test-production-llm__message--bot {
58+
.message-references {
59+
border-top-color: rgba(255, 255, 255, 0.1);
60+
}
61+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { FC } from 'react';
2+
import './MessageContent.scss';
3+
4+
interface MessageContentProps {
5+
content: string;
6+
}
7+
8+
const MessageContent: FC<MessageContentProps> = ({ content }) => {
9+
// Function to parse and render message content with proper formatting
10+
const renderContent = () => {
11+
// Split by **References:** pattern
12+
const referencesMatch = content.match(/\*\*References:\*\*([\s\S]*)/);
13+
14+
if (!referencesMatch) {
15+
// No references, return plain content with line breaks
16+
return (
17+
<div className="message-text">
18+
{content.split('\n').map((line, index) => (
19+
<span key={index}>
20+
{line}
21+
{index < content.split('\n').length - 1 && <br />}
22+
</span>
23+
))}
24+
</div>
25+
);
26+
}
27+
28+
// Split content into main text and references
29+
const mainText = content.substring(0, referencesMatch.index);
30+
const referencesText = referencesMatch[1].trim();
31+
32+
// Parse numbered references with URLs
33+
const referenceLines = referencesText
34+
.split('\n')
35+
.filter(line => line.trim())
36+
.map(line => {
37+
// Match pattern: "1. https://url" or "1. url"
38+
const match = line.match(/^(\d+)\.\s+(https?:\/\/[^\s]+)/);
39+
if (match) {
40+
return {
41+
number: match[1],
42+
url: match[2],
43+
};
44+
}
45+
return null;
46+
})
47+
.filter(Boolean);
48+
49+
return (
50+
<div className="message-content-wrapper">
51+
{/* Main text */}
52+
{mainText && (
53+
<div className="message-text">
54+
{mainText.split('\n').map((line, index) => (
55+
<span key={index}>
56+
{line}
57+
{index < mainText.split('\n').length - 1 && <br />}
58+
</span>
59+
))}
60+
</div>
61+
)}
62+
63+
{/* References section */}
64+
{referenceLines.length > 0 && (
65+
<div className="message-references">
66+
<strong className="references-title">References:</strong>
67+
<ol className="references-list">
68+
{referenceLines.map((ref, index) => (
69+
<li key={index}>
70+
<a
71+
href={ref!.url}
72+
target="_blank"
73+
rel="noopener noreferrer"
74+
className="reference-link"
75+
>
76+
{ref!.url}
77+
</a>
78+
</li>
79+
))}
80+
</ol>
81+
</div>
82+
)}
83+
</div>
84+
);
85+
};
86+
87+
return <>{renderContent()}</>;
88+
};
89+
90+
export default MessageContent;
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { useState, useRef, useCallback, useEffect } from 'react';
2+
import axios from 'axios';
3+
4+
interface StreamingOptions {
5+
authorId: string;
6+
conversationHistory: Array<{ authorRole: string; message: string; timestamp: string }>;
7+
url: string;
8+
}
9+
10+
interface UseStreamingResponseReturn {
11+
startStreaming: (message: string, options: StreamingOptions, onToken: (token: string) => void, onComplete: () => void, onError: (error: string) => void) => Promise<void>;
12+
stopStreaming: () => void;
13+
isStreaming: boolean;
14+
}
15+
16+
export const useStreamingResponse = (channelId: string): UseStreamingResponseReturn => {
17+
const [isStreaming, setIsStreaming] = useState(false);
18+
const eventSourceRef = useRef<EventSource | null>(null);
19+
20+
const stopStreaming = useCallback(() => {
21+
if (eventSourceRef.current) {
22+
console.log('[SSE] Closing connection');
23+
eventSourceRef.current.close();
24+
eventSourceRef.current = null;
25+
}
26+
setIsStreaming(false);
27+
}, []);
28+
29+
// Cleanup on unmount
30+
useEffect(() => {
31+
return () => {
32+
if (eventSourceRef.current) {
33+
eventSourceRef.current.close();
34+
}
35+
};
36+
}, []);
37+
38+
const startStreaming = useCallback(
39+
async (
40+
message: string,
41+
options: StreamingOptions,
42+
onToken: (token: string) => void,
43+
onComplete: () => void,
44+
onError: (error: string) => void
45+
) => {
46+
console.log('[SSE] Starting streaming for channel:', channelId);
47+
48+
// Close any existing connection
49+
stopStreaming();
50+
51+
try {
52+
// Step 1: Open SSE connection FIRST
53+
const sseUrl = `https://est-rag-rtc.rootcode.software/notifications-server/sse/stream/${channelId}`;
54+
console.log('[SSE] Connecting to:', sseUrl);
55+
56+
const eventSource = new EventSource(sseUrl);
57+
eventSourceRef.current = eventSource;
58+
59+
eventSource.onopen = () => {
60+
console.log('[SSE] Connection opened');
61+
};
62+
63+
eventSource.onmessage = (event) => {
64+
console.log('[SSE] Message received:', event.data);
65+
66+
try {
67+
const data = JSON.parse(event.data);
68+
69+
if (data.type === 'stream_start') {
70+
console.log('[SSE] Stream started');
71+
setIsStreaming(true);
72+
} else if (data.type === 'stream_chunk' && data.content) {
73+
console.log('[SSE] Token:', data.content);
74+
onToken(data.content);
75+
} else if (data.type === 'stream_end') {
76+
console.log('[SSE] Stream ended');
77+
setIsStreaming(false);
78+
eventSource.close();
79+
eventSourceRef.current = null;
80+
onComplete();
81+
} else if (data.type === 'stream_error') {
82+
console.error('[SSE] Stream error:', data.error);
83+
setIsStreaming(false);
84+
eventSource.close();
85+
eventSourceRef.current = null;
86+
onError(data.error || 'Stream error occurred');
87+
}
88+
} catch (e) {
89+
console.error('[SSE] Failed to parse message:', e);
90+
}
91+
};
92+
93+
eventSource.onerror = (err) => {
94+
console.error('[SSE] Connection error:', err);
95+
setIsStreaming(false);
96+
eventSource.close();
97+
eventSourceRef.current = null;
98+
onError('Connection error');
99+
};
100+
101+
// Step 2: Wait a moment for SSE connection to establish, then trigger the stream
102+
await new Promise(resolve => setTimeout(resolve, 500));
103+
104+
// Step 3: POST to trigger streaming
105+
const postUrl = `https://est-rag-rtc.rootcode.software/notifications-server/channels/${channelId}/orchestrate/stream`;
106+
console.log('[API] Triggering stream:', postUrl);
107+
108+
await axios.post(postUrl, {
109+
message,
110+
options,
111+
});
112+
113+
console.log('[API] Stream triggered successfully');
114+
115+
} catch (err) {
116+
console.error('[SSE] Error starting stream:', err);
117+
stopStreaming();
118+
onError(err instanceof Error ? err.message : 'Failed to start streaming');
119+
}
120+
},
121+
[channelId, stopStreaming]
122+
);
123+
124+
return {
125+
startStreaming,
126+
stopStreaming,
127+
isStreaming,
128+
};
129+
};
130+

0 commit comments

Comments
 (0)