diff --git a/hax/artifacts/data-table/action.ts b/hax/artifacts/data-table/action.ts new file mode 100644 index 0000000..0b4a5bc --- /dev/null +++ b/hax/artifacts/data-table/action.ts @@ -0,0 +1,76 @@ +/* + * Copyright 2025 Cisco Systems, Inc. and its affiliates + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useCopilotAction } from "@copilotkit/react-core"; +import { DataTableArtifact } from "./types"; +import { DATA_TABLE_DESCRIPTION } from "./description"; + +interface UseDataTableActionProps { + addOrUpdateArtifact: (type: "data-table", data: DataTableArtifact["data"]) => void; +} + +export const useDataTableAction = ({ addOrUpdateArtifact }: UseDataTableActionProps) => { + useCopilotAction({ + name: "create_data_table", + description: DATA_TABLE_DESCRIPTION, + parameters: [ + { + name: "columnsJson", + type: "string", + description: 'JSON array of columns: [{"id": "col1", "header": "Name", "sortable": true}]', + required: true, + }, + { + name: "rowsJson", + type: "string", + description: 'JSON array of rows: [{"id": "1", "cells": {"col1": "value"}}]. Cell values can be strings, numbers, or objects like {"type": "progress", "value": 75} or {"type": "labels", "items": [{"text": "Tag", "color": "green"}]} or {"type": "avatar", "initials": "AB", "name": "Alice"}', + required: true, + }, + { + name: "selectable", + type: "boolean", + description: "Whether rows have checkboxes for selection (default: true)", + required: false, + }, + { + name: "showActions", + type: "boolean", + description: "Whether to show Edit/Delete action buttons per row (default: true)", + required: false, + }, + ], + handler: async (args) => { + const { columnsJson, rowsJson, selectable, showActions } = args; + + try { + const columns = JSON.parse(columnsJson); + const rows = JSON.parse(rowsJson); + + const data: DataTableArtifact["data"] = { columns, rows }; + if (selectable !== undefined) data.selectable = selectable; + if (showActions !== undefined) data.showActions = showActions; + + addOrUpdateArtifact("data-table", data); + + return `Created data table with ${columns.length} columns and ${rows.length} rows`; + } catch (e) { + return `Error creating data table: invalid JSON`; + } + }, + }); +}; diff --git a/hax/artifacts/data-table/data-table.tsx b/hax/artifacts/data-table/data-table.tsx new file mode 100644 index 0000000..a2915d3 --- /dev/null +++ b/hax/artifacts/data-table/data-table.tsx @@ -0,0 +1,373 @@ +/* + * Copyright 2025 Cisco Systems, Inc. and its affiliates + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +"use client" + +import * as React from "react" +import { useState, useMemo } from "react" +import { ArrowUpDown, Pencil, Trash2, Check } from "lucide-react" +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +// --------------------------------------------------------------------------- +// Utility function (inlined to avoid path alias dependency) +// --------------------------------------------------------------------------- + +function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} + +// --------------------------------------------------------------------------- +// Internal Checkbox component (inlined to avoid dependency on @/components/ui) +// --------------------------------------------------------------------------- + +interface CheckboxProps { + checked?: boolean + onCheckedChange?: (checked: boolean) => void + className?: string +} + +function Checkbox({ checked = false, onCheckedChange, className }: CheckboxProps) { + return ( + + ) +} + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export type CellValue = + | string + | number + | { type: "avatar"; initials: string; name: string } + | { type: "progress"; value: number } + | { type: "labels"; items: LabelItem[] } + +export interface LabelItem { + text: string + color?: "red" | "green" | "blue" | "orange" | "gray" +} + +export interface DataTableColumn { + id: string + header: string + sortable?: boolean + align?: "left" | "right" | "center" +} + +export interface DataTableRow { + id: string + cells: Record +} + +export interface HAXDataTableProps { + columns: DataTableColumn[] + rows: DataTableRow[] + selectable?: boolean + showActions?: boolean + onEdit?: (rowId: string) => void + onDelete?: (rowId: string) => void + onSelectionChange?: (selectedIds: string[]) => void + className?: string +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function getProgressColor(value: number): string { + if (value <= 25) return "#EF4444" + if (value <= 50) return "#F97316" + if (value <= 75) return "#EAB308" + return "#22C55E" +} + +function getLabelClasses(color?: string): string { + switch (color) { + case "red": + return "bg-red-500 text-white border-red-500" + case "green": + return "bg-green-100 text-green-800 border-green-200" + case "blue": + return "bg-blue-100 text-blue-800 border-blue-200" + case "orange": + return "bg-orange-100 text-orange-800 border-orange-200" + default: + return "bg-gray-100 text-gray-700 border-gray-200" + } +} + +// --------------------------------------------------------------------------- +// Cell renderers +// --------------------------------------------------------------------------- + +function AvatarCell({ initials, name }: { initials: string; name: string }) { + return ( +
+
+ {initials} +
+ {name} +
+ ) +} + +function ProgressCell({ value }: { value: number }) { + const clamped = Math.max(0, Math.min(100, value)) + return ( +
+
+
+
+ {clamped}% +
+ ) +} + +function LabelsCell({ items }: { items: LabelItem[] }) { + return ( +
+ {items.map((label, i) => ( + + {label.text} + + ))} +
+ ) +} + +function renderCell(value: CellValue) { + if (value === null || value === undefined) return null + + if (typeof value === "string") { + return {value} + } + + if (typeof value === "number") { + return {value} + } + + switch (value.type) { + case "avatar": + return + case "progress": + return + case "labels": + return + default: + return null + } +} + +// --------------------------------------------------------------------------- +// HAXDataTable component +// --------------------------------------------------------------------------- + +export function HAXDataTable({ + columns, + rows, + selectable = true, + showActions = true, + onEdit, + onDelete, + onSelectionChange, + className, +}: HAXDataTableProps) { + const [selectedIds, setSelectedIds] = useState>(new Set()) + const [sortColumn, setSortColumn] = useState(null) + const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc") + + const allSelected = rows.length > 0 && selectedIds.size === rows.length + + const toggleSelectAll = () => { + const next = allSelected ? new Set() : new Set(rows.map((r) => r.id)) + setSelectedIds(next) + onSelectionChange?.([...next]) + } + + const toggleSelectRow = (id: string) => { + const next = new Set(selectedIds) + if (next.has(id)) next.delete(id) + else next.add(id) + setSelectedIds(next) + onSelectionChange?.([...next]) + } + + const handleSort = (colId: string) => { + if (sortColumn === colId) { + setSortDirection((d) => (d === "asc" ? "desc" : "asc")) + } else { + setSortColumn(colId) + setSortDirection("asc") + } + } + + const sortedRows = useMemo(() => { + if (!sortColumn) return rows + return [...rows].sort((a, b) => { + const av = a.cells[sortColumn] + const bv = b.cells[sortColumn] + + const aStr = typeof av === "string" ? av : typeof av === "number" ? String(av) : "" + const bStr = typeof bv === "string" ? bv : typeof bv === "number" ? String(bv) : "" + + const cmp = aStr.localeCompare(bStr, undefined, { numeric: true }) + return sortDirection === "asc" ? cmp : -cmp + }) + }, [rows, sortColumn, sortDirection]) + + return ( +
+
+
+ + {/* Header */} + + + {selectable && ( + + )} + {columns.map((col) => ( + + ))} + {showActions && ( + + + + {/* Body */} + + {sortedRows.map((row) => { + const isSelected = selectedIds.has(row.id) + return ( + + {selectable && ( + + )} + {columns.map((col) => ( + + ))} + {showActions && ( + + )} + + ) + })} + +
+ + + {col.sortable ? ( + + ) : ( + col.header + )} + + )} +
+ toggleSelectRow(row.id)} + /> + + {renderCell(row.cells[col.id])} + +
+ + +
+
+
+
+
+ ) +} + +// Also export the sub-types for convenience +export type { LabelItem as DataTableLabelItem } diff --git a/hax/artifacts/data-table/description.ts b/hax/artifacts/data-table/description.ts new file mode 100644 index 0000000..2e8eb07 --- /dev/null +++ b/hax/artifacts/data-table/description.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2025 Cisco Systems, Inc. and its affiliates + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export const DATA_TABLE_DESCRIPTION = + `Use data tables to display structured, tabular data that the user can manage and refine. Best for datasets, comparisons, lists with multiple attributes, task tracking, and bulk data management. + +Define columns with id and header. Each row has an id and a cells object mapping column ids to values. Cell values can be plain text, numbers, or special types: +- { "type": "avatar", "initials": "AB", "name": "Alice Brown" } for user avatars with names +- { "type": "progress", "value": 75 } for progress bars (0-100, auto-colored: red/orange/yellow/green) +- { "type": "labels", "items": [{ "text": "Tag", "color": "green" }] } for label badges (colors: red, green, blue, orange, gray) + +Keep tables concise — prefer 3-6 columns and reasonable row counts. Use sortable columns for key data fields.` as const; diff --git a/hax/artifacts/data-table/index.ts b/hax/artifacts/data-table/index.ts new file mode 100644 index 0000000..68db43a --- /dev/null +++ b/hax/artifacts/data-table/index.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2025 Cisco Systems, Inc. and its affiliates + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export { + HAXDataTable, +} from "./data-table" +export type { + HAXDataTableProps, + DataTableColumn, + DataTableRow, + CellValue, + LabelItem, + DataTableLabelItem, +} from "./data-table" +export { useDataTableAction } from "./action" +export type { DataTableArtifact, DataTableData } from "./types" +export { + DataTableArtifactZod, + DataTableColumnZod, + DataTableRowZod, + CellValueZod, + LabelItemZod, +} from "./types" +export { DATA_TABLE_DESCRIPTION } from "./description" diff --git a/hax/artifacts/data-table/types.ts b/hax/artifacts/data-table/types.ts new file mode 100644 index 0000000..5b769ec --- /dev/null +++ b/hax/artifacts/data-table/types.ts @@ -0,0 +1,58 @@ +/* + * Copyright 2025 Cisco Systems, Inc. and its affiliates + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { z } from "zod"; + +export const DataTableColumnZod = z.object({ + id: z.string(), + header: z.string(), + sortable: z.boolean().optional(), + align: z.enum(["left", "right", "center"]).optional(), +}); + +export const LabelItemZod = z.object({ + text: z.string(), + color: z.enum(["red", "green", "blue", "orange", "gray"]).optional(), +}); + +export const CellValueZod = z.union([ + z.string(), + z.number(), + z.object({ type: z.literal("avatar"), initials: z.string(), name: z.string() }), + z.object({ type: z.literal("progress"), value: z.number() }), + z.object({ type: z.literal("labels"), items: z.array(LabelItemZod) }), +]); + +export const DataTableRowZod = z.object({ + id: z.string(), + cells: z.record(z.string(), CellValueZod), +}); + +export const DataTableArtifactZod = z.object({ + id: z.string(), + type: z.literal("data-table"), + data: z.object({ + columns: z.array(DataTableColumnZod), + rows: z.array(DataTableRowZod), + selectable: z.boolean().optional(), + showActions: z.boolean().optional(), + }), +}); + +export type DataTableArtifact = z.infer; +export type DataTableData = DataTableArtifact["data"];