From a7bc9ce8a2f2f96f362a700bf9929169ac004556 Mon Sep 17 00:00:00 2001 From: JsCodeDevlopment Date: Wed, 13 May 2026 22:49:52 -0300 Subject: [PATCH 1/6] feat: implement interactive code player and problem visualization system for Two Sum algorithm --- .../dashboard/dashboard-filters.tsx | 22 +- .../_components/dashboard/dashboard-types.ts | 22 ++ .../_components/forms/problem-form.tsx | 13 +- app/admin/content/_components/types.ts | 8 +- .../content/content-dashboard-client.tsx | 21 +- app/admin/pricing/pricing-manager-client.tsx | 163 ++++++++-- app/problems/[slug]/[solution]/page.tsx | 7 +- components/code-player/code-player.tsx | 91 ++++-- components/code-player/code-view.tsx | 10 +- components/code-player/dynamic-input-form.tsx | 94 ++++++ .../code-player/dynamic-player-wrapper.tsx | 92 ++++++ .../code-player/execution-trace-panel.tsx | 13 +- components/code-player/explanation-panel.tsx | 34 +- components/code-player/player-controls.tsx | 36 ++- components/code-player/use-player-store.ts | 63 +++- components/code-player/visualizers/index.ts | 20 ++ .../visualizers/two-sum-visualizer.tsx | 301 ++++++++++++++++++ components/layout/session-nav.tsx | 12 +- components/seo/json-ld.tsx | 9 +- lib/actions/admin.ts | 11 +- lib/content/content-repository.ts | 2 + lib/content/schemas.ts | 5 + lib/simulators/index.ts | 20 ++ 23 files changed, 945 insertions(+), 124 deletions(-) create mode 100644 components/code-player/dynamic-input-form.tsx create mode 100644 components/code-player/dynamic-player-wrapper.tsx create mode 100644 components/code-player/visualizers/index.ts create mode 100644 components/code-player/visualizers/two-sum-visualizer.tsx create mode 100644 lib/simulators/index.ts diff --git a/app/admin/content/_components/dashboard/dashboard-filters.tsx b/app/admin/content/_components/dashboard/dashboard-filters.tsx index 02c811e..3a47d3c 100644 --- a/app/admin/content/_components/dashboard/dashboard-filters.tsx +++ b/app/admin/content/_components/dashboard/dashboard-filters.tsx @@ -1,6 +1,6 @@ 'use client'; -import { EDITORIAL_TYPES, SYSTEM_TYPE_OPTIONS, STATUS_FILTERS } from "./dashboard-types"; +import { EDITORIAL_TYPES, SYSTEM_TYPE_OPTIONS, STATUS_FILTERS, CATEGORY_OPTIONS } from "./dashboard-types"; interface DashboardFiltersProps { tab: 'editorial' | 'sistema'; @@ -10,6 +10,8 @@ interface DashboardFiltersProps { onTypeFilterChange: (v: string) => void; statusFilter: string; onStatusFilterChange: (v: string) => void; + categoryFilter: string; + onCategoryFilterChange: (v: string) => void; onClearFilters: () => void; } @@ -21,6 +23,8 @@ export function DashboardFilters({ onTypeFilterChange, statusFilter, onStatusFilterChange, + categoryFilter, + onCategoryFilterChange, onClearFilters, }: DashboardFiltersProps) { const typeOptions = tab === 'editorial' ? EDITORIAL_TYPES : SYSTEM_TYPE_OPTIONS; @@ -63,6 +67,20 @@ export function DashboardFilters({ ))} + {typeOptions === EDITORIAL_TYPES && ( + + )} + - {(search || typeFilter || statusFilter) && ( + {(search || typeFilter || statusFilter || categoryFilter) && ( + + + +

+ INFO: Configura o cenário de teste acima e dispara o motor de execução. +

+ + + ); +} diff --git a/components/code-player/dynamic-player-wrapper.tsx b/components/code-player/dynamic-player-wrapper.tsx new file mode 100644 index 0000000..6e8925f --- /dev/null +++ b/components/code-player/dynamic-player-wrapper.tsx @@ -0,0 +1,92 @@ +"use client"; + +import { ExecutionTraceStep, LineAnnotation } from "@/lib/content/schemas"; +import { HighlightedLine } from "@/lib/content/shiki"; +import { getSimulator } from "@/lib/simulators"; +import { useCallback, useState } from "react"; +import { CodePlayer } from "./code-player"; +import { DynamicInputForm } from "./dynamic-input-form"; +import { hasBespokeVisualizer } from "./visualizers"; + +interface Props { + lines: HighlightedLine[]; + annotations: LineAnnotation[]; + conceptTitles: Record; + readOnlyExplanationMd?: string; + executionTrace: ExecutionTraceStep[]; + problemSlug: string; + solutionSlug: string; + autoPlay?: boolean; + simulatorCode?: string; +} + +export function DynamicPlayerWrapper(props: Props) { + const [dynamicTrace, setDynamicTrace] = useState< + ExecutionTraceStep[] | undefined + >(undefined); + const [shouldAutoPlay, setShouldAutoPlay] = useState(false); + + const isBespoke = hasBespokeVisualizer(props.problemSlug); + + const handleRun = useCallback( + (nums: number[], target: number) => { + // 1. Prioridade para o código vindo do banco (Admin) + if (props.simulatorCode) { + try { + const dynamicSim = new Function( + "nums", + "target", + "const simulate = " + props.simulatorCode + ";\n" + + "return typeof simulate === 'function' ? simulate(nums, target) : null;" + ); + + const trace = dynamicSim(nums, target); + + if (trace && Array.isArray(trace) && trace.length > 0) { + setDynamicTrace(trace); + setShouldAutoPlay(true); + return; + } + } catch (err) { + console.error("Erro ao executar simulador dinâmico:", err); + } + } + + // 2. Fallback para simuladores locais (legado) + const simulator = getSimulator(props.problemSlug, props.solutionSlug); + if (simulator) { + const trace = simulator(nums, target); + if (trace.length > 0) { + setDynamicTrace(trace); + setShouldAutoPlay(true); + } + } + }, + [props.problemSlug, props.solutionSlug, props.simulatorCode], + ); + + const handleReset = useCallback(() => { + setDynamicTrace(undefined); + setShouldAutoPlay(false); + }, []); + + return ( +
+ {isBespoke && props.problemSlug === "two-sum" && ( + + )} + + +
+ ); +} diff --git a/components/code-player/execution-trace-panel.tsx b/components/code-player/execution-trace-panel.tsx index 61a5892..8391296 100644 --- a/components/code-player/execution-trace-panel.tsx +++ b/components/code-player/execution-trace-panel.tsx @@ -13,11 +13,14 @@ interface Props { export function ExecutionTracePanel({ steps }: Props) { const currentLine = usePlayerStore((s) => s.currentLine); + const currentStepIndex = usePlayerStore((s) => s.currentStepIndex); - const snapshot = useMemo( - () => resolveExecutionSnapshot(currentLine, steps), - [currentLine, steps], - ); + const snapshot = useMemo(() => { + if (currentStepIndex !== -1 && steps[currentStepIndex]) { + return steps[currentStepIndex].snapshot; + } + return resolveExecutionSnapshot(currentLine, steps); + }, [currentLine, currentStepIndex, steps]); if (!snapshot) { return ( @@ -25,7 +28,7 @@ export function ExecutionTracePanel({ steps }: Props) { className="rounded-xl border border-dashed border-zinc-300 bg-zinc-50/80 p-4 text-sm text-zinc-500 dark:border-zinc-700 dark:bg-zinc-950/40 dark:text-zinc-400" aria-label="Estado da execução (demonstração)" > - Sem modelo visual para esta linha — avança no player para ver arrays e mapas quando definidos no{' '} + Sem modelo visual para esta linha — avance no player para ver arrays e mapas quando definidos no{' '} trace.json. ); diff --git a/components/code-player/explanation-panel.tsx b/components/code-player/explanation-panel.tsx index efc83e9..847bf06 100644 --- a/components/code-player/explanation-panel.tsx +++ b/components/code-player/explanation-panel.tsx @@ -37,8 +37,8 @@ export function ExplanationPanel({ annotations, conceptTitles }: Props) { if (!annotation) { return ( -