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
53 changes: 46 additions & 7 deletions site/components/DataPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,45 @@ interface DataPreviewProps {
data: DataRow[];
}

// Helper function to convert data to CSV format
const convertToCSV = (data: DataRow[]): string => {
if (data.length === 0) return '';

const headers = Object.keys(data[0]);
const csvHeaders = headers.join(',');

const csvRows = data.map(row => {
return headers.map(header => {
const value = row[header];
// Handle values that contain commas or quotes
if (value === null || value === undefined) return '';
const stringValue = String(value);
if (stringValue.includes(',') || stringValue.includes('"') || stringValue.includes('\n')) {
return `"${stringValue.replace(/"/g, '""')}"`;
}
return stringValue;
}).join(',');
});

return [csvHeaders, ...csvRows].join('\n');
};

// Helper function to download CSV
const downloadCSV = (data: DataRow[]) => {
const csvContent = convertToCSV(data);
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);

link.setAttribute('href', url);
link.setAttribute('download', `data_${new Date().getTime()}.csv`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};

const DataPreview: React.FC<DataPreviewProps> = ({ data }) => {
const formatValue = (value: string | number | boolean | null): string => {
if (value === null || value === undefined) return '-';
Expand Down Expand Up @@ -40,13 +79,13 @@ const DataPreview: React.FC<DataPreviewProps> = ({ data }) => {
<span className="flex items-center justify-center w-6 h-6 bg-[#78d3f8]/20 text-[#78d3f8] text-sm font-semibold rounded-full">2</span>
<h2 className="text-lg font-semibold text-gray-800">Structured Data Preview</h2>
</div>
<div className="flex items-center gap-2 text-gray-400">
<button className="p-2 hover:bg-gray-100 rounded-lg transition-colors">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
</svg>
</button>
<button className="p-2 hover:bg-gray-100 rounded-lg transition-colors">
<div className="flex items-center gap-2">
<button
onClick={() => downloadCSV(data)}
disabled={data.length === 0}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors text-gray-400 hover:text-[#78d3f8] disabled:opacity-50 disabled:cursor-not-allowed"
title="Download as CSV"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
Expand Down
89 changes: 88 additions & 1 deletion site/components/Visualization.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,40 @@ interface VisualizationProps {
isInitialized: boolean;
}

// Helper function to download HTML
const downloadHTML = (html: string) => {
const blob = new Blob([html], { type: 'text/html;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);

link.setAttribute('href', url);
link.setAttribute('download', `chart_${new Date().getTime()}.html`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};

// Helper function to copy HTML to clipboard
const copyToClipboard = async (html: string): Promise<boolean> => {
try {
await navigator.clipboard.writeText(html);
return true;
} catch (err) {
console.error('Failed to copy:', err);
return false;
}
};

const Visualization: React.FC<VisualizationProps> = ({ avaInstance, data, isInitialized }) => {
const [query, setQuery] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [result, setResult] = useState<AnalysisResponse | null>(null);
const [error, setError] = useState<string | null>(null);
const [showCode, setShowCode] = useState(false);
const [isHoveringChart, setIsHoveringChart] = useState(false);
const [copySuccess, setCopySuccess] = useState(false);
const iframeRef = useRef<HTMLIFrameElement>(null);

// Restore query and result from localStorage on mount
Expand Down Expand Up @@ -92,6 +120,24 @@ const Visualization: React.FC<VisualizationProps> = ({ avaInstance, data, isInit
// Get analysis code (JavaScript or SQL)
const analysisCode = result?.code || result?.sql;

// Handle copy action
const handleCopy = async () => {
if (result?.visualizationHTML) {
const success = await copyToClipboard(result.visualizationHTML);
if (success) {
setCopySuccess(true);
setTimeout(() => setCopySuccess(false), 2000);
}
}
};

// Handle download action
const handleDownload = () => {
if (result?.visualizationHTML) {
downloadHTML(result.visualizationHTML);
}
};

return (
<div className="bg-white rounded-2xl border border-gray-100 p-6 shadow-sm">
<div className="flex items-center gap-3 mb-5">
Expand Down Expand Up @@ -177,7 +223,48 @@ const Visualization: React.FC<VisualizationProps> = ({ avaInstance, data, isInit

{/* Visualization Area - Only show when visualizationHTML exists */}
{result?.visualizationHTML && (
<div className="min-h-[400px] border-2 border-dashed border-[#78d3f8]/20 rounded-xl overflow-hidden bg-gradient-to-b from-gray-50 to-white">
<div
className="relative min-h-[400px] border-2 border-dashed border-[#78d3f8]/20 rounded-xl overflow-hidden bg-gradient-to-b from-gray-50 to-white"
onMouseEnter={() => setIsHoveringChart(true)}
onMouseLeave={() => setIsHoveringChart(false)}
>
{/* Action buttons overlay */}
{isHoveringChart && (
<div className="absolute top-3 right-3 z-10 flex items-center gap-2 bg-white/90 backdrop-blur-sm rounded-lg shadow-lg p-1 border border-gray-200">
<button
onClick={handleCopy}
className="flex items-center gap-1.5 px-3 py-1.5 hover:bg-gray-100 rounded-md transition-colors text-sm text-gray-700"
title="Copy HTML"
>
{copySuccess ? (
<>
<svg className="w-4 h-4 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className="text-green-500">Copied!</span>
</>
) : (
<>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
<span>Copy</span>
</>
)}
</button>
<button
onClick={handleDownload}
className="flex items-center gap-1.5 px-3 py-1.5 hover:bg-gray-100 rounded-md transition-colors text-sm text-gray-700"
title="Download HTML"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
<span>Download</span>
</button>
</div>
)}

<iframe
ref={iframeRef}
className="w-full h-[400px] border-0"
Expand Down