feat: replace async renderer (#9253)

* feat: replace async renderer

* fix: balsamiq font
This commit is contained in:
Arik Chakma
2025-10-13 17:33:06 +06:00
committed by GitHub
parent 8247f19850
commit bb09fbd322
9 changed files with 59 additions and 31 deletions

View File

@@ -113,13 +113,21 @@ export function AICourseRoadmapView(props: AICourseRoadmapViewProps) {
onStream: async (result) => {
const roadmap = generateAICourseRoadmapStructure(result, true);
const { nodes, edges } = generateAIRoadmapFromText(roadmap);
const svg = await renderFlowJSON({ nodes, edges });
const svg = renderFlowJSON({ nodes, edges });
// it will be null only on server side
if (!svg) {
return;
}
replaceChildren(containerEl.current!, svg);
},
onStreamEnd: async (result) => {
const roadmap = generateAICourseRoadmapStructure(result, true);
const { nodes, edges } = generateAIRoadmapFromText(roadmap);
const svg = await renderFlowJSON({ nodes, edges });
const svg = renderFlowJSON({ nodes, edges });
// it will be null only on server side
if (!svg) {
return;
}
replaceChildren(containerEl.current!, svg);
setRoadmapStructure(roadmap);
setIsGenerating(false);
@@ -217,7 +225,7 @@ export function AICourseRoadmapView(props: AICourseRoadmapViewProps) {
);
return (
<div className="relative overflow-hidden mx-auto min-h-[500px] rounded-xl border border-gray-200 bg-white shadow-xs lg:max-w-5xl">
<div className="relative mx-auto min-h-[500px] overflow-hidden rounded-xl border border-gray-200 bg-white shadow-xs lg:max-w-5xl">
<AICourseOutlineHeader
course={course}
isLoading={isLoading}
@@ -237,7 +245,9 @@ export function AICourseRoadmapView(props: AICourseRoadmapViewProps) {
</div>
)}
{!isLoggedIn() && <LoginToView className="-mt-1 -mb-2 rounded-none border-none" />}
{!isLoggedIn() && (
<LoginToView className="-mt-1 -mb-2 rounded-none border-none" />
)}
{error && !isGenerating && !isLoggedIn() && (
<div className="absolute inset-0 flex h-full w-full flex-col items-center justify-center">

View File

@@ -133,13 +133,16 @@ export function GenerateRoadmap(props: GenerateRoadmapProps) {
const isKeyOnly = IS_KEY_ONLY_ROADMAP_GENERATION;
const renderRoadmap = async (roadmap: string) => {
const renderRoadmap = (roadmap: string) => {
const result = generateAICourseRoadmapStructure(roadmap);
const { nodes, edges } = generateAIRoadmapFromText(result);
const svg = await renderFlowJSON({ nodes, edges });
if (roadmapContainerRef?.current) {
replaceChildren(roadmapContainerRef?.current, svg);
const svg = renderFlowJSON({ nodes, edges });
const container = roadmapContainerRef?.current;
if (!svg || !container) {
return;
}
replaceChildren(container, svg);
};
const loadTermRoadmap = async (term: string) => {
@@ -221,7 +224,7 @@ export function GenerateRoadmap(props: GenerateRoadmapProps) {
});
}
await renderRoadmap(result);
renderRoadmap(result);
},
onStreamEnd: async (result) => {
result = result
@@ -358,7 +361,7 @@ export function GenerateRoadmap(props: GenerateRoadmapProps) {
}
const { term, title, data } = response;
await renderRoadmap(data);
renderRoadmap(data);
setCurrentRoadmap({
id: roadmapId,
@@ -557,7 +560,7 @@ export function GenerateRoadmap(props: GenerateRoadmapProps) {
<span>
<span
className={cn(
'mr-0.5 inline-block rounded-xl border px-1.5 text-center text-sm tabular-nums text-gray-800',
'mr-0.5 inline-block rounded-xl border px-1.5 text-center text-sm text-gray-800 tabular-nums',
{
'animate-pulse border-zinc-300 bg-zinc-300 text-zinc-300':
!roadmapLimit,
@@ -652,7 +655,7 @@ export function GenerateRoadmap(props: GenerateRoadmapProps) {
<div className="flex w-full 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"
className="inline-flex items-center justify-center gap-2 rounded-md bg-yellow-400 py-1.5 pr-3 pl-2.5 text-xs font-medium transition-opacity duration-300 hover:bg-yellow-500 sm:text-sm"
onClick={downloadGeneratedRoadmapContent}
>
<Download size={15} />
@@ -668,7 +671,7 @@ export function GenerateRoadmap(props: GenerateRoadmapProps) {
<div className="flex items-center justify-between gap-2">
<button
className="inline-flex items-center justify-center gap-2 rounded-md bg-gray-200 py-1.5 pl-2.5 pr-3 text-xs font-medium text-black transition-colors duration-300 hover:bg-gray-300 sm:text-sm"
className="inline-flex items-center justify-center gap-2 rounded-md bg-gray-200 py-1.5 pr-3 pl-2.5 text-xs font-medium text-black transition-colors duration-300 hover:bg-gray-300 sm:text-sm"
onClick={async () => {
const response = await saveAIRoadmap();
if (response?.roadmapSlug) {
@@ -685,7 +688,7 @@ export function GenerateRoadmap(props: GenerateRoadmapProps) {
</button>
<button
className="hidden items-center justify-center gap-2 rounded-md bg-gray-200 py-1.5 pl-2.5 pr-3 text-xs font-medium text-black transition-colors duration-300 hover:bg-gray-300 sm:inline-flex sm:text-sm"
className="hidden items-center justify-center gap-2 rounded-md bg-gray-200 py-1.5 pr-3 pl-2.5 text-xs font-medium text-black transition-colors duration-300 hover:bg-gray-300 sm:inline-flex sm:text-sm"
onClick={async () => {
const response = await saveAIRoadmap();
if (response?.roadmapId) {
@@ -718,7 +721,7 @@ export function GenerateRoadmap(props: GenerateRoadmapProps) {
className="relative min-h-[400px] px-4 py-5 [&>svg]:mx-auto [&>svg]:max-w-[1300px]"
/>
{!isAuthenticatedUser && (
<div className="absolute bottom-0 left-0 right-0">
<div className="absolute right-0 bottom-0 left-0">
<div className="h-80 w-full bg-linear-to-t from-gray-100 to-transparent" />
<div className="bg-gray-100">
<div className="mx-auto max-w-[600px] flex-col items-center justify-center bg-gray-100 px-5 pt-px">
@@ -726,7 +729,7 @@ export function GenerateRoadmap(props: GenerateRoadmapProps) {
<h2 className="mb-0.5 text-xl font-medium sm:mb-3 sm:text-2xl">
Sign up to View the full roadmap
</h2>
<p className="mb-6 text-balance text-sm text-gray-600 sm:text-base">
<p className="mb-6 text-sm text-balance text-gray-600 sm:text-base">
You must be logged in to view the complete roadmap
</p>
</div>

View File

@@ -2,22 +2,18 @@ import './ChatRoadmapRenderer.css';
import { lazy, useCallback, useEffect, useRef, useState } from 'react';
import {
renderResourceProgress,
updateResourceProgress,
type ResourceProgressType,
renderTopicProgress,
refreshProgressCounters,
} from '../../lib/resource-progress';
import { pageProgressMessage } from '../../stores/page';
import { useToast } from '../../hooks/use-toast';
import type { Edge, Node } from '@roadmapsh/editor';
import { slugify } from '../../lib/slugger';
import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup';
import { queryClient } from '../../stores/query-client';
import { userResourceProgressOptions } from '../../queries/resource-progress';
import { useQuery } from '@tanstack/react-query';
import { TopicResourcesModal } from './TopicResourcesModal';
const Renderer = lazy(() =>
import('@roadmapsh/editor').then((mod) => ({

View File

@@ -136,7 +136,7 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) {
}, [roadmapDetail]);
useEffect(() => {
if (!roadmapDetail || !roadmapContainerRef.current) {
if (!roadmapDetail || !roadmapContainerRef.current || !roadmapDetail.svg) {
return;
}

View File

@@ -108,12 +108,17 @@ export function MemberProgressModal(props: ProgressMapProps) {
const json = await res.json();
const svg =
renderer === 'editor'
? await renderFlowJSON(json)
? renderFlowJSON(json)
: await wireframeJSONToSVG(json, {
fontURL: '/fonts/balsamiq.woff2',
});
replaceChildren(containerEl.current!, svg);
const container = containerEl.current;
if (!svg || !container) {
return;
}
replaceChildren(container, svg);
}
useKeydown('Escape', () => {

View File

@@ -101,7 +101,7 @@ export function UserProgressModal(props: ProgressMapProps) {
}
return renderer === 'editor'
? await renderFlowJSON(roadmapJson as any)
? (renderFlowJSON(roadmapJson as any) as SVGElement)
: await wireframeJSONToSVG(roadmapJson, {
fontURL: '/fonts/balsamiq.woff2',
});
@@ -210,7 +210,7 @@ export function UserProgressModal(props: ProgressMapProps) {
return (
<div
id={'user-progress-modal'}
className="fixed left-0 right-0 top-0 z-100 h-full items-center justify-center overflow-y-auto overflow-x-hidden overscroll-contain bg-black/50"
className="fixed top-0 right-0 left-0 z-100 h-full items-center justify-center overflow-x-hidden overflow-y-auto overscroll-contain bg-black/50"
>
<div className="relative mx-auto h-full w-full max-w-4xl p-4 md:h-auto">
<div
@@ -230,7 +230,7 @@ export function UserProgressModal(props: ProgressMapProps) {
<button
type="button"
className={`absolute right-2.5 top-3 ml-auto inline-flex items-center rounded-lg bg-gray-100 bg-transparent p-1.5 text-sm text-gray-400 hover:text-gray-900 lg:hidden`}
className={`absolute top-3 right-2.5 ml-auto inline-flex items-center rounded-lg bg-gray-100 bg-transparent p-1.5 text-sm text-gray-400 hover:text-gray-900 lg:hidden`}
onClick={onClose}
>
<X className="h-4 w-4" />

View File

@@ -40,7 +40,11 @@ export function aiRoadmapOptions(roadmapSlug?: string) {
const result = generateAICourseRoadmapStructure(res.data);
const { nodes, edges } = generateAIRoadmapFromText(result);
const svg = await renderFlowJSON({ nodes, edges });
const svg = renderFlowJSON({ nodes, edges });
if (!svg) {
return res;
}
const svgHtml = svg.outerHTML;
return {
@@ -153,7 +157,11 @@ export async function generateAIRoadmap(options: GenerateAIRoadmapOptions) {
onMessage: async (message) => {
const result = generateAICourseRoadmapStructure(message);
const { nodes, edges } = generateAIRoadmapFromText(result);
const svg = await renderFlowJSON({ nodes, edges });
const svg = renderFlowJSON({ nodes, edges });
if (!svg) {
return;
}
onRoadmapSvgChange?.(svg);
},
onMessageEnd: async () => {

View File

@@ -23,7 +23,13 @@ export function roadmapJSONOptions(roadmapId: string) {
`${baseUrl}/${roadmapId}.json`,
);
const svg = await renderFlowJSON(roadmapJSON);
const svg = renderFlowJSON(roadmapJSON);
if (!svg) {
return {
json: roadmapJSON,
svg: null,
};
}
return {
json: roadmapJSON,

View File

@@ -5,7 +5,7 @@
@plugin 'tailwind-scrollbar';
@font-face {
font-family: 'Balsamiq Sans';
font-family: 'balsamiq';
src: url('/fonts/balsamiq.woff2') format('woff2');
font-weight: 400;
font-style: normal;
@@ -13,7 +13,7 @@
}
@theme {
--font-balsamiq: 'Balsamiq Sans', sans-serif;
--font-balsamiq: 'balsamiq', sans-serif;
}
/*