diff --git a/apps/web/package.json b/apps/web/package.json
index 7562e78..b743abc 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -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",
diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css
index 295bcee..8a5b753 100644
--- a/apps/web/src/app/globals.css
+++ b/apps/web/src/app/globals.css
@@ -1,4 +1,5 @@
@import "tailwindcss";
+@plugin "@tailwindcss/typography";
:root {
--background: #0a0a0f;
diff --git a/apps/web/src/components/code-block.tsx b/apps/web/src/components/code-block.tsx
new file mode 100644
index 0000000..98ef55a
--- /dev/null
+++ b/apps/web/src/components/code-block.tsx
@@ -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 (
+
+ {children}
+
+ );
+ }
+
+ 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 (
+
+ {language && (
+
+ {language}
+
+ )}
+
+
+ {codeString}
+
+
+ );
+}
diff --git a/apps/web/src/components/doc-viewer.tsx b/apps/web/src/components/doc-viewer.tsx
index 19299b7..e84d56a 100644
--- a/apps/web/src/components/doc-viewer.tsx
+++ b/apps/web/src/components/doc-viewer.tsx
@@ -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 (
+ {children}
+ );
+ }
+ return {children};
+ },
+};
+
+function Md({ children }: { children: string }) {
+ return (
+
+ {children}
+
+ );
+}
+
export function DocViewer({ docs }: DocViewerProps) {
const [activeSection, setActiveSection] = useState("overview");
const [expandedModules, setExpandedModules] = useState>(
@@ -116,9 +142,7 @@ export function DocViewer({ docs }: DocViewerProps) {
{docs.sections.overview.title}
-
- {docs.sections.overview.description}
-
+ {docs.sections.overview.description}
{docs.sections.overview.techStack.map((tech: string) => (
- {module.summary}
+
+ {module.summary}
+
{module.keyFiles.length > 0 && (
@@ -211,7 +237,9 @@ export function DocViewer({ docs }: DocViewerProps) {
{file.path}
-
{file.purpose}
+ {file.purpose && (
+
{file.purpose}
+ )}
))}
@@ -252,11 +280,11 @@ export function DocViewer({ docs }: DocViewerProps) {
Coding Conventions
-
+
{docs.sections.patterns.conventions.map((convention: string, i: number) => (
- -
- •
- {convention}
+
-
+ •
+ {convention}
))}
@@ -264,11 +292,11 @@ export function DocViewer({ docs }: DocViewerProps) {
Design Patterns
-
+
{docs.sections.patterns.designPatterns.map((pattern: string, i: number) => (
- -
- •
- {pattern}
+
-
+ •
+ {pattern}
))}
@@ -282,9 +310,9 @@ export function DocViewer({ docs }: DocViewerProps) {
{docs.sections.patterns.architecturalDecisions.map((decision: string, i: number) => (
- -
+
-
- {decision}
+ {decision}
))}
@@ -303,9 +331,9 @@ export function DocViewer({ docs }: DocViewerProps) {
Prerequisites
{docs.sections.gettingStarted.prerequisites.map((prereq: string, i: number) => (
- -
-
- {prereq}
+
-
+
+ {prereq}
))}
@@ -313,25 +341,26 @@ export function DocViewer({ docs }: DocViewerProps) {
Setup Steps
-
+
{docs.sections.gettingStarted.setupSteps.map((step: string, i: number) => (
-
-
+
{i + 1}
-
-
-
First Task
-
- {docs.sections.gettingStarted.firstTask}
-
+
+
First Task
+
+ A suggested first contribution to help you learn the codebase
+
+
{docs.sections.gettingStarted.firstTask}
diff --git a/package-lock.json b/package-lock.json
index 1f672c8..df4d688 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/packages/llm/src/pipeline.ts b/packages/llm/src/pipeline.ts
index 8cc7f19..90a0c30 100644
--- a/packages/llm/src/pipeline.ts
+++ b/packages/llm/src/pipeline.ts
@@ -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);