Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 25e5c0d

Browse files
Merge branch 'bot_ui_mermaid_diagram_improvements' into 'master'
Bot UI: Mermaid diagram gestures, save button and other improvements See merge request postgres-ai/database-lab!905
2 parents 42b6de2 + ee687ee commit 25e5c0d

File tree

5 files changed

+330
-9
lines changed

5 files changed

+330
-9
lines changed

‎ui/cspell.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,12 @@
193193
"citus",
194194
"pgvector",
195195
"partman",
196-
"fstype"
196+
"fstype",
197+
"pgsql",
198+
"sqlalchemy",
199+
"tsql",
200+
"TSQL",
201+
"sparql",
202+
"SPARQL"
197203
]
198204
}

‎ui/packages/platform/src/pages/Bot/Messages/Message/CodeBlock.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
77
import FileCopyOutlinedIcon from '@material-ui/icons/FileCopyOutlined';
88
import CheckCircleOutlineIcon from '@material-ui/icons/CheckCircleOutline';
99
import CodeIcon from '@material-ui/icons/Code';
10+
import { formatLanguageName } from "../../utils";
1011

1112
const useStyles = makeStyles((theme) => ({
1213
container: {
@@ -131,7 +132,7 @@ export const CodeBlock = memo(({ value, language }: CodeBlockProps) => {
131132
className={classes.summaryText}
132133
>
133134
<CodeIcon className={classes.summaryTextIcon} />
134-
{expanded ? 'Hide' : 'Show'} code block ({codeLines.length} LOC)
135+
{expanded ? 'Hide' : 'Show'}{language ? ` ${formatLanguageName(language)}` : ''} code block ({codeLines.length} LOC)
135136
</Typography>
136137
</AccordionSummary>
137138
<AccordionDetails className={classes.details}>
Lines changed: 152 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,161 @@
1-
import React, { useEffect } from 'react';
1+
import React, { useCallback,useEffect,useRef,useState } from 'react';
22
import mermaid from 'mermaid';
3+
import { makeStyles } from "@material-ui/core";
4+
import { MermaidDiagramControls } from "./MermaidDiagramControls";
5+
import cn from "classnames";
36

47
type MermaidDiagramProps = {
58
chart: string
69
}
710

11+
const useStyles = makeStyles(
12+
() => ({
13+
container: {
14+
position: 'relative',
15+
width: '100%',
16+
overflow: 'hidden'
17+
},
18+
mermaid: {
19+
minHeight: 300,
20+
},
21+
}))
22+
23+
mermaid.initialize({ startOnLoad: true, er: { diagramPadding: 20, useMaxWidth: false } });
24+
825
export const MermaidDiagram = React.memo((props: MermaidDiagramProps) => {
926
const { chart } = props;
10-
mermaid.initialize({ startOnLoad: true });
27+
28+
const classes = useStyles();
29+
30+
// Consolidated state management
31+
const [diagramState, setDiagramState] = useState({
32+
scale: 1,
33+
position: { x: 0, y: 0 },
34+
dragging: false,
35+
startPosition: { x: 0, y: 0 },
36+
});
37+
38+
const [isDiagramValid, setDiagramValid] = useState<boolean | null>(null);
39+
const [diagramError, setDiagramError] = useState<string | null>(null)
40+
41+
const diagramRef = useRef<HTMLDivElement>(null);
42+
1143
useEffect(() => {
12-
mermaid.contentLoaded();
13-
}, [chart]);
14-
return <div className="mermaid">{chart}</div>;
15-
})
44+
let isMounted = true;
45+
if (isDiagramValid === null || chart) {
46+
mermaid.parse(chart)
47+
.then(() => {
48+
if (isMounted) {
49+
setDiagramValid(true);
50+
mermaid.contentLoaded();
51+
}
52+
})
53+
.catch((e) => {
54+
if (isMounted) {
55+
setDiagramValid(false);
56+
setDiagramError(e.message)
57+
console.error('Diagram contains errors:', e.message);
58+
}
59+
});
60+
}
61+
62+
return () => {
63+
isMounted = false;
64+
};
65+
}, [chart, isDiagramValid]);
66+
67+
const handleZoomIn = useCallback(() => {
68+
setDiagramState((prev) => ({
69+
...prev,
70+
scale: Math.min(prev.scale + 0.1, 2),
71+
}));
72+
}, []);
73+
74+
const handleZoomOut = useCallback(() => {
75+
setDiagramState((prev) => ({
76+
...prev,
77+
scale: Math.max(prev.scale - 0.1, 0.8),
78+
}));
79+
}, []);
80+
81+
const handleMouseDown = useCallback((event: React.MouseEvent) => {
82+
setDiagramState((prev) => ({
83+
...prev,
84+
dragging: true,
85+
startPosition: { x: event.clientX - prev.position.x, y: event.clientY - prev.position.y },
86+
}));
87+
}, []);
88+
89+
const handleMouseMove = useCallback((event: React.MouseEvent) => {
90+
if (diagramState.dragging) {
91+
setDiagramState((prev) => ({
92+
...prev,
93+
position: { x: event.clientX - prev.startPosition.x, y: event.clientY - prev.startPosition.y },
94+
}));
95+
}
96+
}, [diagramState.dragging]);
97+
98+
const handleMouseUp = useCallback(() => {
99+
setDiagramState((prev) => ({ ...prev, dragging: false }));
100+
}, []);
101+
102+
const handleTouchStart = useCallback((event: React.TouchEvent) => {
103+
const touch = event.touches[0];
104+
setDiagramState((prev) => ({
105+
...prev,
106+
dragging: true,
107+
startPosition: { x: touch.clientX - prev.position.x, y: touch.clientY - prev.position.y },
108+
}));
109+
}, []);
110+
111+
const handleTouchMove = useCallback((event: React.TouchEvent) => {
112+
if (diagramState.dragging) {
113+
const touch = event.touches[0];
114+
setDiagramState((prev) => ({
115+
...prev,
116+
position: { x: touch.clientX - prev.startPosition.x, y: touch.clientY - prev.startPosition.y },
117+
}));
118+
}
119+
}, [diagramState.dragging]);
120+
121+
const handleTouchEnd = useCallback(() => {
122+
setDiagramState((prev) => ({ ...prev, dragging: false }));
123+
}, []);
124+
125+
if (isDiagramValid === null) {
126+
return <p>Validating diagram...</p>;
127+
}
128+
129+
if (isDiagramValid) {
130+
return (
131+
<div className={classes.container}>
132+
<div
133+
className={cn("mermaid", classes.mermaid)}
134+
ref={diagramRef}
135+
style={{
136+
transform: `scale(${diagramState.scale}) translate(${diagramState.position.x}px, ${diagramState.position.y}px)`,
137+
transformOrigin: '0 0',
138+
cursor: diagramState.dragging ? 'grabbing' : 'grab',
139+
}}
140+
onMouseDown={handleMouseDown}
141+
onMouseMove={handleMouseMove}
142+
onMouseUp={handleMouseUp}
143+
onMouseLeave={handleMouseUp}
144+
onTouchStart={handleTouchStart}
145+
onTouchMove={handleTouchMove}
146+
onTouchEnd={handleTouchEnd}
147+
>
148+
{chart}
149+
</div>
150+
<MermaidDiagramControls
151+
handleZoomIn={handleZoomIn}
152+
handleZoomOut={handleZoomOut}
153+
diagramRef={diagramRef}
154+
sourceCode={chart}
155+
/>
156+
</div>
157+
);
158+
} else {
159+
return <p>{diagramError}</p>;
160+
}
161+
});
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import IconButton from "@material-ui/core/IconButton";
2+
import { ZoomInRounded, ZoomOutRounded, SaveAltRounded, FileCopyOutlined } from "@material-ui/icons";
3+
import { makeStyles } from "@material-ui/core";
4+
import React, { useCallback } from "react";
5+
import Divider from "@material-ui/core/Divider";
6+
7+
const useStyles = makeStyles(
8+
() => ({
9+
container: {
10+
display: 'flex',
11+
flexDirection: 'column',
12+
alignItems: 'center',
13+
14+
position: 'absolute',
15+
bottom: 20,
16+
right: 10,
17+
zIndex: 2,
18+
},
19+
controlButtons: {
20+
display: 'flex',
21+
flexDirection: 'column',
22+
alignItems: 'center',
23+
24+
border: '1px solid rgba(0, 0, 0, 0.12)',
25+
borderRadius: 8,
26+
27+
background: 'white',
28+
29+
"& .MuiIconButton-root": {
30+
fontSize: '1.5rem',
31+
color: 'rgba(0, 0, 0, 0.72)',
32+
padding: 8,
33+
'&:hover': {
34+
color: 'rgba(0, 0, 0, 0.95)',
35+
},
36+
'&:first-child': {
37+
borderRadius: '8px 8px 0 0',
38+
},
39+
'&:last-child': {
40+
borderRadius: ' 0 0 8px 8px',
41+
}
42+
}
43+
},
44+
divider: {
45+
width: 'calc(100% - 8px)',
46+
},
47+
actionButton: {
48+
fontSize: '1.5rem',
49+
color: 'rgba(0, 0, 0, 0.72)',
50+
padding: 8,
51+
marginBottom: 8,
52+
'&:hover': {
53+
color: 'rgba(0, 0, 0, 0.95)',
54+
},
55+
}
56+
}))
57+
58+
59+
type MermaidDiagramControlsProps = {
60+
handleZoomIn: () => void,
61+
handleZoomOut: () => void,
62+
diagramRef: React.RefObject<HTMLDivElement>,
63+
sourceCode: string
64+
}
65+
66+
export const MermaidDiagramControls = (props: MermaidDiagramControlsProps) => {
67+
const { sourceCode, handleZoomOut, handleZoomIn, diagramRef } = props;
68+
const classes = useStyles();
69+
70+
const handleSaveClick = useCallback(() => {
71+
if (diagramRef.current) {
72+
const svgElement = diagramRef.current.querySelector('svg');
73+
if (svgElement) {
74+
const svgData = new XMLSerializer().serializeToString(svgElement);
75+
const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
76+
const url = URL.createObjectURL(svgBlob);
77+
78+
const link = document.createElement('a');
79+
link.href = url;
80+
link.download = 'er-diagram.svg';
81+
document.body.appendChild(link);
82+
link.click();
83+
document.body.removeChild(link);
84+
85+
URL.revokeObjectURL(url);
86+
}
87+
}
88+
}, []);
89+
90+
const handleCopyClick = async () => {
91+
if ('clipboard' in navigator) {
92+
await navigator.clipboard.writeText(sourceCode);
93+
}
94+
}
95+
96+
return (
97+
<div className={classes.container}>
98+
<IconButton
99+
title="Copy contents"
100+
aria-label="Copy contents"
101+
className={classes.actionButton}
102+
onClick={handleCopyClick}
103+
>
104+
<FileCopyOutlined />
105+
</IconButton>
106+
<IconButton
107+
title="Download as SVG"
108+
aria-label="Download diagram as SVG"
109+
className={classes.actionButton}
110+
onClick={handleSaveClick}
111+
>
112+
<SaveAltRounded />
113+
</IconButton>
114+
115+
<div className={classes.controlButtons}>
116+
<IconButton
117+
onClick={handleZoomIn}
118+
title="Zoom In"
119+
aria-label="Zoom In"
120+
>
121+
<ZoomInRounded />
122+
</IconButton>
123+
<Divider className={classes.divider} />
124+
<IconButton
125+
onClick={handleZoomOut}
126+
title="Zoom Out"
127+
aria-label="Zoom Out"
128+
>
129+
<ZoomOutRounded />
130+
</IconButton>
131+
</div>
132+
</div>
133+
)
134+
}

‎ui/packages/platform/src/pages/Bot/utils.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,38 @@ export const createMessageFragment = (messages: DebugMessage[]): DocumentFragmen
5757
});
5858

5959
return fragment;
60-
};
60+
};
61+
62+
export const formatLanguageName = (language: string): string => {
63+
const specificCases: { [key: string]: string } = {
64+
"sql": "SQL",
65+
"pl/pgsql": "PL/pgSQL",
66+
"pl/python": "PL/Python",
67+
"json": "JSON",
68+
"yaml": "YAML",
69+
"html": "HTML",
70+
"xml": "XML",
71+
"css": "CSS",
72+
"csv": "CSV",
73+
"toml": "TOML",
74+
"ini": "INI",
75+
"r": "R",
76+
"php": "PHP",
77+
"sqlalchemy": "SQLAlchemy",
78+
"xslt": "XSLT",
79+
"xsd": "XSD",
80+
"ajax": "AJAX",
81+
"tsql": "TSQL",
82+
"pl/sql": "PL/SQL",
83+
"dax": "DAX",
84+
"sparql": "SPARQL"
85+
};
86+
87+
const normalizedLanguage = language.toLowerCase();
88+
89+
if (specificCases[normalizedLanguage]) {
90+
return specificCases[normalizedLanguage];
91+
} else {
92+
return language.charAt(0).toUpperCase() + language.slice(1).toLowerCase();
93+
}
94+
}

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /