Skip to content

Commit 1f47016

Browse files
committed
Merging with main branch
Signed-off-by: Vineeth Kalluru <[email protected]>
1 parent fc982f6 commit 1f47016

File tree

4 files changed

+560
-2
lines changed

4 files changed

+560
-2
lines changed

components/Chat/ChatMessage.tsx

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ import rehypeRaw from 'rehype-raw';
3333
import remarkGfm from 'remark-gfm';
3434
import remarkMath from 'remark-math';
3535

36+
import { HtmlFileRenderer } from './HtmlFileRenderer';
37+
import { detectHtmlFileLinks, removeHtmlFileLinksFromContent, HtmlFileLink } from '@/utils/app/htmlFileDetector';
38+
3639
export interface Props {
3740
message: Message;
3841
messageIndex: number;
@@ -182,6 +185,12 @@ export const ChatMessage: FC<Props> = memo(
182185
};
183186
}, []);
184187

188+
// Detect HTML files in assistant messages
189+
const htmlFileLinks: HtmlFileLink[] =
190+
message.role === 'assistant'
191+
? detectHtmlFileLinks(message.content)
192+
: [];
193+
185194
const prepareContent = ({
186195
message = {} as Message,
187196
responseContent = true,
@@ -205,6 +214,32 @@ export const ChatMessage: FC<Props> = memo(
205214
return fixMalformedHtml(result)?.trim()?.replace(/\n\s+/, '\n ');
206215
};
207216

217+
// Prepare content with HTML file links removed to avoid duplicate display
218+
const prepareContentWithoutHtmlLinks = ({
219+
message = {} as Message,
220+
responseContent = true,
221+
intermediateStepsContent = false,
222+
role = 'assistant',
223+
} = {}) => {
224+
const { content = '', intermediateSteps = [] } = message;
225+
226+
if (role === 'user') return content.trim();
227+
228+
let result = '';
229+
if (intermediateStepsContent) {
230+
result += generateContentIntermediate(intermediateSteps);
231+
}
232+
233+
if (responseContent) {
234+
// Remove HTML file links from content
235+
const cleanContent = removeHtmlFileLinksFromContent(content);
236+
result += result ? `\n\n${cleanContent}` : cleanContent;
237+
}
238+
239+
// fixing malformed html and removing extra spaces to avoid markdown issues
240+
return fixMalformedHtml(result)?.trim()?.replace(/\n\s+/, '\n ');
241+
};
242+
208243
return (
209244
<div
210245
className={`group md:px-4 ${
@@ -316,7 +351,7 @@ export const ChatMessage: FC<Props> = memo(
316351
linkTarget="_blank"
317352
components={markdownComponents}
318353
>
319-
{prepareContent({
354+
{prepareContentWithoutHtmlLinks({
320355
message,
321356
role: 'assistant',
322357
intermediateStepsContent: true,
@@ -341,14 +376,28 @@ export const ChatMessage: FC<Props> = memo(
341376
linkTarget="_blank"
342377
components={markdownComponents}
343378
>
344-
{prepareContent({
379+
{prepareContentWithoutHtmlLinks({
345380
message,
346381
role: 'assistant',
347382
intermediateStepsContent: false,
348383
responseContent: true,
349384
})}
350385
</MemoizedReactMarkdown>
351386
</div>
387+
{/* HTML File Renderers */}
388+
{htmlFileLinks.length > 0 && (
389+
<div className="mt-4">
390+
{htmlFileLinks.map((htmlFile, index) => (
391+
<HtmlFileRenderer
392+
key={`${htmlFile.filePath}-${index}`}
393+
filePath={htmlFile.filePath}
394+
title={htmlFile.title}
395+
isInlineHtml={htmlFile.isInlineHtml}
396+
htmlContent={htmlFile.htmlContent}
397+
/>
398+
))}
399+
</div>
400+
)}
352401
<div className="mt-1 flex gap-1">
353402
{!messageIsStreaming && (
354403
<>
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
'use client';
2+
import React, { useState, useEffect } from 'react';
3+
import { IconEye, IconEyeOff, IconExternalLink, IconFile, IconDownload } from '@tabler/icons-react';
4+
5+
interface HtmlFileRendererProps {
6+
filePath: string;
7+
title?: string;
8+
isInlineHtml?: boolean;
9+
htmlContent?: string;
10+
}
11+
12+
export const HtmlFileRenderer: React.FC<HtmlFileRendererProps> = ({
13+
filePath,
14+
title,
15+
isInlineHtml = false,
16+
htmlContent: inlineHtmlContent
17+
}) => {
18+
const [isExpanded, setIsExpanded] = useState<boolean>(false);
19+
const [htmlContent, setHtmlContent] = useState<string>(inlineHtmlContent || '');
20+
const [isLoading, setIsLoading] = useState<boolean>(false);
21+
const [error, setError] = useState<string>('');
22+
23+
const cleanFilePath = (path: string): string => {
24+
// For inline HTML, return a descriptive name
25+
if (isInlineHtml) {
26+
return title || 'Inline HTML Content';
27+
}
28+
29+
// Remove any malformed prefixes or HTML artifacts
30+
let cleaned = path.replace(/^.*?href=["']?/, '');
31+
cleaned = cleaned.replace(/["'>].*$/, '');
32+
33+
// Remove file:// prefix for API call
34+
cleaned = cleaned.replace('file://', '');
35+
36+
return cleaned;
37+
};
38+
39+
const loadHtmlContent = async () => {
40+
// If it's inline HTML, content is already provided
41+
if (isInlineHtml && inlineHtmlContent) {
42+
setHtmlContent(inlineHtmlContent);
43+
return;
44+
}
45+
46+
if (isExpanded && !htmlContent && !error) {
47+
setIsLoading(true);
48+
setError('');
49+
50+
try {
51+
const cleanPath = cleanFilePath(filePath);
52+
console.log('Loading HTML file via API:', cleanPath);
53+
54+
const response = await fetch(`/api/load-html-file?path=${encodeURIComponent(cleanPath)}`);
55+
56+
if (!response.ok) {
57+
throw new Error(`Failed to load file: ${response.status} ${response.statusText}`);
58+
}
59+
60+
const content = await response.text();
61+
setHtmlContent(content);
62+
} catch (err: any) {
63+
console.error('Error loading HTML file:', err);
64+
setError(`Failed to load HTML file: ${err.message}`);
65+
} finally {
66+
setIsLoading(false);
67+
}
68+
}
69+
};
70+
71+
useEffect(() => {
72+
if (isExpanded) {
73+
loadHtmlContent();
74+
}
75+
}, [isExpanded, filePath, isInlineHtml, inlineHtmlContent]);
76+
77+
const openInSystemBrowser = () => {
78+
if (isInlineHtml) {
79+
// For inline HTML, create a blob URL and try to open it
80+
try {
81+
const blob = new Blob([inlineHtmlContent || ''], { type: 'text/html' });
82+
const url = URL.createObjectURL(blob);
83+
window.open(url, '_blank');
84+
// Clean up the URL after a delay
85+
setTimeout(() => URL.revokeObjectURL(url), 10000);
86+
} catch (error) {
87+
console.error('Error opening inline HTML:', error);
88+
alert('Unable to open inline HTML content in new window.');
89+
}
90+
return;
91+
}
92+
93+
const cleanPath = cleanFilePath(filePath);
94+
// Try to open in system file manager/browser
95+
try {
96+
// For desktop apps or Electron, this might work
97+
if ((window as any).electronAPI) {
98+
(window as any).electronAPI.openFile(cleanPath);
99+
} else {
100+
// Provide instructions to user
101+
alert(`To view this plot, please open the following file in your browser:\n\n${cleanPath}\n\nYou can copy this path and paste it into your browser's address bar.`);
102+
}
103+
} catch (error) {
104+
console.error('Error opening file:', error);
105+
alert(`To view this plot, please open the following file in your browser:\n\n${cleanPath}`);
106+
}
107+
};
108+
109+
const copyPathToClipboard = async () => {
110+
try {
111+
if (isInlineHtml) {
112+
// For inline HTML, copy the HTML content itself
113+
await navigator.clipboard.writeText(inlineHtmlContent || '');
114+
alert('HTML content copied to clipboard!');
115+
} else {
116+
const cleanPath = cleanFilePath(filePath);
117+
await navigator.clipboard.writeText(cleanPath);
118+
alert('File path copied to clipboard! Paste it into your browser address bar to view the plot.');
119+
}
120+
} catch (error) {
121+
console.error('Failed to copy to clipboard:', error);
122+
if (isInlineHtml) {
123+
alert('Failed to copy HTML content to clipboard.');
124+
} else {
125+
const cleanPath = cleanFilePath(filePath);
126+
alert(`Copy this path to your browser:\n\n${cleanPath}`);
127+
}
128+
}
129+
};
130+
131+
const displayPath = cleanFilePath(filePath);
132+
133+
return (
134+
<div className="my-4 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
135+
{/* Header */}
136+
<div className="bg-gradient-to-r from-green-50 to-blue-50 dark:from-green-900/20 dark:to-blue-900/20 px-4 py-3 flex justify-between items-center">
137+
<div className="flex items-center gap-3">
138+
<IconFile size={20} className="text-green-600 dark:text-green-400" />
139+
<div>
140+
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
141+
{title || (isInlineHtml ? 'Inline HTML Content' : 'Interactive Plot')}
142+
</span>
143+
<div className="flex items-center gap-2 mt-1">
144+
<span className="text-xs bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 px-2 py-1 rounded">
145+
{isInlineHtml ? 'Inline HTML' : 'HTML Plot'}
146+
</span>
147+
<span className="text-xs text-gray-500 dark:text-gray-400">
148+
{isInlineHtml ? 'HTML response content' : 'Interactive Bokeh visualization'}
149+
</span>
150+
</div>
151+
</div>
152+
</div>
153+
154+
<div className="flex items-center gap-2">
155+
<button
156+
onClick={() => setIsExpanded(!isExpanded)}
157+
className="flex items-center gap-2 px-4 py-2 text-sm bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors shadow-sm"
158+
>
159+
{isExpanded ? <IconEyeOff size={16} /> : <IconEye size={16} />}
160+
{isExpanded ? 'Hide' : 'Show'} {isInlineHtml ? 'Content' : 'Plot'}
161+
</button>
162+
163+
<button
164+
onClick={copyPathToClipboard}
165+
className="flex items-center gap-1 px-3 py-2 text-sm bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors"
166+
title={isInlineHtml ? "Copy HTML content" : "Copy file path"}
167+
>
168+
<IconDownload size={16} />
169+
</button>
170+
171+
{!isInlineHtml && (
172+
<button
173+
onClick={openInSystemBrowser}
174+
className="flex items-center gap-1 px-3 py-2 text-sm bg-gray-600 hover:bg-gray-700 text-white rounded-md transition-colors"
175+
title="Open instructions"
176+
>
177+
<IconExternalLink size={16} />
178+
</button>
179+
)}
180+
181+
{isInlineHtml && (
182+
<button
183+
onClick={openInSystemBrowser}
184+
className="flex items-center gap-1 px-3 py-2 text-sm bg-gray-600 hover:bg-gray-700 text-white rounded-md transition-colors"
185+
title="Open in new window"
186+
>
187+
<IconExternalLink size={16} />
188+
</button>
189+
)}
190+
</div>
191+
</div>
192+
193+
{/* Content */}
194+
{isExpanded && (
195+
<div className="p-4">
196+
{isLoading && (
197+
<div className="flex items-center justify-center py-8">
198+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-green-600"></div>
199+
<span className="ml-2 text-gray-600 dark:text-gray-400">Loading {isInlineHtml ? 'content' : 'plot'}...</span>
200+
</div>
201+
)}
202+
203+
{error && !isInlineHtml && (
204+
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md p-4">
205+
<p className="text-red-700 dark:text-red-400 text-sm font-medium mb-2">Could not load plot inline</p>
206+
<p className="text-red-600 dark:text-red-500 text-sm mb-3">{error}</p>
207+
<div className="space-y-2">
208+
<p className="text-sm text-gray-700 dark:text-gray-300">
209+
<strong>To view this plot:</strong>
210+
</p>
211+
<ol className="text-sm text-gray-600 dark:text-gray-400 list-decimal list-inside space-y-1">
212+
<li>Click the copy button above to copy the file path</li>
213+
<li>Open a new browser tab</li>
214+
<li>Paste the path into the address bar</li>
215+
<li>Press Enter to view the interactive plot</li>
216+
</ol>
217+
<button
218+
onClick={copyPathToClipboard}
219+
className="mt-3 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded-md transition-colors"
220+
>
221+
📋 Copy File Path
222+
</button>
223+
</div>
224+
</div>
225+
)}
226+
227+
{htmlContent && !error && (
228+
<div className="border border-gray-200 dark:border-gray-700 rounded-md overflow-hidden">
229+
<iframe
230+
srcDoc={htmlContent}
231+
className="w-full border-0"
232+
sandbox="allow-scripts allow-same-origin"
233+
title={title || (isInlineHtml ? 'Inline HTML Content' : 'HTML Content')}
234+
style={{ height: '600px', minHeight: '500px' }}
235+
/>
236+
</div>
237+
)}
238+
</div>
239+
)}
240+
241+
{/* File path info */}
242+
<div className="bg-gray-50 dark:bg-gray-800/50 px-4 py-2 border-t border-gray-200 dark:border-gray-700">
243+
<span className="text-xs text-gray-500 dark:text-gray-400 font-mono">
244+
{isInlineHtml ? 'Inline HTML Content' : displayPath}
245+
</span>
246+
</div>
247+
</div>
248+
);
249+
};

0 commit comments

Comments
 (0)