feat: implement ai roadmap download

ai/roadmap
Arik Chakma 11 months ago
parent 6e0f170680
commit 1aa54662d6
  1. 2
      package.json
  2. 14
      pnpm-lock.yaml
  3. 5
      src/components/GenerateRoadmap/GenerateRoadmap.css
  4. 85
      src/components/GenerateRoadmap/GenerateRoadmap.tsx

@ -32,6 +32,7 @@
"astro": "^4.4.0", "astro": "^4.4.0",
"astro-compress": "^2.2.10", "astro-compress": "^2.2.10",
"clsx": "^2.1.0", "clsx": "^2.1.0",
"dom-to-image": "^2.6.0",
"dracula-prism": "^2.1.16", "dracula-prism": "^2.1.16",
"jose": "^5.2.2", "jose": "^5.2.2",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
@ -57,6 +58,7 @@
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.41.2", "@playwright/test": "^1.41.2",
"@tailwindcss/typography": "^0.5.10", "@tailwindcss/typography": "^0.5.10",
"@types/dom-to-image": "^2.6.7",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"@types/prismjs": "^1.26.3", "@types/prismjs": "^1.26.3",
"csv-parser": "^3.0.0", "csv-parser": "^3.0.0",

@ -35,6 +35,9 @@ dependencies:
clsx: clsx:
specifier: ^2.1.0 specifier: ^2.1.0
version: 2.1.0 version: 2.1.0
dom-to-image:
specifier: ^2.6.0
version: 2.6.0
dracula-prism: dracula-prism:
specifier: ^2.1.16 specifier: ^2.1.16
version: 2.1.16 version: 2.1.16
@ -106,6 +109,9 @@ devDependencies:
'@tailwindcss/typography': '@tailwindcss/typography':
specifier: ^0.5.10 specifier: ^0.5.10
version: 0.5.10(tailwindcss@3.4.1) version: 0.5.10(tailwindcss@3.4.1)
'@types/dom-to-image':
specifier: ^2.6.7
version: 2.6.7
'@types/js-cookie': '@types/js-cookie':
specifier: ^3.0.6 specifier: ^3.0.6
version: 3.0.6 version: 3.0.6
@ -1624,6 +1630,10 @@ packages:
'@types/ms': 0.7.34 '@types/ms': 0.7.34
dev: false dev: false
/@types/dom-to-image@2.6.7:
resolution: {integrity: sha512-me5VbCv+fcXozblWwG13krNBvuEOm6kA5xoa4RrjDJCNFOZSWR3/QLtOXimBHk1Fisq69Gx3JtOoXtg1N1tijg==}
dev: true
/@types/estree@1.0.5: /@types/estree@1.0.5:
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
dev: false dev: false
@ -2703,6 +2713,10 @@ packages:
entities: 4.5.0 entities: 4.5.0
dev: false dev: false
/dom-to-image@2.6.0:
resolution: {integrity: sha512-Dt0QdaHmLpjURjU7Tnu3AgYSF2LuOmksSGsUcE6ItvJoCWTBEmiMXcqBdNSAm9+QbbwD7JMoVsuuKX6ZVQv1qA==}
dev: false
/domelementtype@2.3.0: /domelementtype@2.3.0:
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
dev: false dev: false

@ -1,3 +1,8 @@
@font-face {
font-family: 'balsamiq';
src: url('/fonts/balsamiq.woff2');
}
svg text tspan { svg text tspan {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;

@ -22,8 +22,7 @@ export function GenerateRoadmap() {
const [hasSubmitted, setHasSubmitted] = useState<boolean>(false); const [hasSubmitted, setHasSubmitted] = useState<boolean>(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [roadmapTopic, setRoadmapTopic] = useState(''); const [roadmapTopic, setRoadmapTopic] = useState('');
const [nodes, setNodes] = useState<any[]>([]); const [generatedRoadmap, setGeneratedRoadmap] = useState('');
const [edges, setEdges] = useState<any[]>([]);
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => { const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
@ -79,23 +78,7 @@ export function GenerateRoadmap() {
} }
}, },
onStreamEnd: async (result) => { onStreamEnd: async (result) => {
const { nodes, edges } = generateAIRoadmapFromText(result); setGeneratedRoadmap(result);
setNodes(
nodes.map((node) => ({
...node,
// To reset the width and height of the node
// so that it can be calculated based on the content in the editor
width: undefined,
height: undefined,
style: {
...node.style,
width: undefined,
height: undefined,
},
})),
);
setEdges(edges);
}, },
}); });
@ -105,11 +88,25 @@ export function GenerateRoadmap() {
const editGeneratedRoadmap = async () => { const editGeneratedRoadmap = async () => {
pageProgressMessage.set('Redirecting to Editor'); pageProgressMessage.set('Redirecting to Editor');
const { nodes, edges } = generateAIRoadmapFromText(generatedRoadmap);
const { response, error } = await httpPost<{ const { response, error } = await httpPost<{
roadmapId: string; roadmapId: string;
}>(`${import.meta.env.PUBLIC_API_URL}/v1-edit-ai-generated-roadmap`, { }>(`${import.meta.env.PUBLIC_API_URL}/v1-edit-ai-generated-roadmap`, {
title: roadmapTopic, title: roadmapTopic,
nodes, nodes: nodes.map((node) => ({
...node,
// To reset the width and height of the node
// so that it can be calculated based on the content in the editor
width: undefined,
height: undefined,
style: {
...node.style,
width: undefined,
height: undefined,
},
})),
edges, edges,
}); });
@ -122,6 +119,46 @@ export function GenerateRoadmap() {
window.location.href = `${import.meta.env.PUBLIC_EDITOR_APP_URL}/${response.roadmapId}`; window.location.href = `${import.meta.env.PUBLIC_EDITOR_APP_URL}/${response.roadmapId}`;
}; };
const downloadGeneratedRoadmap = async () => {
pageProgressMessage.set('Downloading Roadmap');
const node = document.getElementById('roadmap-container');
if (!node) {
toast.error('Something went wrong');
return;
}
// Append a watermark to the bottom right of the image
const watermark = document.createElement('div');
watermark.className = 'flex justify-end absolute bottom-4 right-4 gap-2';
watermark.innerHTML = `
<span
class='rounded-md bg-black py-2 px-2 text-white'
>
roadmap.sh
</span>
`;
node.insertAdjacentElement('afterbegin', watermark);
try {
const domtoimage = (await import('dom-to-image')).default;
const dataUrl = await domtoimage.toJpeg(node, {
bgcolor: 'white',
quality: 1,
});
node?.removeChild(watermark);
const link = document.createElement('a');
link.download = `${roadmapTopic}-roadmap.jpg`;
link.href = dataUrl;
link.click();
pageProgressMessage.set('');
} catch (error) {
console.error(error);
toast.error('Something went wrong');
}
};
if (!hasSubmitted) { if (!hasSubmitted) {
return ( return (
<RoadmapSearch <RoadmapSearch
@ -175,7 +212,10 @@ export function GenerateRoadmap() {
</form> </form>
<div className="flex w-full items-center justify-between gap-2"> <div className="flex w-full items-center justify-between gap-2">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<button className="inline-flex items-center justify-center gap-2 rounded-md bg-yellow-400 py-1.5 pl-2.5 pr-3 text-xs font-medium transition-opacity duration-300 hover:bg-yellow-500 sm:text-sm"> <button
className="inline-flex items-center justify-center gap-2 rounded-md bg-yellow-400 py-1.5 pl-2.5 pr-3 text-xs font-medium transition-opacity duration-300 hover:bg-yellow-500 sm:text-sm"
onClick={downloadGeneratedRoadmap}
>
<Download size={15} /> <Download size={15} />
Download Download
</button> </button>
@ -198,7 +238,8 @@ export function GenerateRoadmap() {
</div> </div>
<div <div
ref={roadmapContainerRef} ref={roadmapContainerRef}
className="px-4 py-5 [&>svg]:mx-auto [&>svg]:max-w-[1300px] " id="roadmap-container"
className="relative px-4 py-5 [&>svg]:mx-auto [&>svg]:max-w-[1300px]"
/> />
</section> </section>
); );

Loading…
Cancel
Save