Compare commits
2 Commits
6bd0ea6d4f
...
4dcecd3e63
| Author | SHA1 | Date |
|---|---|---|
|
|
4dcecd3e63 | |
|
|
a167d9aaef |
|
|
@ -0,0 +1,146 @@
|
||||||
|
import { lazy, Suspense } from "react";
|
||||||
|
|
||||||
|
interface ChartData {
|
||||||
|
type: "bar" | "line" | "pie" | "area";
|
||||||
|
title: string;
|
||||||
|
data: Record<string, any>[];
|
||||||
|
xKey: string;
|
||||||
|
yKeys: string[];
|
||||||
|
colors: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lazy load do recharts para não pesar o bundle inicial
|
||||||
|
const RechartsComponents = lazy(() =>
|
||||||
|
import("recharts").then((module) => ({
|
||||||
|
default: function RechartsWrapper({ chart }: { chart: ChartData }) {
|
||||||
|
const {
|
||||||
|
BarChart,
|
||||||
|
Bar,
|
||||||
|
LineChart,
|
||||||
|
Line,
|
||||||
|
PieChart,
|
||||||
|
Pie,
|
||||||
|
AreaChart,
|
||||||
|
Area,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
CartesianGrid,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Cell,
|
||||||
|
} = module;
|
||||||
|
|
||||||
|
const renderChart = () => {
|
||||||
|
switch (chart.type) {
|
||||||
|
case "bar":
|
||||||
|
return (
|
||||||
|
<BarChart data={chart.data}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey={chart.xKey} />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip />
|
||||||
|
<Legend />
|
||||||
|
{chart.yKeys.map((key, index) => (
|
||||||
|
<Bar
|
||||||
|
key={key}
|
||||||
|
dataKey={key}
|
||||||
|
fill={chart.colors[index % chart.colors.length]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</BarChart>
|
||||||
|
);
|
||||||
|
case "line":
|
||||||
|
return (
|
||||||
|
<LineChart data={chart.data}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey={chart.xKey} />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip />
|
||||||
|
<Legend />
|
||||||
|
{chart.yKeys.map((key, index) => (
|
||||||
|
<Line
|
||||||
|
key={key}
|
||||||
|
type="monotone"
|
||||||
|
dataKey={key}
|
||||||
|
stroke={chart.colors[index % chart.colors.length]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</LineChart>
|
||||||
|
);
|
||||||
|
case "area":
|
||||||
|
return (
|
||||||
|
<AreaChart data={chart.data}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey={chart.xKey} />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip />
|
||||||
|
<Legend />
|
||||||
|
{chart.yKeys.map((key, index) => (
|
||||||
|
<Area
|
||||||
|
key={key}
|
||||||
|
type="monotone"
|
||||||
|
dataKey={key}
|
||||||
|
stroke={chart.colors[index % chart.colors.length]}
|
||||||
|
fill={chart.colors[index % chart.colors.length]}
|
||||||
|
fillOpacity={0.6}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</AreaChart>
|
||||||
|
);
|
||||||
|
case "pie":
|
||||||
|
return (
|
||||||
|
<PieChart>
|
||||||
|
<Tooltip />
|
||||||
|
<Legend />
|
||||||
|
<Pie
|
||||||
|
data={chart.data}
|
||||||
|
dataKey={chart.yKeys[0]}
|
||||||
|
nameKey={chart.xKey}
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
outerRadius={80}
|
||||||
|
label
|
||||||
|
>
|
||||||
|
{chart.data.map((_, index) => (
|
||||||
|
<Cell
|
||||||
|
key={`cell-${index}`}
|
||||||
|
fill={chart.colors[index % chart.colors.length]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
</PieChart>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full h-64">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
{renderChart()}
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
interface ChartRendererProps {
|
||||||
|
chart: ChartData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChartRenderer({ chart }: ChartRendererProps) {
|
||||||
|
return (
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<div className="w-full h-64 flex items-center justify-center bg-muted/20 rounded">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<RechartsComponents chart={chart} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
// Utilitários de exportação com lazy loading dinâmico
|
||||||
|
// Reduz o bundle inicial do Agent.tsx
|
||||||
|
|
||||||
|
export async function exportToPDF(content: string, prompt?: string, chartContainer?: HTMLElement | null) {
|
||||||
|
const [{ jsPDF }, html2canvas] = await Promise.all([
|
||||||
|
import("jspdf"),
|
||||||
|
import("html2canvas").then(m => m.default)
|
||||||
|
]);
|
||||||
|
|
||||||
|
const doc = new jsPDF();
|
||||||
|
const pageWidth = doc.internal.pageSize.getWidth();
|
||||||
|
const margin = 20;
|
||||||
|
const maxWidth = pageWidth - margin * 2;
|
||||||
|
|
||||||
|
doc.setFontSize(16);
|
||||||
|
doc.setTextColor(31, 51, 77);
|
||||||
|
doc.text("Arcádia Agent - Resultado", margin, 20);
|
||||||
|
|
||||||
|
let currentY = 30;
|
||||||
|
|
||||||
|
if (prompt) {
|
||||||
|
doc.setFontSize(10);
|
||||||
|
doc.setTextColor(90, 108, 125);
|
||||||
|
doc.text("Tarefa:", margin, currentY);
|
||||||
|
currentY += 7;
|
||||||
|
const promptLines = doc.splitTextToSize(prompt, maxWidth);
|
||||||
|
doc.text(promptLines, margin, currentY);
|
||||||
|
currentY += promptLines.length * 5 + 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
doc.setFontSize(12);
|
||||||
|
doc.setTextColor(31, 51, 77);
|
||||||
|
doc.text("Resposta:", margin, currentY);
|
||||||
|
currentY += 8;
|
||||||
|
|
||||||
|
const textContent = content.replace(/__CHART_DATA__[\s\S]*?__END_CHART_DATA__/g, "").trim();
|
||||||
|
doc.setFontSize(11);
|
||||||
|
const lines = doc.splitTextToSize(textContent, maxWidth);
|
||||||
|
doc.text(lines, margin, currentY);
|
||||||
|
currentY += lines.length * 5 + 10;
|
||||||
|
|
||||||
|
// Verifica se há dados de gráfico e captura
|
||||||
|
const chartDataMatch = content.match(/__CHART_DATA__([\s\S]*?)__END_CHART_DATA__/);
|
||||||
|
if (chartDataMatch && chartContainer) {
|
||||||
|
try {
|
||||||
|
const canvas = await html2canvas(chartContainer, {
|
||||||
|
backgroundColor: "#ffffff",
|
||||||
|
scale: 2,
|
||||||
|
});
|
||||||
|
const imgData = canvas.toDataURL("image/png");
|
||||||
|
const imgWidth = maxWidth;
|
||||||
|
const imgHeight = (canvas.height * imgWidth) / canvas.width;
|
||||||
|
|
||||||
|
if (currentY + imgHeight > doc.internal.pageSize.getHeight() - margin) {
|
||||||
|
doc.addPage();
|
||||||
|
currentY = margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
doc.addImage(imgData, "PNG", margin, currentY, imgWidth, imgHeight);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error capturing chart:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
doc.save("arcadia-resultado.pdf");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function exportToWord(content: string, prompt?: string) {
|
||||||
|
const { Document, Packer, Paragraph, TextRun } = await import("docx");
|
||||||
|
const { saveAs } = await import("file-saver");
|
||||||
|
|
||||||
|
const paragraphs = [];
|
||||||
|
|
||||||
|
paragraphs.push(
|
||||||
|
new Paragraph({
|
||||||
|
children: [new TextRun({ text: "Arcádia Agent - Resultado", bold: true, size: 32 })],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (prompt) {
|
||||||
|
paragraphs.push(
|
||||||
|
new Paragraph({ children: [] }),
|
||||||
|
new Paragraph({
|
||||||
|
children: [new TextRun({ text: "Tarefa: ", bold: true }), new TextRun({ text: prompt })],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
paragraphs.push(
|
||||||
|
new Paragraph({ children: [] }),
|
||||||
|
new Paragraph({
|
||||||
|
children: [new TextRun({ text: "Resposta:", bold: true })],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
content.split("\n").forEach((line) => {
|
||||||
|
paragraphs.push(new Paragraph({ children: [new TextRun({ text: line })] }));
|
||||||
|
});
|
||||||
|
|
||||||
|
const doc = new Document({
|
||||||
|
sections: [{ properties: {}, children: paragraphs }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const blob = await Packer.toBlob(doc);
|
||||||
|
saveAs(blob, "arcadia-resultado.docx");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function exportToText(content: string, prompt?: string) {
|
||||||
|
const { saveAs } = await import("file-saver");
|
||||||
|
const textContent = prompt ? `Tarefa: ${prompt}\n\nResposta:\n${content}` : content;
|
||||||
|
const blob = new Blob([textContent], { type: "text/plain;charset=utf-8" });
|
||||||
|
saveAs(blob, "arcadia-resultado.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function exportToCSV(content: string) {
|
||||||
|
const { saveAs } = await import("file-saver");
|
||||||
|
const lines = content.split("\n");
|
||||||
|
let csvContent = "";
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.includes("|")) {
|
||||||
|
const cells = line
|
||||||
|
.split("|")
|
||||||
|
.map((cell) => cell.trim())
|
||||||
|
.filter((cell) => cell && !cell.match(/^[-:]+$/));
|
||||||
|
if (cells.length > 0) {
|
||||||
|
csvContent += cells.map((cell) => `"${cell.replace(/"/g, """)}"`).join(",") + "\n";
|
||||||
|
}
|
||||||
|
} else if (line.includes("\t")) {
|
||||||
|
csvContent +=
|
||||||
|
line
|
||||||
|
.split("\t")
|
||||||
|
.map((cell) => `"${cell.trim().replace(/"/g, """)}"`)
|
||||||
|
.join(",") + "\n";
|
||||||
|
} else if (line.trim()) {
|
||||||
|
csvContent += `"${line.replace(/"/g, """)}"\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8" });
|
||||||
|
saveAs(blob, "arcadia-resultado.csv");
|
||||||
|
}
|
||||||
|
|
@ -54,13 +54,10 @@ import {
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { CardDescription } from "@/components/ui/card";
|
import { CardDescription } from "@/components/ui/card";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { jsPDF } from "jspdf";
|
|
||||||
import { Document, Packer, Paragraph, TextRun } from "docx";
|
|
||||||
import { saveAs } from "file-saver";
|
|
||||||
import html2canvas from "html2canvas";
|
|
||||||
import { BarChart, Bar, LineChart, Line, PieChart, Pie, AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, Cell } from "recharts";
|
|
||||||
import { ResultViewer } from "@/components/ResultViewer";
|
import { ResultViewer } from "@/components/ResultViewer";
|
||||||
import DevHistory from "@/components/DevHistory";
|
import DevHistory from "@/components/DevHistory";
|
||||||
|
import { ChartRenderer } from "@/components/ChartRenderer";
|
||||||
|
import { exportToPDF, exportToWord, exportToText, exportToCSV } from "@/lib/export-utils";
|
||||||
|
|
||||||
interface ChartData {
|
interface ChartData {
|
||||||
type: 'bar' | 'line' | 'pie' | 'area';
|
type: 'bar' | 'line' | 'pie' | 'area';
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@
|
||||||
"@tiptap/starter-kit": "^3.15.3",
|
"@tiptap/starter-kit": "^3.15.3",
|
||||||
"@types/adm-zip": "^0.5.7",
|
"@types/adm-zip": "^0.5.7",
|
||||||
"@types/bson": "^4.0.5",
|
"@types/bson": "^4.0.5",
|
||||||
|
"@types/compression": "^1.8.1",
|
||||||
"@types/file-saver": "^2.0.7",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/imap": "^0.8.43",
|
"@types/imap": "^0.8.43",
|
||||||
"@types/mailparser": "^3.4.6",
|
"@types/mailparser": "^3.4.6",
|
||||||
|
|
@ -66,6 +67,7 @@
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
|
"compression": "^1.8.1",
|
||||||
"connect-pg-simple": "^10.0.0",
|
"connect-pg-simple": "^10.0.0",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"docx": "^9.5.1",
|
"docx": "^9.5.1",
|
||||||
|
|
@ -7300,6 +7302,16 @@
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/compression": {
|
||||||
|
"version": "1.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.8.1.tgz",
|
||||||
|
"integrity": "sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/express": "*",
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/connect": {
|
"node_modules/@types/connect": {
|
||||||
"version": "3.4.38",
|
"version": "3.4.38",
|
||||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
||||||
|
|
@ -8506,6 +8518,69 @@
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/compressible": {
|
||||||
|
"version": "2.0.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
|
||||||
|
"integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": ">= 1.43.0 < 2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/compression": {
|
||||||
|
"version": "1.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
|
||||||
|
"integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bytes": "3.1.2",
|
||||||
|
"compressible": "~2.0.18",
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"negotiator": "~0.6.4",
|
||||||
|
"on-headers": "~1.1.0",
|
||||||
|
"safe-buffer": "5.2.1",
|
||||||
|
"vary": "~1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/compression/node_modules/debug": {
|
||||||
|
"version": "2.6.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
|
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/compression/node_modules/ms": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/compression/node_modules/negotiator": {
|
||||||
|
"version": "0.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
|
||||||
|
"integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/compression/node_modules/on-headers": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/concat-stream": {
|
"node_modules/concat-stream": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@
|
||||||
"@tiptap/starter-kit": "^3.15.3",
|
"@tiptap/starter-kit": "^3.15.3",
|
||||||
"@types/adm-zip": "^0.5.7",
|
"@types/adm-zip": "^0.5.7",
|
||||||
"@types/bson": "^4.0.5",
|
"@types/bson": "^4.0.5",
|
||||||
|
"@types/compression": "^1.8.1",
|
||||||
"@types/file-saver": "^2.0.7",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/imap": "^0.8.43",
|
"@types/imap": "^0.8.43",
|
||||||
"@types/mailparser": "^3.4.6",
|
"@types/mailparser": "^3.4.6",
|
||||||
|
|
@ -73,6 +74,7 @@
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
|
"compression": "^1.8.1",
|
||||||
"connect-pg-simple": "^10.0.0",
|
"connect-pg-simple": "^10.0.0",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"docx": "^9.5.1",
|
"docx": "^9.5.1",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import express, { type Express } from "express";
|
import express, { type Express } from "express";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import compression from "compression";
|
||||||
|
|
||||||
export function serveStatic(app: Express) {
|
export function serveStatic(app: Express) {
|
||||||
const distPath = path.resolve(__dirname, "public");
|
const distPath = path.resolve(__dirname, "public");
|
||||||
|
|
@ -10,7 +11,22 @@ export function serveStatic(app: Express) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use(express.static(distPath));
|
// Enable gzip compression for all responses
|
||||||
|
app.use(compression({
|
||||||
|
level: 6, // balance between speed and compression
|
||||||
|
filter: (req, res) => {
|
||||||
|
// Compress all JSON and static assets
|
||||||
|
if (req.headers['x-no-compression']) return false;
|
||||||
|
return compression.filter(req, res);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Serve static files with proper caching
|
||||||
|
app.use(express.static(distPath, {
|
||||||
|
maxAge: '1y', // Cache static assets for 1 year (they have hash in filename)
|
||||||
|
etag: true,
|
||||||
|
lastModified: true,
|
||||||
|
}));
|
||||||
|
|
||||||
// fall through to index.html if the file doesn't exist
|
// fall through to index.html if the file doesn't exist
|
||||||
app.use("*", (_req, res) => {
|
app.use("*", (_req, res) => {
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,31 @@ export default defineConfig({
|
||||||
build: {
|
build: {
|
||||||
outDir: path.resolve(import.meta.dirname, "dist/public"),
|
outDir: path.resolve(import.meta.dirname, "dist/public"),
|
||||||
emptyOutDir: true,
|
emptyOutDir: true,
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
// Ensure consistent hashing for caching
|
||||||
|
entryFileNames: "assets/[name]-[hash].js",
|
||||||
|
chunkFileNames: "assets/[name]-[hash].js",
|
||||||
|
assetFileNames: (assetInfo) => {
|
||||||
|
const info = assetInfo.name.split(".");
|
||||||
|
const ext = info[info.length - 1];
|
||||||
|
return `assets/[name]-[hash][extname]`;
|
||||||
|
},
|
||||||
|
// Manual chunks for better caching
|
||||||
|
manualChunks: {
|
||||||
|
// Vendor libraries that change less frequently
|
||||||
|
vendor: ["react", "react-dom", "wouter"],
|
||||||
|
// UI components
|
||||||
|
ui: ["@radix-ui/react-dialog", "@radix-ui/react-dropdown-menu", "@radix-ui/react-tabs"],
|
||||||
|
// Charts (heavy)
|
||||||
|
charts: ["recharts"],
|
||||||
|
// Export libraries (heavy, loaded on demand)
|
||||||
|
export: ["jspdf", "docx", "html2canvas"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Generate source maps for production debugging
|
||||||
|
sourcemap: true,
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue