mirror of
https://github.com/czl9707/build-your-own-openclaw.git
synced 2026-08-14 00:47:59 +00:00
use source dir
This commit is contained in:
@@ -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
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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'
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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() {
|
||||
@@ -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
@@ -19,7 +19,7 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
|
||||
Reference in New Issue
Block a user