use source dir

This commit is contained in:
zane
2026-03-15 17:26:48 -04:00
parent 87a6199556
commit 4b222ab6bf
38 changed files with 28 additions and 365 deletions
-35
View File
@@ -1,35 +0,0 @@
export const PHASES = {
1: {
name: 'Capable Single Agent',
steps: ['00', '01', '02', '03', '04', '05', '06'],
description: 'Build a fully-functional agent that can chat, use tools, learn skills, remember conversations, and access the internet.',
},
2: {
name: 'Event-Driven Architecture',
steps: ['07', '08', '09', '10'],
description: 'Refactor to event-driven architecture for scalability and multi-platform support.',
},
3: {
name: 'Autonomous & Multi-Agent',
steps: ['11', '12', '13', '14', '15'],
description: 'Add scheduled tasks, agent collaboration, and intelligent routing.',
},
4: {
name: 'Production & Scale',
steps: ['16', '17'],
description: 'Production features for reliability and long-term memory.',
},
} as const
export const SKIP_PATTERNS = [
'**/.venv/**',
'**/__pycache__/**',
'**/node_modules/**',
'**/*.lock',
'**/*.svg',
'**/*.png',
'**/*.jpg',
'**/*.pyc',
]
export const STEPS_DIR = '../' // Relative to web/ directory
-62
View File
@@ -1,62 +0,0 @@
import * as Diff from 'diff'
export interface DiffLine {
type: 'added' | 'removed' | 'unchanged'
content: string
oldLineNumber?: number
newLineNumber?: number
}
export interface DiffResult {
lines: DiffLine[]
addedCount: number
removedCount: number
}
/**
* Compute line-by-line diff between two strings
*/
export function computeDiff(fromContent: string, toContent: string): DiffResult {
const changes = Diff.diffLines(fromContent, toContent)
const lines: DiffLine[] = []
let addedCount = 0
let removedCount = 0
let oldLine = 1
let newLine = 1
for (const change of changes) {
const changeLines = change.value.split('\n')
// Remove last empty string if content ends with newline
if (changeLines[changeLines.length - 1] === '') {
changeLines.pop()
}
for (const line of changeLines) {
if (change.added) {
lines.push({
type: 'added',
content: line,
newLineNumber: newLine++,
})
addedCount++
} else if (change.removed) {
lines.push({
type: 'removed',
content: line,
oldLineNumber: oldLine++,
})
removedCount++
} else {
lines.push({
type: 'unchanged',
content: line,
oldLineNumber: oldLine++,
newLineNumber: newLine++,
})
}
}
}
return { lines, addedCount, removedCount }
}
-114
View File
@@ -1,114 +0,0 @@
import fs from 'fs'
import path from 'path'
import { glob } from 'glob'
import { SKIP_PATTERNS, STEPS_DIR } from './constants'
export interface FileDiff {
path: string
status: 'added' | 'removed' | 'modified' | 'unchanged'
fromContent?: string
toContent?: string
}
/**
* Get all Python files in a step directory
*/
function getPythonFiles(stepFolder: string): string[] {
const stepPath = path.join(process.cwd(), STEPS_DIR, stepFolder, 'src', 'mybot')
if (!fs.existsSync(stepPath)) {
return []
}
const files = glob.sync('**/*.py', {
cwd: stepPath,
ignore: SKIP_PATTERNS,
nodir: true,
})
return files
}
/**
* Read file content from a step directory
*/
function readFileContent(stepFolder: string, relativePath: string): string | null {
const filePath = path.join(process.cwd(), STEPS_DIR, stepFolder, 'src', 'mybot', relativePath)
if (!fs.existsSync(filePath)) {
return null
}
return fs.readFileSync(filePath, 'utf-8')
}
/**
* Discover and compare files between two steps
*/
export function discoverFiles(fromFolder: string, toFolder: string): FileDiff[] {
const fromFiles = new Set(getPythonFiles(fromFolder))
const toFiles = new Set(getPythonFiles(toFolder))
const allFiles = new Set([...fromFiles, ...toFiles])
const results: FileDiff[] = []
for (const file of allFiles) {
const inFrom = fromFiles.has(file)
const inTo = toFiles.has(file)
const fromContent = inFrom ? (readFileContent(fromFolder, file) ?? undefined) : undefined
const toContent = inTo ? (readFileContent(toFolder, file) ?? undefined) : undefined
let status: FileDiff['status']
if (!inFrom && inTo) {
status = 'added'
} else if (inFrom && !inTo) {
status = 'removed'
} else if (fromContent !== toContent) {
status = 'modified'
} else {
status = 'unchanged'
}
results.push({
path: file,
status,
fromContent,
toContent,
})
}
// Sort: modified first, then added, then removed, then unchanged
const statusOrder: Record<FileDiff['status'], number> = {
modified: 0,
added: 1,
removed: 2,
unchanged: 3,
}
results.sort((a, b) => {
const orderDiff = statusOrder[a.status] - statusOrder[b.status]
if (orderDiff !== 0) return orderDiff
return a.path.localeCompare(b.path)
})
return results
}
/**
* Get only changed files (exclude unchanged)
*/
export function getChangedFiles(fromFolder: string, toFolder: string): FileDiff[] {
return discoverFiles(fromFolder, toFolder).filter(
(f) => f.status !== 'unchanged'
)
}
/**
* Get only unchanged files
*/
export function getUnchangedFiles(fromFolder: string, toFolder: string): FileDiff[] {
return discoverFiles(fromFolder, toFolder).filter(
(f) => f.status === 'unchanged'
)
}
-120
View File
@@ -1,120 +0,0 @@
import fs from 'fs'
import path from 'path'
import { PHASES, STEPS_DIR } from './constants'
export interface Step {
id: string
title: string
description: string
phase: number
folderName: string
readmePath: string
}
/**
* Get the folder name for a step ID (e.g., "00" -> "00-chat-loop")
*/
function getFolderName(stepId: string): string | null {
const stepsDir = path.join(process.cwd(), STEPS_DIR)
const entries = fs.readdirSync(stepsDir, { withFileTypes: true })
const folder = entries.find(
(e) => e.isDirectory() && e.name.startsWith(`${stepId}-`)
)
return folder?.name ?? null
}
/**
* Parse title from README H1
* Format: "# Step XX: Title Here" -> "Title Here"
*/
function parseTitle(readmeContent: string): string {
const h1Match = readmeContent.match(/^# .+/m)
if (!h1Match) return 'Unknown'
const fullTitle = h1Match[0].replace('# ', '')
return fullTitle.split(': ')[1] ?? fullTitle
}
/**
* Parse the first blockquote from README
* Format: "> Description text here" -> "Description text here"
*/
function parseDescription(readmeContent: string): string {
const blockquoteMatch = readmeContent.match(/^>\s*(.+)$/m)
return blockquoteMatch?.[1]?.trim() ?? ''
}
/**
* Get phase number for a step ID
*/
function getPhase(stepId: string): number {
const phaseEntries = Object.entries(PHASES) as [string, typeof PHASES[1]][]
for (const [phase, data] of phaseEntries) {
if (data.steps.includes(stepId as any)) {
return parseInt(phase, 10)
}
}
return 0
}
/**
* Load all steps from the parent directory
*/
export function loadSteps(): Step[] {
const steps: Step[] = []
const phaseEntries = Object.entries(PHASES) as [string, typeof PHASES[1]][]
for (const [phaseStr, phaseData] of phaseEntries) {
for (const stepId of phaseData.steps) {
const folderName = getFolderName(stepId)
if (!folderName) continue
const readmePath = path.join(process.cwd(), STEPS_DIR, folderName, 'README.md')
let title = `Step ${stepId}`
let description = ''
if (fs.existsSync(readmePath)) {
const content = fs.readFileSync(readmePath, 'utf-8')
title = parseTitle(content)
description = parseDescription(content)
}
steps.push({
id: stepId,
title,
description,
phase: parseInt(phaseStr, 10),
folderName,
readmePath,
})
}
}
return steps
}
// Cache steps at module level for build
let _steps: Step[] | null = null
export function getSteps(): Step[] {
if (!_steps) {
_steps = loadSteps()
}
return _steps
}
export function getStep(id: string): Step | undefined {
return getSteps().find((s) => s.id === id)
}
export function getStepsByPhase(): Record<number, Step[]> {
const steps = getSteps()
const result: Record<number, Step[]> = {}
for (const step of steps) {
if (!result[step.phase]) {
result[step.phase] = []
}
result[step.phase].push(step)
}
return result
}
-6
View File
@@ -1,6 +0,0 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

Before

Width:  |  Height:  |  Size: 371 B

After

Width:  |  Height:  |  Size: 371 B

Before

Width:  |  Height:  |  Size: 660 B

After

Width:  |  Height:  |  Size: 660 B

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

@@ -1,8 +1,8 @@
import type { Metadata } from 'next'
import { Geist, Red_Hat_Mono } from 'next/font/google'
import './globals.css'
import { ThemeProvider } from '@/components/theme-provider'
import { Header } from '@/components/header'
import { ThemeProvider } from '@//components/theme-provider'
import { Header } from '@//components/header'
const geist = Geist({
subsets: ['latin'],
@@ -1,4 +1,4 @@
import { H1, P } from '@/components/ui/typography'
import { H1, P } from '@//components/ui/typography'
import Link from 'next/link'
export default function NotFound() {
+3 -3
View File
@@ -1,9 +1,9 @@
import Link from 'next/link'
import { getStepsByPhase } from '@/lib/steps'
import { PHASES } from '@/lib/constants'
import { H1, H2, Lead, Muted } from '@/components/ui/typography'
import { StepCard } from '@/components/step-card'
import { CloneCommand } from '@/components/clone-command'
import { H1, H2, Lead, Muted } from '@//components/ui/typography'
import { StepCard } from '@//components/step-card'
import { CloneCommand } from '@//components/clone-command'
import { GithubIcon, StarIcon } from 'lucide-react'
const GITHUB_REPO_URL = 'https://github.com/czl9707/build-your-own-openclaw'
@@ -2,9 +2,9 @@ import { notFound } from 'next/navigation'
import Link from 'next/link'
import { getStep, getSteps } from '@/lib/steps'
import { getChangedFiles, getUnchangedFiles } from '@/lib/files'
import { DiffViewer } from '@/components/diff-viewer'
import { FileNavDropdown } from '@/components/file-nav-dropdown'
import { DiffPageSelector } from '@/components/diff-page-selector'
import { DiffViewer } from '@//components/diff-viewer'
import { FileNavDropdown } from '@//components/file-nav-dropdown'
import { DiffPageSelector } from '@//components/diff-page-selector'
import {
Breadcrumb,
BreadcrumbList,
@@ -12,7 +12,7 @@ import {
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb'
} from '@//components/ui/breadcrumb'
interface DiffPageProps {
params: Promise<{
@@ -4,8 +4,8 @@ import { notFound } from 'next/navigation'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import { getStep, getSteps } from '@/lib/steps'
import { ReadmeRenderer } from '@/components/readme-renderer'
import { StepDiffSelector } from '@/components/step-diff-selector'
import { ReadmeRenderer } from '@//components/readme-renderer'
import { StepDiffSelector } from '@//components/step-diff-selector'
import {
Breadcrumb,
BreadcrumbItem,
@@ -13,8 +13,8 @@ import {
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb'
import { H1 } from '@/components/ui/typography'
} from '@//components/ui/breadcrumb'
import { H1 } from '@//components/ui/typography'
// Button-like link styles for Server Components (matches buttonVariants outline)
const buttonOutlineStyles =
@@ -1,7 +1,7 @@
'use client'
import { useState } from 'react'
import { Card } from '@/components/ui/card'
import { Card } from '@//components/ui/card'
import { CheckIcon, CopyIcon } from 'lucide-react'
const CLONE_COMMAND = 'git clone https://github.com/czl9707/build-your-own-openclaw.git'
@@ -1,7 +1,7 @@
'use client'
import { useRouter } from 'next/navigation'
import { DiffSelector } from '@/components/diff-selector'
import { DiffSelector } from '@//components/diff-selector'
import type { Step } from '@/lib/steps'
interface DiffPageSelectorProps {
@@ -6,7 +6,7 @@ import {
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
} from '@//components/ui/select'
import type { Step } from '@/lib/steps'
interface DiffSelectorProps {
@@ -2,12 +2,12 @@
import * as React from 'react'
import { codeToHtml } from 'shiki'
import { ScrollArea } from '@/components/ui/scroll-area'
import { ScrollArea } from '@//components/ui/scroll-area'
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible'
} from '@//components/ui/collapsible'
import { cn } from '@/lib/utils'
import { computeDiff, type DiffLine } from '@/lib/diff'
import { useIsMobile } from '@/lib/hooks'
@@ -7,7 +7,7 @@ import {
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
} from '@//components/ui/select'
import { PlusIcon, MinusIcon } from 'lucide-react'
interface FileItem {
@@ -2,7 +2,7 @@ import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import rehypeSlug from 'rehype-slug'
import rehypeRaw from 'rehype-raw'
import { H1, H2, H3, H4, P } from '@/components/ui/typography'
import { H1, H2, H3, H4, P } from '@//components/ui/typography'
import { CodeBlock } from './code-block'
import type { Components } from 'react-markdown'
@@ -1,7 +1,7 @@
import Link from 'next/link'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Muted } from '@/components/ui/typography'
import { Card, CardContent, CardHeader, CardTitle } from '@//components/ui/card'
import { Badge } from '@//components/ui/badge'
import { Muted } from '@//components/ui/typography'
import type { Step } from '@/lib/steps'
interface StepCardProps {
@@ -1,7 +1,7 @@
'use client'
import { useRouter } from 'next/navigation'
import { DiffSelector } from '@/components/diff-selector'
import { DiffSelector } from '@//components/diff-selector'
import type { Step } from '@/lib/steps'
interface StepDiffSelectorProps {
@@ -4,13 +4,13 @@ import * as React from 'react'
import { Moon, Sun } from 'lucide-react'
import { useTheme } from 'next-themes'
import { Button } from '@/components/ui/button'
import { Button } from '@//components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
} from '@//components/ui/dropdown-menu'
export function ThemeToggle() {
const { setTheme } = useTheme()
+1 -1
View File
@@ -19,7 +19,7 @@
}
],
"paths": {
"@/*": ["./*"]
"@/*": ["./src/*"]
}
},
"include": [