Skip to content
Closed
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
1,495 changes: 1,445 additions & 50 deletions frontend/package-lock.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
"diff": "^8.0.2",
"lucide-react": "^0.552.0",
"marked": "^16.4.1",
"mermaid": "^11.12.2",
"papaparse": "^5.5.3",
"prosemirror-changeset": "^2.3.1",
"prosemirror-commands": "^1.7.1",
"prosemirror-history": "^1.4.1",
Expand All @@ -54,6 +56,7 @@
"react-dom": "^19.1.1",
"react-markdown": "^10.1.0",
"react-resizable-panels": "^3.0.6",
"recharts": "^3.6.0",
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.1",
"shiki": "^3.14.0",
Expand All @@ -73,6 +76,7 @@
"@testing-library/react": "^16.3.0",
"@types/markdown-it": "^14.1.2",
"@types/node": "^24.5.2",
"@types/papaparse": "^5.5.2",
"@types/react": "^19.1.13",
"@types/react-dom": "^19.1.9",
"@vitejs/plugin-react": "^4.7.0",
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/editor/CollaborativeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { uploadImage, isImageFile } from '../../services/upload-api';
import { useAuth } from '../../contexts/AuthContext';
import { EditorToolbar } from './EditorToolbar';
import { useWikiLinks } from '../../hooks/useWikiLinks';
import { createNodeViews } from '../../editor/nodeViews';

import './prosemirror.css';

Expand Down Expand Up @@ -439,6 +440,7 @@ export const CollaborativeEditor: React.FC<CollaborativeEditorProps> = ({
view = new EditorView(editorRef.current, {
state,
editable: () => editableRef.current,
nodeViews: createNodeViews(pagePath),
});

viewRef.current = view;
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/editor/ProseMirrorEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { schema, setCurrentPagePath } from '../../editor/schema';
import { markdownToProseMirror, prosemirrorToMarkdown } from '../../editor/conversion';
import { buildKeymap } from '../../editor/keymap';
import { useWikiLinks } from '../../hooks/useWikiLinks';
import { createNodeViews } from '../../editor/nodeViews';
import './prosemirror.css';

interface ProseMirrorEditorProps {
Expand Down Expand Up @@ -85,6 +86,7 @@ export const ProseMirrorEditor = forwardRef<ProseMirrorEditorHandle, ProseMirror
const view = new EditorView(editorRef.current, {
state,
editable: () => editable,
nodeViews: createNodeViews(pagePath || null),
dispatchTransaction(transaction: Transaction) {
const newState = view.state.apply(transaction);
view.updateState(newState);
Expand Down
188 changes: 188 additions & 0 deletions frontend/src/components/visualizations/ChartViewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { useMemo } from 'react';
import Papa from 'papaparse';
import {
LineChart,
BarChart,
PieChart,
AreaChart,
Line,
Bar,
Pie,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
Cell,
} from 'recharts';

export type ChartType = 'line' | 'bar' | 'pie' | 'area';

interface ChartViewerProps {
csvData: string;
chartType?: ChartType;
title?: string;
}

const COLORS = [
'hsl(var(--primary))',
'hsl(var(--secondary))',
'hsl(var(--accent))',
'#8884d8',
'#82ca9d',
'#ffc658',
'#ff7c7c',
'#a28dff',
];

function parseCSV(csv: string): Array<Record<string, any>> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security Concern: CSV Parsing Vulnerability

The current CSV parser is overly simplistic and vulnerable to CSV injection attacks. It only splits by commas without proper escaping or quoted field handling.

Issues:

  1. Fields with commas inside quotes aren't handled (e.g., "City, State" would be split incorrectly)
  2. No handling of escaped quotes or newlines within fields
  3. Could lead to data corruption or XSS if CSV data contains malicious content

Recommendation: Use a proper CSV parsing library like papaparse or csv-parse that handles:

  • Quoted fields with commas
  • Escaped quotes
  • Multi-line fields
  • Different delimiters

Example fix:

import Papa from 'papaparse';

function parseCSV(csv: string): Array<Record<string, any>> {
  const result = Papa.parse(csv, {
    header: true,
    dynamicTyping: true, // Auto-converts numbers
    skipEmptyLines: true,
  });
  return result.data;
}

// Use papaparse for proper CSV parsing (handles quotes, escapes, etc.)
const result = Papa.parse(csv.trim(), {
header: true,
skipEmptyLines: true,
dynamicTyping: true, // Auto-converts numbers
transform: (value) => {
// Handle empty strings - keep as string instead of converting to 0
const trimmed = value.trim();
if (trimmed === '') return trimmed;

// Try to parse as number, otherwise keep as string
const num = Number(trimmed);
return trimmed !== '' && !isNaN(num) ? num : trimmed;
},
});

if (result.errors.length > 0) {
console.error('CSV parsing errors:', result.errors);
}

return result.data as Array<Record<string, any>>;
}

function detectChartType(data: Array<Record<string, any>>): ChartType {
if (data.length === 0) return 'bar';

const keys = Object.keys(data[0]);
const numericKeys = keys.filter((key) => typeof data[0][key] === 'number');

// If only one numeric column and few rows, suggest pie chart
if (numericKeys.length === 1 && data.length <= 10) {
return 'pie';
}

// Default to line chart for time series data
return 'line';
}

export function ChartViewer({ csvData, chartType, title }: ChartViewerProps) {
const data = useMemo(() => parseCSV(csvData), [csvData]);

const detectedChartType = chartType || detectChartType(data);

if (data.length === 0) {
return (
<div className="flex items-center justify-center h-64 border rounded-md bg-muted/10">
<p className="text-muted-foreground">No data to display</p>
</div>
);
}

const keys = Object.keys(data[0]);
const xKey = keys[0]; // First column is X-axis
const dataKeys = keys.slice(1).filter((key) => typeof data[0][key] === 'number');

const renderChart = () => {
switch (detectedChartType) {
case 'line':
return (
<LineChart data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey={xKey} />
<YAxis />
<Tooltip />
<Legend />
{dataKeys.map((key, index) => (
<Line
key={key}
type="monotone"
dataKey={key}
stroke={COLORS[index % COLORS.length]}
strokeWidth={2}
/>
))}
</LineChart>
);

case 'bar':
return (
<BarChart data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey={xKey} />
<YAxis />
<Tooltip />
<Legend />
{dataKeys.map((key, index) => (
<Bar key={key} dataKey={key} fill={COLORS[index % COLORS.length]} />
))}
</BarChart>
);

case 'area':
return (
<AreaChart data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey={xKey} />
<YAxis />
<Tooltip />
<Legend />
{dataKeys.map((key, index) => (
<Area
key={key}
type="monotone"
dataKey={key}
fill={COLORS[index % COLORS.length]}
stroke={COLORS[index % COLORS.length]}
/>
))}
</AreaChart>
);

case 'pie':
// For pie charts, use the first numeric column
const pieDataKey = dataKeys[0];
return (
<PieChart>
<Pie
data={data}
dataKey={pieDataKey}
nameKey={xKey}
cx="50%"
cy="50%"
outerRadius={80}
label
>
{data.map((_entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip />
<Legend />
</PieChart>
);

default:
return null;
}
};

return (
<div className="w-full my-4">
{title && <h3 className="text-lg font-semibold mb-2">{title}</h3>}
<ResponsiveContainer width="100%" height={400}>
{renderChart()}
</ResponsiveContainer>
</div>
);
}
59 changes: 59 additions & 0 deletions frontend/src/components/visualizations/MermaidDiagram.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useEffect, useRef, useState } from 'react';
import mermaid from 'mermaid';

interface MermaidDiagramProps {
chart: string;
}

// Initialize mermaid once
mermaid.initialize({
startOnLoad: false,
theme: 'default',
securityLevel: 'strict',
fontFamily: 'inherit',
});

export function MermaidDiagram({ chart }: MermaidDiagramProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [error, setError] = useState<string | null>(null);
const [id] = useState(() => `mermaid-${Math.random().toString(36).substring(2, 11)}`);

useEffect(() => {
const renderDiagram = async () => {
if (!containerRef.current || !chart.trim()) return;

try {
setError(null);
// Clear previous content
containerRef.current.innerHTML = '';

// Render the diagram
const { svg } = await mermaid.render(id, chart.trim());
containerRef.current.innerHTML = svg;
} catch (err) {
console.error('Mermaid rendering error:', err);
setError(err instanceof Error ? err.message : 'Failed to render diagram');
}
};

renderDiagram();
}, [chart, id]);

if (error) {
return (
<div className="border border-destructive rounded-md p-4 my-4 bg-destructive/10">
<p className="text-sm text-destructive">
<strong>Mermaid Error:</strong> {error}
</p>
<pre className="mt-2 text-xs overflow-x-auto">{chart}</pre>
</div>
);
}

return (
<div
ref={containerRef}
className="mermaid-container flex justify-center items-center my-4 p-4 bg-muted/10 rounded-md overflow-x-auto"
/>
);
}
Loading