From cbe52f32b3282bd93b7acd3023fded1c3b4a370b Mon Sep 17 00:00:00 2001 From: Vectry Date: Mon, 9 Feb 2026 18:18:24 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20doc=20rendering=20=E2=80=94=20markdown?= =?UTF-8?q?=20prose=20styling,=20code=20blocks=20with=20copy=20button,=20p?= =?UTF-8?q?roper=20step=20parsing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- apps/web/package.json | 1 + apps/web/src/app/globals.css | 1 + apps/web/src/components/code-block.tsx | 55 +++++++++++++++++ apps/web/src/components/doc-viewer.tsx | 85 +++++++++++++++++--------- package-lock.json | 45 +++++++++++++- packages/llm/src/pipeline.ts | 25 +++++++- 6 files changed, 182 insertions(+), 30 deletions(-) create mode 100644 apps/web/src/components/code-block.tsx 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) => (
        1. - + {i + 1} -
          - {step} +
          + {step}
        2. ))}
      -
      -

      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);