Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions lib/BasePipelineSolver.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import type { GraphicsObject } from "graphics-debug"
import { BaseSolver } from "./BaseSolver"

export interface PipelineStep<T extends BaseSolver> {
export interface PipelineStep<
T extends BaseSolver = BaseSolver,
P = any,
> {
solverName: string
solverClass: new (...args: any[]) => T
getConstructorParams: (pipelineInstance: any) => any[]
solverClass: new (params: P) => T
getConstructorParams: (pipelineInstance: any) => P
onSolved?: (pipelineInstance: any) => void
}

Expand All @@ -15,11 +18,11 @@ export function definePipelineStep<
>(
solverName: string,
solverClass: new (params: P) => T,
getConstructorParams: (instance: Instance) => [P],
getConstructorParams: (instance: Instance) => P,
opts: {
onSolved?: (instance: Instance) => void
} = {},
): PipelineStep<T> {
): PipelineStep<T, P> {
return {
solverName,
solverClass,
Expand All @@ -40,7 +43,7 @@ export abstract class BasePipelineSolver<TInput> extends BaseSolver {
/** Stores the outputs from each completed pipeline step */
pipelineOutputs: Record<string, any> = {}

abstract pipelineDef: PipelineStep<any>[]
abstract pipelineDef: PipelineStep<any, any>[]

constructor(inputProblem: TInput) {
super()
Expand Down Expand Up @@ -81,7 +84,7 @@ export abstract class BasePipelineSolver<TInput> extends BaseSolver {
}

const constructorParams = pipelineStepDef.getConstructorParams(this)
this.activeSubSolver = new pipelineStepDef.solverClass(...constructorParams)
this.activeSubSolver = new pipelineStepDef.solverClass(constructorParams)
;(this as any)[pipelineStepDef.solverName] = this.activeSubSolver
this.timeSpentOnPhase[pipelineStepDef.solverName] = 0
this.startTimeOfPhase[pipelineStepDef.solverName] = performance.now()
Expand Down
5 changes: 3 additions & 2 deletions lib/BaseSolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import type { GraphicsObject } from "graphics-debug"
* Solvers should override visualize() to return a GraphicsObject representing
* the current state of the solver.
*
* Solvers should override getConstructorParams() to return the parameters
* needed to construct the solver.
* Solvers should override getConstructorParams() to return the object passed as
* the first (and only) constructor parameter. This keeps constructor
* signatures serializable for tooling like the download dropdowns.
*/
export class BaseSolver {
MAX_ITERATIONS = 100e3
Expand Down
254 changes: 254 additions & 0 deletions lib/react/DownloadDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
import React, { useEffect, useRef, useState } from "react"
import type { BaseSolver } from "../BaseSolver"
import { BasePipelineSolver } from "../BasePipelineSolver"

export interface DownloadTemplateContext {
solver: BaseSolver
solverName: string
params: any
isPipelineSolver: boolean
}

export interface DownloadTemplateOverrides {
generatePageTsxContent?: (context: DownloadTemplateContext) => string
generateTestTsContent?: (context: DownloadTemplateContext) => string
}

export interface DownloadDropdownProps {
solver: BaseSolver
className?: string
overrides?: DownloadTemplateOverrides
}

export const deepRemoveUnderscoreProperties = (obj: any): any => {
if (obj === null || typeof obj !== "object") {
return obj
}

if (Array.isArray(obj)) {
return obj.map(deepRemoveUnderscoreProperties)
}

const result: Record<string, any> = {}
for (const [key, value] of Object.entries(obj)) {
if (!key.startsWith("_")) {
result[key] = deepRemoveUnderscoreProperties(value)
}
}
return result
}

const defaultPageTsxContent = ({
solverName,
params,
isPipelineSolver,
}: DownloadTemplateContext) => {
const serializedParams = JSON.stringify(params, null, 2)

if (isPipelineSolver) {
return `import { useMemo } from "react"
import { PipelineDebugger } from "solver-utils/lib/react"
import { ${solverName} } from "lib/solvers/${solverName}/${solverName}"

export const inputProblem = ${serializedParams}

export default () => {
const solver = useMemo(() => new ${solverName}(inputProblem as any), [])
return <PipelineDebugger solver={solver} />
}
`
}

return `import { useMemo } from "react"
import { GenericSolverDebugger } from "solver-utils/lib/react"
import { ${solverName} } from "lib/solvers/${solverName}/${solverName}"

export const inputProblem = ${serializedParams}

export default () => {
const solver = useMemo(() => new ${solverName}(inputProblem as any), [])
return <GenericSolverDebugger solver={solver} />
}
`
}

const defaultTestTsContent = ({ solverName, params }: DownloadTemplateContext) => {
const serializedParams = JSON.stringify(params, null, 2)

return `import { ${solverName} } from "lib/solvers/${solverName}/${solverName}"
import { test, expect } from "bun:test"

test("${solverName} should solve problem correctly", () => {
const input = ${serializedParams}

const solver = new ${solverName}(input as any)
solver.solve()

expect(solver).toMatchSolverSnapshot(import.meta.path)

// Add more specific assertions based on expected output
// expect(solver.getOutput()).toMatchInlineSnapshot()
})
`
}

export const DownloadDropdown: React.FC<DownloadDropdownProps> = ({
solver,
className = "",
overrides,
}) => {
const [isOpen, setIsOpen] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null)

useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setIsOpen(false)
}
}

if (typeof document !== "undefined") {
document.addEventListener("mousedown", handleClickOutside)
}
return () => {
if (typeof document !== "undefined") {
document.removeEventListener("mousedown", handleClickOutside)
}
}
}, [])

const runWithParams = (
action: (context: DownloadTemplateContext) => void,
) => {
if (typeof solver.getConstructorParams !== "function") {
const message = `getConstructorParams() is not implemented for ${solver.constructor.name}`
if (typeof window !== "undefined") {
window.alert?.(message)
} else {
console.error(message)
}
return
}

try {
const rawParams = solver.getConstructorParams()
const params = deepRemoveUnderscoreProperties(rawParams)
const solverName = solver.constructor.name
const context: DownloadTemplateContext = {
solver,
solverName,
params,
isPipelineSolver: solver instanceof BasePipelineSolver,
}
action(context)
} catch (error) {
const message = `Error gathering constructor params for ${solver.constructor.name}: ${error instanceof Error ? error.message : String(error)}`
if (typeof window !== "undefined") {
window.alert?.(message)
} else {
console.error(message)
}
}
}

const downloadJSON = () => {
runWithParams(({ solverName, params }) => {
if (typeof document === "undefined") {
console.error("Document is not available to trigger downloads")
return
}
const blob = new Blob([JSON.stringify(params, null, 2)], {
type: "application/json",
})
const url = URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = `${solverName}_params.json`
a.click()
URL.revokeObjectURL(url)
})
setIsOpen(false)
}

const downloadPageTsx = () => {
runWithParams((context) => {
if (typeof document === "undefined") {
console.error("Document is not available to trigger downloads")
return
}
const content =
overrides?.generatePageTsxContent?.(context) ??
defaultPageTsxContent(context)
const blob = new Blob([content], { type: "text/plain" })
const url = URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = `${context.solverName}.page.tsx`
a.click()
URL.revokeObjectURL(url)
})
setIsOpen(false)
}

const downloadTestTs = () => {
runWithParams((context) => {
if (typeof document === "undefined") {
console.error("Document is not available to trigger downloads")
return
}
const content =
overrides?.generateTestTsContent?.(context) ??
defaultTestTsContent(context)
const blob = new Blob([content], { type: "text/plain" })
const url = URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = `${context.solverName}.test.ts`
a.click()
URL.revokeObjectURL(url)
})
setIsOpen(false)
}

return (
<div className={`relative ${className}`} ref={dropdownRef}>
<button
className="px-2 py-1 rounded text-xs cursor-pointer"
onClick={() => setIsOpen(!isOpen)}
title={`Download options for ${solver.constructor.name}`}
type="button"
>
{solver.constructor.name}
</button>

{isOpen && (
<div className="absolute top-full left-0 mt-1 bg-white border border-gray-300 rounded shadow-lg z-10 min-w-[150px]">
<button
className="w-full text-left px-3 py-2 hover:bg-gray-100 text-xs"
onClick={downloadJSON}
type="button"
>
Download JSON
</button>
<button
className="w-full text-left px-3 py-2 hover:bg-gray-100 text-xs"
onClick={downloadPageTsx}
type="button"
>
Download page.tsx
</button>
<button
className="w-full text-left px-3 py-2 hover:bg-gray-100 text-xs"
onClick={downloadTestTs}
type="button"
>
Download test.ts
</button>
</div>
)}
</div>
)
}
4 changes: 4 additions & 0 deletions lib/react/GenericSolverDebugger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,22 @@ import React, { useEffect, useMemo, useReducer } from "react"
import type { BaseSolver } from "../BaseSolver"
import { InteractiveGraphics } from "graphics-debug/react"
import { GenericSolverToolbar } from "./GenericSolverToolbar"
import type { DownloadTemplateOverrides } from "./DownloadDropdown"

export interface GenericSolverDebuggerProps {
solver: BaseSolver
animationSpeed?: number
onSolverStarted?: (solver: BaseSolver) => void
onSolverCompleted?: (solver: BaseSolver) => void
downloadOverrides?: DownloadTemplateOverrides
}

export const GenericSolverDebugger = ({
solver,
animationSpeed = 25,
onSolverStarted,
onSolverCompleted,
downloadOverrides,
}: GenericSolverDebuggerProps) => {
const [renderCount, incRenderCount] = useReducer((x) => x + 1, 0)

Expand Down Expand Up @@ -59,6 +62,7 @@ export const GenericSolverDebugger = ({
animationSpeed={animationSpeed}
onSolverStarted={onSolverStarted}
onSolverCompleted={onSolverCompleted}
downloadOverrides={downloadOverrides}
/>
{graphicsAreEmpty ? (
<div className="p-4 text-gray-500">No Graphics Yet</div>
Expand Down
9 changes: 9 additions & 0 deletions lib/react/GenericSolverToolbar.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import React, { useReducer, useRef, useEffect } from "react"
import type { BaseSolver } from "../BaseSolver"
import type { BasePipelineSolver } from "../BasePipelineSolver"
import { SolverBreadcrumbInputDownloader } from "./SolverBreadcrumbInputDownloader"
import type { DownloadTemplateOverrides } from "./DownloadDropdown"

export interface GenericSolverToolbarProps {
solver: BaseSolver
triggerRender: () => void
animationSpeed?: number
onSolverStarted?: (solver: BaseSolver) => void
onSolverCompleted?: (solver: BaseSolver) => void
downloadOverrides?: DownloadTemplateOverrides
}

export const GenericSolverToolbar = ({
Expand All @@ -16,6 +19,7 @@ export const GenericSolverToolbar = ({
animationSpeed = 25,
onSolverStarted,
onSolverCompleted,
downloadOverrides,
}: GenericSolverToolbarProps) => {
const [isAnimating, setIsAnimating] = useReducer((x) => !x, false)
const animationRef = useRef<NodeJS.Timeout | undefined>(undefined)
Expand Down Expand Up @@ -111,6 +115,11 @@ export const GenericSolverToolbar = ({

return (
<div className="space-y-2 p-2 border-b">
<SolverBreadcrumbInputDownloader
solver={solver}
overrides={downloadOverrides}
/>

<div className="flex gap-2 items-center flex-wrap">
<button
onClick={handleStep}
Expand Down
Loading
Loading