From 55b33f589abe697c5be9c12000464c3109a0d74f Mon Sep 17 00:00:00 2001 From: Sandeep Salwan Date: Sat, 19 Apr 2025 00:21:08 -0400 Subject: [PATCH] [FEATURE] Implement collapsible() Component for UI Layout --- docs/layout/guide.mdx | 44 ++++- docs/sdk/collapsible.mdx | 65 +++++++ examples/iris/hello.py | 71 +------ frontend/components.json | 2 +- frontend/eslint.config.js | 86 ++++---- frontend/postcss.config.js | 2 +- frontend/src/components/DynamicComponents.jsx | 13 ++ .../components/widgets/CollapsibleWidget.jsx | 29 +++ frontend/tailwind.config.js | 184 +++++++++--------- frontend/tsconfig.json | 16 +- frontend/vite.config.js | 14 +- preswald/interfaces/__init__.py | 1 + preswald/interfaces/components.py | 168 ++++++++++++---- 13 files changed, 430 insertions(+), 265 deletions(-) create mode 100644 docs/sdk/collapsible.mdx create mode 100644 frontend/src/components/widgets/CollapsibleWidget.jsx diff --git a/docs/layout/guide.mdx b/docs/layout/guide.mdx index 4daf006a..1bc4a045 100644 --- a/docs/layout/guide.mdx +++ b/docs/layout/guide.mdx @@ -4,13 +4,17 @@ icon: "table-cells" description: "" --- -# Using the `size` Parameter in Components +# Layout Tools and Techniques + +Preswald offers several tools to help you control and organize your application's layout. + +## Using the `size` Parameter in Components The `size` parameter lets you control how much space a component occupies in a row, enabling you to create dynamic and responsive layouts. --- -## How the `size` Parameter Works +### How the `size` Parameter Works - **Default Behavior**: All components have a default `size=1.0`, taking up the full width of the row. - **Custom Sizes**: You can specify a `size` less than `1.0` to allow multiple components to share the same row. @@ -19,9 +23,9 @@ The `size` parameter lets you control how much space a component occupies in a r --- -## Example: Multiple Components in One Row +### Example: Multiple Components in One Row -Here’s an example of how to use the `size` parameter to arrange components in a row: +Here's an example of how to use the `size` parameter to arrange components in a row: ```python from preswald import slider, button @@ -35,7 +39,35 @@ submit_button = button("Submit", size=0.3) threshold_slider = slider("Threshold", min_val=0.0, max_val=100.0, default=50.0, size=0.7) ``` ---- - - **Flexible Layouts**: Multiple components with smaller sizes can fit side by side in a single row. - **Spacing Management**: Verify the combined sizes of all components in a row add up to `1.0` or less. + +--- + +## Using the `collapsible` Component for Group Organization + +The `collapsible` component helps you organize related UI elements into expandable/collapsible sections, improving the overall structure and readability of your application. + +### Benefits of Using Collapsible Sections + +- **Reduced Visual Clutter**: Hide optional or advanced controls until needed +- **Logical Grouping**: Group related inputs and outputs +- **Progressive Disclosure**: Implement step-by-step workflows +- **Better Mobile Experience**: Improve usability on smaller screens + +### Example: Organizing Components with Collapsible + +```python +from preswald import collapsible, slider, text, table + +# Main data view (expanded by default) +collapsible("Data Overview") +table(my_dataframe) + +# Advanced filters section (collapsed by default) +collapsible("Advanced Filters", open=False) +text("Adjust parameters to filter the data") +slider("Threshold", min_val=0, max_val=100, default=50) +``` + +See the [`collapsible` documentation](/sdk/collapsible) for more details on its usage and parameters. diff --git a/docs/sdk/collapsible.mdx b/docs/sdk/collapsible.mdx new file mode 100644 index 00000000..b4b1da63 --- /dev/null +++ b/docs/sdk/collapsible.mdx @@ -0,0 +1,65 @@ +--- +title: "collapsible" +icon: "chevron-down" +description: "" +--- + +```python +collapsible( + label: str, + open: bool = True, + size: float = 1.0, +) -> None: +``` + +The `collapsible` function creates an expandable/collapsible container that groups related UI components. This helps organize complex interfaces by reducing visual clutter and allowing users to focus on relevant content. + +## Parameters + +- **`label`** _(str)_: The title/header of the collapsible section. +- **`open`** _(bool)_: Whether the section is open (expanded) by default. Defaults to `True`. +- **`size`** _(float)_: _(Optional)_ The width of the component in a row. Defaults to `1.0` (full row). See the [Layout Guide](/layout/guide) for details. + + +Collapsible component + + +## Returns + +- This component doesn't return a value. It's used for layout organization only. + +## Usage Example + +```python +from preswald import collapsible, slider, text + +# Create a collapsible section for advanced filters +collapsible("Advanced Filters", open=False) + +# All components below will be nested in the collapsible container +text("Adjust the parameters below to filter the data.") +slider("Sepal Width", min_val=0, max_val=10, default=5.5) +slider("Sepal Length", min_val=0, max_val=10, default=4.5) + +# Create another section that's open by default +collapsible("Main Visualizations") +text("These are the primary visualizations for your data.") +# Add more components here... +``` + +### Key Features + +1. **Organized Layout**: Group related components to create a cleaner, more structured interface. +2. **Reduced Visual Clutter**: Hide optional or advanced controls that aren't needed immediately. +3. **Improved User Experience**: Create step-by-step workflows or categorize UI elements by function. +4. **Responsive Design**: Helps make dense dashboards more manageable on smaller screens. + +### Why Use `collapsible`? + +The `collapsible` component is essential for building complex applications with many UI elements. By organizing components into expandable/collapsible sections, you can create more intuitive interfaces that guide users through your data application. + +Enhance your layout with the `collapsible` component! 🔽 \ No newline at end of file diff --git a/examples/iris/hello.py b/examples/iris/hello.py index ca3ed935..1ed590d3 100644 --- a/examples/iris/hello.py +++ b/examples/iris/hello.py @@ -5,6 +5,7 @@ from preswald import ( chat, + collapsible, # fastplotlib, get_df, plotly, @@ -29,6 +30,9 @@ # Load the CSV df = get_df("iris_csv") +# Add collapsible for Sepal visualizations +collapsible("Sepal Visualizations", open=True) + # 1. Scatter plot - Sepal Length vs Sepal Width text( "## Sepal Length vs Sepal Width \n This scatter plot shows the relationship between sepal length and sepal width for different iris species. We can see that Setosa is well-separated from the other two species, while Versicolor and Virginica show some overlap." @@ -68,6 +72,9 @@ fig5.update_layout(template="plotly_white") plotly(fig5) +# Add collapsible for Petal visualizations +collapsible("Petal Visualizations", open=False) + # 4. Violin plot of Sepal Length by Species text( "## Sepal Length Distribution by Species \n The violin plot provides a better understanding of the distribution of sepal lengths within each species. We can see the density of values and how they vary across species." @@ -96,68 +103,8 @@ fig10.update_layout(template="plotly_white") plotly(fig10) -# # 6. Fastplotlib Examples -# -# # Retrieve client_id from component state -# client_id = service.get_component_state("client_id") -# -# sidebar(defaultopen=True) -# text("# Fastplotlib Examples") -# -# # 6.1. Simple Image Plot -# text("## Simple Image Plot") -# fig = fpl.Figure(size=(700, 560), canvas="offscreen") -# fig._client_id = client_id -# fig._label = "Simple Image Plot" -# data = iio.imread("images/logo.png") -# fig[0, 0].add_image(data) -# fastplotlib(fig) -# -# # 6.2. Line Plot -# text("## Line Plot") -# x = np.linspace(-1, 10, 100) -# y = np.sin(x) -# sine = np.column_stack([x, y]) -# fig = fpl.Figure(size=(700, 560), canvas="offscreen") -# fig._client_id = client_id -# fig._label = "Line Plot" -# fig[0, 0].add_line(data=sine, colors="w") -# fastplotlib(fig) -# -# # 6.3. Line Plot with Color Maps -# text("## Line Plot ColorMap") -# fig = fpl.Figure(size=(700, 560), canvas="offscreen") -# fig._client_id = client_id -# fig._label = "Line Plot Color Map" -# xs = np.linspace(-10, 10, 100) -# ys = np.sin(xs) -# sine = np.dstack([xs, ys])[0] -# ys = np.cos(xs) - 5 -# cosine = np.dstack([xs, ys])[0] -# -# sine_graphic = fig[0, 0].add_line( -# data=sine, thickness=10, cmap="plasma", cmap_transform=sine[:, 1] -# ) -# labels = [0] * 25 + [5] * 10 + [1] * 35 + [2] * 30 -# cosine_graphic = fig[0, 0].add_line( -# data=cosine, thickness=10, cmap="tab10", cmap_transform=labels -# ) -# fastplotlib(fig) -# -# # 6.4. Scatter Plot from Iris dataset -# text("## Scatter Plot") -# x = df["sepal.length"].tolist() -# y = df["petal.width"].tolist() -# variety = df["variety"].tolist() -# data = np.column_stack((x, y)) -# color_map = {"Setosa": "yellow", "Versicolor": "cyan", "Virginica": "magenta"} -# colors = [color_map[v] for v in variety] -# -# fig = fpl.Figure(size=(700, 560), canvas="offscreen") -# fig._client_id = client_id -# fig._label = "Scatter Plot" -# fig[0, 0].add_scatter(data=data, sizes=4, colors=colors) -# fastplotlib(fig) +# Add collapsible for data view +collapsible("Dataset View", open=False) # Show the first 10 rows of the dataset text( diff --git a/frontend/components.json b/frontend/components.json index d1b7544d..4da4ee89 100644 --- a/frontend/components.json +++ b/frontend/components.json @@ -18,4 +18,4 @@ "hooks": "@/hooks" }, "iconLibrary": "lucide" -} \ No newline at end of file +} diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index aa95a80d..b9bcbcda 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -1,24 +1,17 @@ -import js from '@eslint/js' -import globals from 'globals' -import react from 'eslint-plugin-react' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import importPlugin from 'eslint-plugin-import' - +import js from '@eslint/js'; +import importPlugin from 'eslint-plugin-import'; +import react from 'eslint-plugin-react'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import globals from 'globals'; export default [ { - ignores: [ - 'dist', - 'node_modules', - '*.config.js', - ] + ignores: ['dist', 'node_modules', '*.config.js'], }, { files: ['**/*.{js,jsx}'], - extends: [ - 'prettier' - ], + extends: ['prettier'], languageOptions: { ecmaVersion: 2020, globals: { @@ -51,48 +44,45 @@ export default [ ...react.configs['jsx-runtime'].rules, ...reactHooks.configs.recommended.rules, 'react/jsx-no-target-blank': 'off', - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + 'no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + }, ], - 'no-unused-vars': ['error', { - argsIgnorePattern: '^_', - varsIgnorePattern: '^_', - }], 'no-console': ['warn', { allow: ['warn', 'error'] }], // Disable ESLint's import ordering to let Prettier handle it 'import/order': 'off', ...react.configs['jsx-runtime'].rules, ...reactHooks.configs.recommended.rules, 'react/jsx-no-target-blank': 'off', - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + 'no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + }, ], - 'no-unused-vars': ['error', { - argsIgnorePattern: '^_', - varsIgnorePattern: '^_', - }], 'no-console': ['warn', { allow: ['warn', 'error'] }], - 'import/order': ['error', { - groups: [ - 'builtin', - 'external', - 'internal', - ['parent', 'sibling'], - 'index', - ], - 'newlines-between': 'always', - pathGroups: [ - { pattern: '^react', group: 'external', position: 'before' }, - { pattern: '^@/components/(.*)$', group: 'internal', position: 'before' }, - { pattern: '^@/(.*)$', group: 'internal' }, - ], - alphabetize: { - order: 'asc', - caseInsensitive: true, + 'import/order': [ + 'error', + { + groups: ['builtin', 'external', 'internal', ['parent', 'sibling'], 'index'], + 'newlines-between': 'always', + pathGroups: [ + { pattern: '^react', group: 'external', position: 'before' }, + { pattern: '^@/components/(.*)$', group: 'internal', position: 'before' }, + { pattern: '^@/(.*)$', group: 'internal' }, + ], + alphabetize: { + order: 'asc', + caseInsensitive: true, + }, }, - }], + ], }, }, -] \ No newline at end of file +]; diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js index 2e7af2b7..2aa7205d 100644 --- a/frontend/postcss.config.js +++ b/frontend/postcss.config.js @@ -3,4 +3,4 @@ export default { tailwindcss: {}, autoprefixer: {}, }, -} +}; diff --git a/frontend/src/components/DynamicComponents.jsx b/frontend/src/components/DynamicComponents.jsx index 0db33ff3..9feb44b4 100644 --- a/frontend/src/components/DynamicComponents.jsx +++ b/frontend/src/components/DynamicComponents.jsx @@ -14,6 +14,7 @@ import BigNumberWidget from './widgets/BigNumberWidget'; import ButtonWidget from './widgets/ButtonWidget'; import ChatWidget from './widgets/ChatWidget'; import CheckboxWidget from './widgets/CheckboxWidget'; +import CollapsibleWidget from './widgets/CollapsibleWidget'; import DAGVisualizationWidget from './widgets/DAGVisualizationWidget'; import DataVisualizationWidget from './widgets/DataVisualizationWidget'; import FastplotlibWidget from './widgets/FastplotlibWidget'; @@ -74,6 +75,18 @@ const MemoizedComponent = memo( case 'sidebar': return ; + case 'collapsible': + return ( + + {props.children} + + ); + case 'button': return ( { + const [isOpen, setIsOpen] = React.useState(_open); + + return ( + + +
setIsOpen(!isOpen)} + > +

{_label}

+ +
+ +
{children}
+
+
+
+ ); +}; + +export default CollapsibleWidget; diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 3dc4d8be..fc9e19b0 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -1,96 +1,96 @@ /** @type {import('tailwindcss').Config} */ -import typography from "@tailwindcss/typography"; +import typography from '@tailwindcss/typography'; export default { - darkMode: ["class"], - content: [ - "./index.html", // Include index.html - "./src/**/*.{js,jsx}", // Include all JS and JSX files in src - ], - theme: { - extend: { - borderRadius: { - lg: 'var(--radius)', - md: 'calc(var(--radius) - 2px)', - sm: 'calc(var(--radius) - 4px)' - }, - colors: { - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', - card: { - DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))' - }, - popover: { - DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))' - }, - primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))' - }, - secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))' - }, - muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))' - }, - accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))' - }, - destructive: { - DEFAULT: 'hsl(var(--destructive))', - foreground: 'hsl(var(--destructive-foreground))' - }, - border: 'hsl(var(--border))', - input: 'hsl(var(--input))', - ring: 'hsl(var(--ring))', - chart: { - '1': 'hsl(var(--chart-1))', - '2': 'hsl(var(--chart-2))', - '3': 'hsl(var(--chart-3))', - '4': 'hsl(var(--chart-4))', - '5': 'hsl(var(--chart-5))' - }, - sidebar: { - DEFAULT: 'hsl(var(--sidebar-background))', - foreground: 'hsl(var(--sidebar-foreground))', - primary: 'hsl(var(--sidebar-primary))', - 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))', - accent: 'hsl(var(--sidebar-accent))', - 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))', - border: 'hsl(var(--sidebar-border))', - ring: 'hsl(var(--sidebar-ring))' - } - }, - keyframes: { - "accordion-down": { - from: { height: 0 }, - to: { height: "var(--radix-accordion-content-height)" }, - }, - "accordion-up": { - from: { height: "var(--radix-accordion-content-height)" }, - to: { height: 0 }, - }, - "slide-from-left": { - "0%": { transform: "translateX(-100%)" }, - "100%": { transform: "translateX(0)" }, - }, - "slide-to-left": { - "0%": { transform: "translateX(0)" }, - "100%": { transform: "translateX(-100%)" }, - }, - }, - animation: { - "accordion-down": "accordion-down 0.2s ease-out", - "accordion-up": "accordion-up 0.2s ease-out", - "slide-from-left": "slide-from-left 0.3s ease-out", - "slide-to-left": "slide-to-left 0.3s ease-in", - }, - } - }, - plugins: [typography, require("tailwindcss-animate")], + darkMode: ['class'], + content: [ + './index.html', // Include index.html + './src/**/*.{js,jsx}', // Include all JS and JSX files in src + ], + theme: { + extend: { + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)', + }, + colors: { + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))', + }, + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + chart: { + 1: 'hsl(var(--chart-1))', + 2: 'hsl(var(--chart-2))', + 3: 'hsl(var(--chart-3))', + 4: 'hsl(var(--chart-4))', + 5: 'hsl(var(--chart-5))', + }, + sidebar: { + DEFAULT: 'hsl(var(--sidebar-background))', + foreground: 'hsl(var(--sidebar-foreground))', + primary: 'hsl(var(--sidebar-primary))', + 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))', + accent: 'hsl(var(--sidebar-accent))', + 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))', + border: 'hsl(var(--sidebar-border))', + ring: 'hsl(var(--sidebar-ring))', + }, + }, + keyframes: { + 'accordion-down': { + from: { height: 0 }, + to: { height: 'var(--radix-accordion-content-height)' }, + }, + 'accordion-up': { + from: { height: 'var(--radix-accordion-content-height)' }, + to: { height: 0 }, + }, + 'slide-from-left': { + '0%': { transform: 'translateX(-100%)' }, + '100%': { transform: 'translateX(0)' }, + }, + 'slide-to-left': { + '0%': { transform: 'translateX(0)' }, + '100%': { transform: 'translateX(-100%)' }, + }, + }, + animation: { + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out', + 'slide-from-left': 'slide-from-left 0.3s ease-out', + 'slide-to-left': 'slide-to-left 0.3s ease-in', + }, + }, + }, + plugins: [typography, require('tailwindcss-animate')], }; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 5e0998e1..53c904d2 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -1,10 +1,10 @@ { - "include": ["src/**/*.tsx", "src/**/*.ts"], - "compilerOptions": { - "jsx": "react", - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] - } + "include": ["src/**/*.tsx", "src/**/*.ts"], + "compilerOptions": { + "jsx": "react", + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] } - } \ No newline at end of file + } +} diff --git a/frontend/vite.config.js b/frontend/vite.config.js index a9f75046..fc1ea445 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -1,23 +1,23 @@ -import { defineConfig } from "vite"; -import path from "path"; -import react from "@vitejs/plugin-react"; +import react from '@vitejs/plugin-react'; +import path from 'path'; +import { defineConfig } from 'vite'; // https://vite.dev/config/ export default defineConfig({ build: { - outDir: path.join(path.dirname(__dirname), "preswald", "static"), + outDir: path.join(path.dirname(__dirname), 'preswald', 'static'), emptyOutDir: true, }, - publicDir: "public", + publicDir: 'public', plugins: [react()], resolve: { alias: { - "@": path.resolve(__dirname, "./src"), + '@': path.resolve(__dirname, './src'), }, }, server: { proxy: { - "/api": "http://localhost:8501", // Forward API requests to FastAPI + '/api': 'http://localhost:8501', // Forward API requests to FastAPI }, }, }); diff --git a/preswald/interfaces/__init__.py b/preswald/interfaces/__init__.py index b177c066..6ac53c93 100644 --- a/preswald/interfaces/__init__.py +++ b/preswald/interfaces/__init__.py @@ -9,6 +9,7 @@ button, chat, checkbox, + collapsible, # fastplotlib, image, json_viewer, diff --git a/preswald/interfaces/components.py b/preswald/interfaces/components.py index d4d2897d..42540ed6 100644 --- a/preswald/interfaces/components.py +++ b/preswald/interfaces/components.py @@ -1,5 +1,6 @@ # Standard Library import base64 +import hashlib import io import json import logging @@ -22,10 +23,11 @@ # fplt = None # Internal from preswald.engine.service import PreswaldService -from preswald.interfaces.workflow import Workflow from preswald.interfaces.component_return import ComponentReturn +from preswald.interfaces.workflow import Workflow from preswald.utils import with_render_tracking + # Configure logging logger = logging.getLogger(__name__) @@ -33,8 +35,14 @@ # Components -@with_render_tracking('alert') -def alert(message: str, level: str = "info", size: float = 1.0, component_id: str | None = None) -> ComponentReturn: + +@with_render_tracking("alert") +def alert( + message: str, + level: str = "info", + size: float = 1.0, + component_id: str | None = None, +) -> ComponentReturn: """Create an alert component.""" logger.debug(f"Creating alert component with id {component_id}, message: {message}") @@ -49,7 +57,8 @@ def alert(message: str, level: str = "info", size: float = 1.0, component_id: st return ComponentReturn(message, component) -@with_render_tracking('big_number') + +@with_render_tracking("big_number") def big_number( value: int | float | str, label: str | None = None, @@ -58,7 +67,7 @@ def big_number( icon: str | None = None, description: str | None = None, size: float = 1.0, - component_id: str | None = None + component_id: str | None = None, ) -> ComponentReturn: """Create a big number metric card component.""" @@ -82,14 +91,15 @@ def big_number( return ComponentReturn(str(value), component) -@with_render_tracking('button') + +@with_render_tracking("button") def button( label: str, variant: str = "default", disabled: bool = False, loading: bool = False, size: float = 1.0, - component_id: str | None = None + component_id: str | None = None, ) -> ComponentReturn: """Create a button component that returns True when clicked.""" service = PreswaldService.get_instance() @@ -113,8 +123,11 @@ def button( return ComponentReturn(current_value, component) -@with_render_tracking('chat') -def chat(source: str, table: str | None = None, component_id: str | None = None) -> ComponentReturn: + +@with_render_tracking("chat") +def chat( + source: str, table: str | None = None, component_id: str | None = None +) -> ComponentReturn: """Create a chat component to chat with data source""" service = PreswaldService.get_instance() @@ -163,8 +176,14 @@ def chat(source: str, table: str | None = None, component_id: str | None = None) return ComponentReturn(component, component) -@with_render_tracking('checkbox') -def checkbox(label: str, default: bool = False, size: float = 1.0, component_id: str | None = None) -> ComponentReturn: + +@with_render_tracking("checkbox") +def checkbox( + label: str, + default: bool = False, + size: float = 1.0, + component_id: str | None = None, +) -> ComponentReturn: """Create a checkbox component with consistent ID based on label.""" service = PreswaldService.get_instance() @@ -185,6 +204,37 @@ def checkbox(label: str, default: bool = False, size: float = 1.0, component_id: return ComponentReturn(current_value, component) +@with_render_tracking("collapsible") +def collapsible( + label: str, open: bool = True, size: float = 1.0, component_id: str | None = None +) -> ComponentReturn: + """Create a collapsible component to group related UI components. + + Args: + label: Title/header of the collapsible section + open: Whether the section is open by default + size: Size of the component relative to container (0.0-1.0) + + Returns: + ComponentReturn: Component metadata + """ + component_id = ( + component_id or f"collapsible-{hashlib.md5(label.encode()).hexdigest()[:8]}" + ) + + # SAFE ID + component = { + "type": "collapsible", + "id": component_id, + "label": label, + "open": open, + "size": size, + } + + logger.debug(f"[collapsible] ID={component_id}, label={label}") + return ComponentReturn(component, component) + + # def fastplotlib(fig: "fplt.Figure", size: float = 1.0) -> str: # """ # Render a Fastplotlib figure and asynchronously stream the resulting image to the frontend. @@ -259,8 +309,10 @@ def checkbox(label: str, default: bool = False, size: float = 1.0, component_id: # return component_id -@with_render_tracking('image') -def image(src, alt="Image", size=1.0, component_id: str | None = None) -> ComponentReturn: +@with_render_tracking("image") +def image( + src, alt="Image", size=1.0, component_id: str | None = None +) -> ComponentReturn: """Create an image component. Args: @@ -320,9 +372,14 @@ def image(src, alt="Image", size=1.0, component_id: str | None = None) -> Compon return ComponentReturn(component, component) -@with_render_tracking('json_viewer') + +@with_render_tracking("json_viewer") def json_viewer( - data, title: str | None = None, expanded: bool = True, size: float = 1.0, component_id: str | None = None + data, + title: str | None = None, + expanded: bool = True, + size: float = 1.0, + component_id: str | None = None, ) -> dict: """Create a JSON viewer component with collapsible tree view.""" # Attempt to ensure JSON is serializable and safe @@ -346,10 +403,12 @@ def json_viewer( logger.debug(f"Created JSON viewer component with id {component_id}") return ComponentReturn(component, component) - -@with_render_tracking('matplotlib') -def matplotlib(fig: plt.Figure | None = None, label: str = "plot", component_id: str | None = None) -> ComponentReturn: + +@with_render_tracking("matplotlib") +def matplotlib( + fig: plt.Figure | None = None, label: str = "plot", component_id: str | None = None +) -> ComponentReturn: """Render a Matplotlib figure as a component.""" if fig is None: @@ -369,12 +428,18 @@ def matplotlib(fig: plt.Figure | None = None, label: str = "plot", component_id: "image": img_b64, # Store the image data } - return ComponentReturn(component_id, component) # Returning ID for potential tracking + return ComponentReturn( + component_id, component + ) # Returning ID for potential tracking -@with_render_tracking('playground') +@with_render_tracking("playground") def playground( - label: str, query: str, source: str | None = None, size: float = 1.0, component_id: str | None = None + label: str, + query: str, + source: str | None = None, + size: float = 1.0, + component_id: str | None = None, ) -> ComponentReturn: """ Create a playground component for interactive data querying and visualization. @@ -467,7 +532,8 @@ def playground( # Return the raw DataFrame return ComponentReturn(data, component) -@with_render_tracking('plotly') + +@with_render_tracking("plotly") def plotly(fig, size: float = 1.0, component_id: str | None = None) -> ComponentReturn: # noqa: C901 """ Render a Plotly figure. @@ -609,8 +675,11 @@ def plotly(fig, size: float = 1.0, component_id: str | None = None) -> Component return ComponentReturn(error_component, error_component) -@with_render_tracking('progress') -def progress(label: str, value: float = 0.0, size: float = 1.0, component_id: str | None = None) -> ComponentReturn: + +@with_render_tracking("progress") +def progress( + label: str, value: float = 0.0, size: float = 1.0, component_id: str | None = None +) -> ComponentReturn: """Create a progress component.""" logger.debug(f"Creating progress component with id {component_id}, label: {label}") @@ -624,9 +693,14 @@ def progress(label: str, value: float = 0.0, size: float = 1.0, component_id: st return ComponentReturn(value, component) -@with_render_tracking('selectbox') + +@with_render_tracking("selectbox") def selectbox( - label: str, options: list[str], default: str | None = None, size: float = 1.0, component_id: str | None = None + label: str, + options: list[str], + default: str | None = None, + size: float = 1.0, + component_id: str | None = None, ) -> ComponentReturn: """Create a select component with consistent ID based on label.""" service = PreswaldService.get_instance() @@ -650,7 +724,7 @@ def selectbox( return ComponentReturn(current_value, component) -@with_render_tracking('separator') +@with_render_tracking("separator") def separator(component_id: str | None = None) -> ComponentReturn: """Create a separator component that forces a new row.""" component = {"type": "separator", "id": component_id} @@ -658,7 +732,8 @@ def separator(component_id: str | None = None) -> ComponentReturn: logger.debug(f"[separator] ID={component_id}") return ComponentReturn(component, component) -@with_render_tracking('slider') + +@with_render_tracking("slider") def slider( label: str, min_val: float = 0.0, @@ -690,7 +765,8 @@ def slider( logger.debug(f"[slider] ID={component_id}, value={current_value}") return ComponentReturn(current_value, component) -@with_render_tracking('spinner') + +@with_render_tracking("spinner") def spinner( label: str = "Loading...", variant: str = "default", @@ -719,8 +795,11 @@ def spinner( logger.debug(f"[spinner] ID={component_id}") return ComponentReturn(None, component) -@with_render_tracking('sidebar') -def sidebar(defaultopen: bool = False, component_id: str | None = None) -> ComponentReturn: + +@with_render_tracking("sidebar") +def sidebar( + defaultopen: bool = False, component_id: str | None = None +) -> ComponentReturn: """Create a sidebar component.""" component = {"type": "sidebar", "id": component_id, "defaultopen": defaultopen} @@ -729,9 +808,12 @@ def sidebar(defaultopen: bool = False, component_id: str | None = None) -> Compo return ComponentReturn(component, component) -@with_render_tracking('table') +@with_render_tracking("table") def table( - data: pd.DataFrame, title: str | None = None, limit: int | None = None, component_id: str | None = None + data: pd.DataFrame, + title: str | None = None, + limit: int | None = None, + component_id: str | None = None, ) -> ComponentReturn: """Create a table component that renders data using TableViewerWidget. @@ -815,8 +897,10 @@ def table( return ComponentReturn(error_component, error_component) -@with_render_tracking('text') -def text(markdown_str: str, size: float = 1.0, component_id: str | None = None) -> ComponentReturn: +@with_render_tracking("text") +def text( + markdown_str: str, size: float = 1.0, component_id: str | None = None +) -> ComponentReturn: """Create a text/markdown component.""" component = { "type": "text", @@ -830,13 +914,13 @@ def text(markdown_str: str, size: float = 1.0, component_id: str | None = None) return ComponentReturn(markdown_str, component) -@with_render_tracking('text_input') +@with_render_tracking("text_input") def text_input( label: str, placeholder: str = "", default: str = "", size: float = 1.0, - component_id: str | None = None + component_id: str | None = None, ) -> ComponentReturn: """Create a text input component. @@ -869,7 +953,7 @@ def text_input( return ComponentReturn(current_value, component) -@with_render_tracking('topbar') +@with_render_tracking("topbar") def topbar(component_id: str | None = None) -> ComponentReturn: """Creates a topbar component.""" component = {"type": "topbar", "id": component_id} @@ -878,8 +962,12 @@ def topbar(component_id: str | None = None) -> ComponentReturn: return ComponentReturn(component, component) -@with_render_tracking('workflow_dag') -def workflow_dag(workflow: Workflow, title: str = "Workflow Dependency Graph", component_id: str | None = None) -> ComponentReturn: +@with_render_tracking("workflow_dag") +def workflow_dag( + workflow: Workflow, + title: str = "Workflow Dependency Graph", + component_id: str | None = None, +) -> ComponentReturn: """ Render the workflow's DAG visualization.