diff --git a/src/components/notebook/NotebookContent.tsx b/src/components/notebook/NotebookContent.tsx index 0d5f3785..dc2f4124 100644 --- a/src/components/notebook/NotebookContent.tsx +++ b/src/components/notebook/NotebookContent.tsx @@ -10,10 +10,25 @@ import { contextSelectionMode$ } from "./signals/ai-context.js"; import { focusedCellSignal$, hasManuallyFocused$ } from "./signals/focus.js"; import { GripVerticalIcon } from "lucide-react"; import { useDragDropCellSort } from "@/hooks/useDragDropCellSort"; +import { useCellFilter } from "@/contexts/CellFilterContext.js"; export const NotebookContent = () => { const { store } = useStore(); - const cells = useQuery(queries.cells$); + const allCells = useQuery(queries.cells$); + const { filters } = useCellFilter(); + + // Filter cells based on context values + const cells = React.useMemo(() => { + return allCells.filter((cell) => { + // if (cell.cellType === "code" && !filters.showCodeCells) { + // return false; + // } + if (cell.cellType === "ai" && !filters.showAiCells) { + return false; + } + return true; + }); + }, [allCells, filters]); const focusedCellId = useQuery(focusedCellSignal$); const hasManuallyFocused = useQuery(hasManuallyFocused$); diff --git a/src/components/notebook/cell/ExecutableCell.tsx b/src/components/notebook/cell/ExecutableCell.tsx index 79b30dd6..27768cac 100644 --- a/src/components/notebook/cell/ExecutableCell.tsx +++ b/src/components/notebook/cell/ExecutableCell.tsx @@ -52,6 +52,7 @@ import { useFeatureFlag } from "@/contexts/FeatureFlagContext.js"; import { findBestAiModelForCell } from "./toolbars/ai-model-utils.js"; import { useAvailableAiModels } from "@/util/ai-models.js"; import { toast } from "sonner"; +import { useCellFilter } from "@/contexts/CellFilterContext.js"; // Cell-specific styling configuration const getCellStyling = (cellType: "code" | "sql" | "ai") => { @@ -102,6 +103,10 @@ export const ExecutableCell: React.FC = ({ const hasRunRef = useRef(false); const cellRef = useRef(null); + const { + filters: { showCodeCells }, + } = useCellFilter(); + const { registerEditor, unregisterEditor, @@ -477,7 +482,7 @@ export const ExecutableCell: React.FC = ({ focusBorderColor={focusBorderColor} > {/* Cell Header */} - {!isSourceLessAiOutput && ( + {!isSourceLessAiOutput && showCodeCells && ( = ({ )} {/* Cell Content with Left Gutter Play Button - Desktop Only */} - {!isSourceLessAiOutput && ( + {!isSourceLessAiOutput && showCodeCells && (
{/* Play Button Breaking Through Left Border - Desktop Only */}
= ({ )} {/* Editor Content Area */} - {cell.sourceVisible && ( + {cell.sourceVisible && showCodeCells && (
Error rendering editor
}> = ({ {/* Execution Summary - appears after input */} {(cell.executionCount || cell.executionState === "running" || - cell.executionState === "queued") && ( -
-
- +
- {cell.executionState === "running" - ? "Executing..." - : cell.executionState === "queued" - ? // Show count in case runtime is not responsive, to show that at least something is happening - `Queued for execution (execution count: ${cell.executionCount})` - : cell.executionCount - ? cell.lastExecutionDurationMs - ? `Executed in ${ - cell.lastExecutionDurationMs < 1000 - ? `${cell.lastExecutionDurationMs}ms` - : `${(cell.lastExecutionDurationMs / 1000).toFixed(1)}s` - }` - : "Executed" - : null} - - {(outputs.length > 0 || cell.executionState === "running") && ( -
- {!cell.outputVisible && hasOutputs && ( - - {outputs.length === 1 - ? "1 result hidden" - : `${outputs.length} results hidden`} - - )} - -
- )} + +
+ )} +
-
- )} + )} {/* Output Area */} diff --git a/src/components/notebooks/notebook/NotebookControls.tsx b/src/components/notebooks/notebook/NotebookControls.tsx index 6d999513..f078c87c 100644 --- a/src/components/notebooks/notebook/NotebookControls.tsx +++ b/src/components/notebooks/notebook/NotebookControls.tsx @@ -3,8 +3,11 @@ import { Button } from "@/components/ui/button"; import { useConfirm } from "@/components/ui/confirm"; import { DropdownMenu, + DropdownMenuCheckboxItem, DropdownMenuContent, + DropdownMenuGroup, DropdownMenuItem, + DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; @@ -21,6 +24,7 @@ import type { NotebookProcessed } from "../types"; import { useAuthenticatedUser } from "@/auth/index.js"; import { useDebug } from "@/components/debug/debug-mode"; import { Spinner } from "@/components/ui/Spinner"; +import { useCellFilter } from "@/contexts/CellFilterContext.js"; import { useRuntimeHealth } from "@/hooks/useRuntimeHealth"; import { runnableCellsWithIndices$, runningCells$ } from "@/queries"; import { generateQueueId } from "@/util/queue-id"; @@ -80,18 +84,32 @@ export function NotebookControls({ {allowBulkNotebookControls && ( - + + Execution + + )} - - - {exportEnabled && } + + View + + - {debug.enabled && } - + + {/* Notebook */} + + + {exportEnabled && } + + + + {/* Danger Zone */} + {debug.enabled && } + +
@@ -350,3 +368,24 @@ function useRestartAndRunAllCells() { return { restartAndRunAllCells }; } + +function CellFilterActions() { + const { filters, setShowCodeCells, setShowAiCells } = useCellFilter(); + + return ( + <> + + Show Code Cells + + + Show AI Cells + + + ); +} diff --git a/src/components/notebooks/notebook/NotebookPage.tsx b/src/components/notebooks/notebook/NotebookPage.tsx index 8bdade17..09c78d9d 100644 --- a/src/components/notebooks/notebook/NotebookPage.tsx +++ b/src/components/notebooks/notebook/NotebookPage.tsx @@ -23,6 +23,7 @@ import { useNavigateToCanonicalUrl, useNotebook } from "./helpers.js"; import { NotebookHeader } from "./NotebookHeader.js"; import { AvailableAiModelsProvider } from "@/util/ai-models.js"; import { SidebarItemProvider } from "@/contexts/SidebarItemContext.js"; +import { CellFilterProvider } from "@/contexts/CellFilterContext.js"; export const NotebookPage: React.FC = () => { const { id } = useParams<{ id: string }>(); @@ -31,11 +32,13 @@ export const NotebookPage: React.FC = () => { return ( - - - - - + + + + + + + ); }; diff --git a/src/contexts/CellFilterContext.tsx b/src/contexts/CellFilterContext.tsx new file mode 100644 index 00000000..475466e4 --- /dev/null +++ b/src/contexts/CellFilterContext.tsx @@ -0,0 +1,93 @@ +import React, { createContext, useContext, useMemo } from "react"; +import { useSearchParams } from "react-router-dom"; + +interface CellFilters { + showAiCells: boolean; + showCodeCells: boolean; +} + +const DEFAULT_FILTERS: CellFilters = { + showAiCells: true, + showCodeCells: true, +} as const; + +interface CellFilterContextType { + filters: CellFilters; + setShowAiCells: (value: boolean) => void; + setShowCodeCells: (value: boolean) => void; +} + +const CellFilterContext = createContext( + undefined +); + +interface CellFilterProviderProps { + children: React.ReactNode; +} + +export function CellFilterProvider({ children }: CellFilterProviderProps) { + const [searchParams, setSearchParams] = useSearchParams(); + + // Parse filter values from URL query params, defaulting to true if not present + const filters = useMemo(() => { + const showAiCellsParam = searchParams.get("showAiCells"); + const showCodeCellsParam = searchParams.get("showCodeCells"); + + return { + showAiCells: + showAiCellsParam === null + ? DEFAULT_FILTERS.showAiCells + : showAiCellsParam === "true", + showCodeCells: + showCodeCellsParam === null + ? DEFAULT_FILTERS.showCodeCells + : showCodeCellsParam === "true", + }; + }, [searchParams]); + + const setShowAiCells = React.useCallback( + (value: boolean) => { + const newSearchParams = new URLSearchParams(searchParams); + if (value === DEFAULT_FILTERS.showAiCells) { + // Remove param if it matches default + newSearchParams.delete("showAiCells"); + } else { + newSearchParams.set("showAiCells", String(value)); + } + setSearchParams(newSearchParams, { replace: true }); + }, + [searchParams, setSearchParams] + ); + + const setShowCodeCells = React.useCallback( + (value: boolean) => { + const newSearchParams = new URLSearchParams(searchParams); + if (value === DEFAULT_FILTERS.showCodeCells) { + // Remove param if it matches default + newSearchParams.delete("showCodeCells"); + } else { + newSearchParams.set("showCodeCells", String(value)); + } + setSearchParams(newSearchParams, { replace: true }); + }, + [searchParams, setSearchParams] + ); + + return ( + + {children} + + ); +} + +export function useCellFilter() { + const context = useContext(CellFilterContext); + + if (context === undefined) { + throw new Error("useCellFilter must be used within a CellFilterProvider"); + } + + return context; +}