mirror of
https://github.com/czl9707/build-your-own-openclaw.git
synced 2026-08-14 00:47:59 +00:00
add missing components
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
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
|
||||
@@ -0,0 +1,62 @@
|
||||
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 }
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
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'
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
|
||||
/**
|
||||
* Hook to detect if viewport is mobile-sized
|
||||
*/
|
||||
export function useIsMobile(breakpoint = 768) {
|
||||
const [isMobile, setIsMobile] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
const checkMobile = () => setIsMobile(window.innerWidth < breakpoint)
|
||||
checkMobile()
|
||||
window.addEventListener('resize', checkMobile)
|
||||
return () => window.removeEventListener('resize', checkMobile)
|
||||
}, [breakpoint])
|
||||
|
||||
return isMobile
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
Reference in New Issue
Block a user