fix: doc rendering — markdown prose styling, code blocks with copy button, proper step parsing
- Add @tailwindcss/typography plugin for prose styling - Create CodeBlock component with copy button and language labels - Create Md wrapper component using ReactMarkdown with custom renderers - Replace all plain text renders with Md for proper markdown formatting - Fix parseSteps() in pipeline to group numbered steps with code blocks - Add First Task subtitle explaining its purpose - Add conditional file.purpose render in module key files
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@codeboard/shared": "*",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"bullmq": "^5.34.0",
|
||||
"ioredis": "^5.4.0",
|
||||
"lucide-react": "^0.563.0",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@import "tailwindcss";
|
||||
@plugin "@tailwindcss/typography";
|
||||
|
||||
:root {
|
||||
--background: #0a0a0f;
|
||||
|
||||
55
apps/web/src/components/code-block.tsx
Normal file
55
apps/web/src/components/code-block.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
|
||||
import { useState, type ReactNode } from "react";
|
||||
import { Check, Copy } from "lucide-react";
|
||||
|
||||
interface CodeBlockProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
inline?: boolean;
|
||||
}
|
||||
|
||||
export function CodeBlock({ children, className, inline }: CodeBlockProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
if (inline) {
|
||||
return (
|
||||
<code className="px-1.5 py-0.5 rounded bg-white/10 text-blue-300 text-[0.85em] font-mono border border-white/5">
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
|
||||
const language = className?.replace("language-", "") || "";
|
||||
const codeString = String(children).replace(/\n$/, "");
|
||||
|
||||
const handleCopy = async () => {
|
||||
await navigator.clipboard.writeText(codeString);
|
||||
setCopied(true);
|
||||
window.setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative group my-4">
|
||||
{language && (
|
||||
<div className="absolute top-0 left-0 px-3 py-1 text-[10px] font-medium uppercase tracking-wider text-zinc-500 bg-black/30 rounded-tl-lg rounded-br-lg border-b border-r border-white/5">
|
||||
{language}
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="absolute top-2 right-2 p-1.5 rounded-md bg-white/5 border border-white/10 text-zinc-500 hover:text-white hover:bg-white/10 transition-all opacity-0 group-hover:opacity-100 z-10"
|
||||
aria-label="Copy code"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="w-3.5 h-3.5 text-green-400" />
|
||||
) : (
|
||||
<Copy className="w-3.5 h-3.5" />
|
||||
)}
|
||||
</button>
|
||||
<pre className="overflow-x-auto rounded-lg bg-black/50 border border-white/10 p-4 pt-8 text-sm leading-relaxed font-mono scrollbar-thin">
|
||||
<code className={`text-zinc-300 ${className || ""}`}>{codeString}</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, type ComponentPropsWithoutRef } from "react";
|
||||
import type { GeneratedDocs, DocsModule } from "@codeboard/shared";
|
||||
import { MermaidDiagram } from "./mermaid-diagram";
|
||||
import { CodeBlock } from "./code-block";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import {
|
||||
BookOpen,
|
||||
@@ -21,6 +22,31 @@ interface DocViewerProps {
|
||||
docs: GeneratedDocs;
|
||||
}
|
||||
|
||||
const markdownComponents = {
|
||||
pre({ children, ...props }: ComponentPropsWithoutRef<"pre">) {
|
||||
return <>{children}</>;
|
||||
},
|
||||
code({ children, className, ...props }: ComponentPropsWithoutRef<"code"> & { inline?: boolean }) {
|
||||
const isBlock =
|
||||
className?.startsWith("language-") ||
|
||||
(typeof children === "string" && children.includes("\n"));
|
||||
if (isBlock) {
|
||||
return (
|
||||
<CodeBlock className={className}>{children}</CodeBlock>
|
||||
);
|
||||
}
|
||||
return <CodeBlock inline>{children}</CodeBlock>;
|
||||
},
|
||||
};
|
||||
|
||||
function Md({ children }: { children: string }) {
|
||||
return (
|
||||
<div className="prose prose-invert prose-sm max-w-none prose-p:text-zinc-300 prose-p:leading-relaxed prose-headings:text-white prose-strong:text-white prose-a:text-blue-400 prose-a:no-underline hover:prose-a:underline prose-li:text-zinc-300 prose-code:before:content-none prose-code:after:content-none prose-pre:bg-transparent prose-pre:p-0">
|
||||
<ReactMarkdown components={markdownComponents}>{children}</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DocViewer({ docs }: DocViewerProps) {
|
||||
const [activeSection, setActiveSection] = useState("overview");
|
||||
const [expandedModules, setExpandedModules] = useState<Set<string>>(
|
||||
@@ -116,9 +142,7 @@ export function DocViewer({ docs }: DocViewerProps) {
|
||||
<h1 className="text-3xl sm:text-4xl font-bold text-white mb-4">
|
||||
{docs.sections.overview.title}
|
||||
</h1>
|
||||
<p className="text-lg text-zinc-400">
|
||||
{docs.sections.overview.description}
|
||||
</p>
|
||||
<Md>{docs.sections.overview.description}</Md>
|
||||
<div className="flex flex-wrap gap-2 mt-4">
|
||||
{docs.sections.overview.techStack.map((tech: string) => (
|
||||
<span
|
||||
@@ -195,7 +219,9 @@ export function DocViewer({ docs }: DocViewerProps) {
|
||||
|
||||
{expandedModules.has(module.name) && (
|
||||
<div className="px-5 pb-5 border-t border-white/10">
|
||||
<p className="text-zinc-300 mt-4 mb-4">{module.summary}</p>
|
||||
<div className="mt-4 mb-4">
|
||||
<Md>{module.summary}</Md>
|
||||
</div>
|
||||
|
||||
{module.keyFiles.length > 0 && (
|
||||
<div className="mb-4">
|
||||
@@ -211,7 +237,9 @@ export function DocViewer({ docs }: DocViewerProps) {
|
||||
<FileCode className="w-4 h-4 text-zinc-500 mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<code className="text-blue-300">{file.path}</code>
|
||||
{file.purpose && (
|
||||
<p className="text-zinc-500">{file.purpose}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -252,11 +280,11 @@ export function DocViewer({ docs }: DocViewerProps) {
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="glass rounded-xl p-6">
|
||||
<h3 className="font-semibold text-white mb-4">Coding Conventions</h3>
|
||||
<ul className="space-y-2">
|
||||
<ul className="space-y-3">
|
||||
{docs.sections.patterns.conventions.map((convention: string, i: number) => (
|
||||
<li key={i} className="flex items-start gap-2 text-sm text-zinc-300">
|
||||
<span className="text-blue-400 mt-1">•</span>
|
||||
{convention}
|
||||
<li key={i} className="flex items-start gap-2 text-sm">
|
||||
<span className="text-blue-400 mt-1 flex-shrink-0">•</span>
|
||||
<Md>{convention}</Md>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -264,11 +292,11 @@ export function DocViewer({ docs }: DocViewerProps) {
|
||||
|
||||
<div className="glass rounded-xl p-6">
|
||||
<h3 className="font-semibold text-white mb-4">Design Patterns</h3>
|
||||
<ul className="space-y-2">
|
||||
<ul className="space-y-3">
|
||||
{docs.sections.patterns.designPatterns.map((pattern: string, i: number) => (
|
||||
<li key={i} className="flex items-start gap-2 text-sm text-zinc-300">
|
||||
<span className="text-blue-400 mt-1">•</span>
|
||||
{pattern}
|
||||
<li key={i} className="flex items-start gap-2 text-sm">
|
||||
<span className="text-blue-400 mt-1 flex-shrink-0">•</span>
|
||||
<Md>{pattern}</Md>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -282,9 +310,9 @@ export function DocViewer({ docs }: DocViewerProps) {
|
||||
</h3>
|
||||
<ul className="space-y-3">
|
||||
{docs.sections.patterns.architecturalDecisions.map((decision: string, i: number) => (
|
||||
<li key={i} className="flex items-start gap-3 text-zinc-300">
|
||||
<li key={i} className="flex items-start gap-3">
|
||||
<GitBranch className="w-5 h-5 text-blue-400 flex-shrink-0 mt-0.5" />
|
||||
{decision}
|
||||
<Md>{decision}</Md>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -303,9 +331,9 @@ export function DocViewer({ docs }: DocViewerProps) {
|
||||
<h3 className="font-semibold text-white mb-4">Prerequisites</h3>
|
||||
<ul className="space-y-2">
|
||||
{docs.sections.gettingStarted.prerequisites.map((prereq: string, i: number) => (
|
||||
<li key={i} className="flex items-center gap-2 text-sm text-zinc-300">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-green-400" />
|
||||
{prereq}
|
||||
<li key={i} className="flex items-start gap-2 text-sm">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-green-400 mt-1.5 flex-shrink-0" />
|
||||
<Md>{prereq}</Md>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -313,25 +341,26 @@ export function DocViewer({ docs }: DocViewerProps) {
|
||||
|
||||
<div className="glass rounded-xl p-6">
|
||||
<h3 className="font-semibold text-white mb-4">Setup Steps</h3>
|
||||
<ol className="space-y-4">
|
||||
<ol className="space-y-6">
|
||||
{docs.sections.gettingStarted.setupSteps.map((step: string, i: number) => (
|
||||
<li key={i} className="flex gap-4">
|
||||
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-blue-500/20 text-blue-300 text-sm flex items-center justify-center font-medium">
|
||||
<span className="flex-shrink-0 w-7 h-7 rounded-full bg-blue-500/20 text-blue-300 text-sm flex items-center justify-center font-medium mt-0.5">
|
||||
{i + 1}
|
||||
</span>
|
||||
<div className="prose prose-invert prose-sm max-w-none">
|
||||
<ReactMarkdown>{step}</ReactMarkdown>
|
||||
<div className="flex-1 min-w-0">
|
||||
<Md>{step}</Md>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div className="glass rounded-xl p-6 border-blue-500/20">
|
||||
<h3 className="font-semibold text-white mb-3">First Task</h3>
|
||||
<div className="prose prose-invert prose-sm max-w-none">
|
||||
<ReactMarkdown>{docs.sections.gettingStarted.firstTask}</ReactMarkdown>
|
||||
</div>
|
||||
<div className="glass rounded-xl p-6 border border-blue-500/20">
|
||||
<h3 className="font-semibold text-white mb-1">First Task</h3>
|
||||
<p className="text-sm text-zinc-500 mb-4">
|
||||
A suggested first contribution to help you learn the codebase
|
||||
</p>
|
||||
<Md>{docs.sections.gettingStarted.firstTask}</Md>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
45
package-lock.json
generated
45
package-lock.json
generated
@@ -24,6 +24,7 @@
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@codeboard/shared": "*",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"bullmq": "^5.34.0",
|
||||
"ioredis": "^5.4.0",
|
||||
"lucide-react": "^0.563.0",
|
||||
@@ -1491,6 +1492,18 @@
|
||||
"tailwindcss": "4.1.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/typography": {
|
||||
"version": "0.5.19",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz",
|
||||
"integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"postcss-selector-parser": "6.0.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__traverse": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
|
||||
@@ -2236,6 +2249,18 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/cssesc": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"cssesc": "bin/cssesc"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
@@ -5030,6 +5055,19 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-selector-parser": {
|
||||
"version": "6.0.10",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
|
||||
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/prisma": {
|
||||
"version": "6.19.2",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.2.tgz",
|
||||
@@ -5421,7 +5459,6 @@
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
|
||||
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
@@ -5723,6 +5760,12 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||
|
||||
@@ -23,6 +23,29 @@ function parseList(text: string): string[] {
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function parseSteps(text: string): string[] {
|
||||
const lines = text.split("\n");
|
||||
const steps: string[] = [];
|
||||
let current = "";
|
||||
|
||||
for (const line of lines) {
|
||||
if (/^\d+\.\s/.test(line)) {
|
||||
if (current.trim()) {
|
||||
steps.push(current.trim());
|
||||
}
|
||||
current = line.replace(/^\d+\.\s*/, "");
|
||||
} else {
|
||||
current += "\n" + line;
|
||||
}
|
||||
}
|
||||
|
||||
if (current.trim()) {
|
||||
steps.push(current.trim());
|
||||
}
|
||||
|
||||
return steps.filter(Boolean);
|
||||
}
|
||||
|
||||
export async function generateDocumentation(
|
||||
codeStructure: CodeStructure,
|
||||
provider: LLMProvider,
|
||||
@@ -112,7 +135,7 @@ export async function generateDocumentation(
|
||||
const gsResponse = await provider.chat(gsMessages);
|
||||
|
||||
const prerequisites = parseList(parseSection(gsResponse, "Prerequisites"));
|
||||
const setupSteps = parseList(parseSection(gsResponse, "Setup Steps"));
|
||||
const setupSteps = parseSteps(parseSection(gsResponse, "Setup Steps"));
|
||||
const firstTask = parseSection(gsResponse, "Your First Task");
|
||||
|
||||
onProgress?.("complete", 100);
|
||||
|
||||
Reference in New Issue
Block a user