From 867e5e16bf1cc6a5b93cd11ea6156f8c3d863cfa Mon Sep 17 00:00:00 2001 From: Jaswant Singh Date: Thu, 9 Apr 2026 12:27:36 +0530 Subject: [PATCH] feat: add showcase artifact component Signed-off-by: Jaswant Singh --- hax/artifacts/showcase/action.ts | 101 ++++ hax/artifacts/showcase/description.ts | 41 ++ hax/artifacts/showcase/index.ts | 34 ++ hax/artifacts/showcase/showcase.tsx | 649 ++++++++++++++++++++++++++ hax/artifacts/showcase/types.ts | 66 +++ 5 files changed, 891 insertions(+) create mode 100644 hax/artifacts/showcase/action.ts create mode 100644 hax/artifacts/showcase/description.ts create mode 100644 hax/artifacts/showcase/index.ts create mode 100644 hax/artifacts/showcase/showcase.tsx create mode 100644 hax/artifacts/showcase/types.ts diff --git a/hax/artifacts/showcase/action.ts b/hax/artifacts/showcase/action.ts new file mode 100644 index 0000000..fef7f04 --- /dev/null +++ b/hax/artifacts/showcase/action.ts @@ -0,0 +1,101 @@ +/* + * 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 { z } from "zod"; +import { + ShowcaseArtifact, + ShowcaseItemZod, + ShowcaseCategoryZod, + ShowcaseVariantZod, +} from "./types"; +import { SHOWCASE_DESCRIPTION } from "./description"; + +interface UseShowcaseActionProps { + addOrUpdateArtifact: (artifact: ShowcaseArtifact) => void; +} + +export const useShowcaseAction = ({ + addOrUpdateArtifact, +}: UseShowcaseActionProps) => { + useCopilotAction({ + name: "create_showcase", + description: SHOWCASE_DESCRIPTION, + parameters: [ + { + name: "variant", + type: "string", + description: + "Layout variant: grid, list, dense-grid, table, categorized, featured", + required: false, + }, + { + name: "itemsJson", + type: "string", + description: + "JSON string of items array: [{id, title, description, badge?, imageUrl?, author?, category?}]", + required: false, + }, + { + name: "categoriesJson", + type: "string", + description: + 'JSON string of categories array (for categorized variant): [{title, icon?, items: [...]}]', + required: false, + }, + ], + handler: async (args: { + variant?: string; + itemsJson?: string; + categoriesJson?: string; + }) => { + try { + const variant = args.variant + ? ShowcaseVariantZod.parse(args.variant) + : "grid"; + + let items: z.infer[] | undefined; + if (args.itemsJson) { + const parsed = JSON.parse(args.itemsJson); + items = z.array(ShowcaseItemZod).parse(parsed); + } + + let categories: z.infer[] | undefined; + if (args.categoriesJson) { + const parsed = JSON.parse(args.categoriesJson); + categories = z.array(ShowcaseCategoryZod).parse(parsed); + } + + const artifact: ShowcaseArtifact = { + id: `showcase-${Date.now()}`, + type: "showcase", + data: { + variant, + ...(items && { items }), + ...(categories && { categories }), + }, + }; + + addOrUpdateArtifact(artifact); + return `Created showcase (${variant}) with ${items?.length ?? 0} items`; + } catch (error) { + return `Error creating showcase: ${String(error)}`; + } + }, + }); +}; \ No newline at end of file diff --git a/hax/artifacts/showcase/description.ts b/hax/artifacts/showcase/description.ts new file mode 100644 index 0000000..14538b1 --- /dev/null +++ b/hax/artifacts/showcase/description.ts @@ -0,0 +1,41 @@ +/* + * 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 SHOWCASE_DESCRIPTION = + `Use the showcase artifact to present a browsable collection of items for the user to explore, preview, and act on. Best for displaying templates, search results, recommendations, generated options, or curated content. + +Supports 6 layout variants: +- "grid": 3x2 vertical card grid with image, badge, heading, description. Best for visual-heavy content. +- "list": 2-column horizontal cards with image left, content right, Preview button + favorite. Best for items with rich metadata. +- "dense-grid": 4x3 compact grid with smaller cards. Best for large collections. +- "table": Row-based table with Preview thumbnail, Title, Author, Category badge, action icons. Best for data-dense scanning. +- "categorized": Sections with icon headers (Trending/Star/Clock), 3 overlay cards per section. Best for grouped/curated content. +- "featured": Full-width hero card + 3-column masonry grid below. Best for highlighting a primary item with supporting items. + +Each item requires: id (unique), title, description. +Optional per item: badge (category label), imageUrl (preview), author, category. + +For "categorized" variant, provide categories array instead of items. Each category has: title, icon (trending/star/clock), items array. + +Choose the variant that best matches the user's intent: +- Browsing/exploring → grid or dense-grid +- Comparing with details → list or table +- Curated sections → categorized +- Highlighting one item → featured + +Ensure each item has a unique id. Include badges for categorization. Keep descriptions concise (1-2 sentences).` as const; \ No newline at end of file diff --git a/hax/artifacts/showcase/index.ts b/hax/artifacts/showcase/index.ts new file mode 100644 index 0000000..63c269b --- /dev/null +++ b/hax/artifacts/showcase/index.ts @@ -0,0 +1,34 @@ +/* + * 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 { HAXShowcase } from "./showcase"; +export type { HAXShowcaseProps } from "./showcase"; +export { useShowcaseAction } from "./action"; +export { SHOWCASE_DESCRIPTION } from "./description"; +export { + ShowcaseArtifactZod, + ShowcaseItemZod, + ShowcaseCategoryZod, + ShowcaseVariantZod, +} from "./types"; +export type { + ShowcaseArtifact, + ShowcaseItemData, + ShowcaseCategoryData, + ShowcaseVariant, +} from "./types"; \ No newline at end of file diff --git a/hax/artifacts/showcase/showcase.tsx b/hax/artifacts/showcase/showcase.tsx new file mode 100644 index 0000000..ed335d8 --- /dev/null +++ b/hax/artifacts/showcase/showcase.tsx @@ -0,0 +1,649 @@ +/* + * 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 { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { Eye, Heart, TrendingUp, Star, Clock3, ArrowRight } from "lucide-react" +import type { + ShowcaseArtifact, + ShowcaseItemData, + ShowcaseCategoryData, + ShowcaseVariant, +} from "./types" + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +const cardShadow = "0 1px 3px rgba(0,0,0,0.1), 0 1px 2px -1px rgba(0,0,0,0.1)" +const sectionIcons = { trending: TrendingUp, star: Star, clock: Clock3 } +const sectionIconBg: Record = { + trending: "#f3e8ff", + star: "#dbeafe", + clock: "#ffedd5", +} + +// --------------------------------------------------------------------------- +// Sub-components (not exported) +// --------------------------------------------------------------------------- + +/* ---------- PaginationDots ---------- */ + +function PaginationDots({ total, active = 0 }: { total: number; active?: number }) { + return ( +
+ {Array.from({ length: total }).map((_, i) => ( +
+ ))} +
+ ) +} + +/* ---------- MediaPlaceholder ---------- */ + +function MediaPlaceholder({ className }: { className?: string }) { + return ( +
+
+
+
+
+
+ ) +} + +/* ---------- Badge ---------- */ + +function Badge({ + label, + variant = "default", +}: { + label: string + variant?: "default" | "purple" +}) { + return ( + + {label} + + ) +} + +/* ---------- CardContainer (text area below image) ---------- */ + +function CardContainer({ + item, + showButtons = false, + clamp = false, + padding = "p-1", + onPreview, + onFavorite, +}: { + item: ShowcaseItemData + showButtons?: boolean + clamp?: boolean + padding?: string + onPreview?: () => void + onFavorite?: () => void +}) { + return ( +
+ {item.badge && } +
+

+ {item.title} +

+

+ {item.description} +

+
+ {showButtons && ( +
+ + +
+ )} +
+ ) +} + +/* ---------- CardVariation2: Vertical Card ---------- */ + +function VerticalCard({ + item, + imageHeight = "h-[260px]", + showButtons = false, + clamp = false, + onPreview, + onFavorite, +}: { + item: ShowcaseItemData + imageHeight?: string + showButtons?: boolean + clamp?: boolean + onPreview?: () => void + onFavorite?: () => void +}) { + return ( +
+
+
+ + {item.badge && ( +
+ +
+ )} +
+
+ +
+ ) +} + +/* ---------- CardVariation3: Horizontal Card ---------- */ + +function HorizontalCard({ + item, + onPreview, + onFavorite, +}: { + item: ShowcaseItemData + onPreview?: () => void + onFavorite?: () => void +}) { + return ( +
+ +
+ +
+
+ ) +} + +/* ---------- CardVariation4: Overlay Card ---------- */ + +function OverlayCard({ + item, + width, + height, + onPreview, + onFavorite, +}: { + item: ShowcaseItemData + width?: string + height?: string + onPreview?: () => void + onFavorite?: () => void +}) { + return ( +
+
+ +
+
+ +
+
+ ) +} + +/* ---------- CardVariation5: Masonry Card ---------- */ + +function MasonryCard({ item, tall = false }: { item: ShowcaseItemData; tall?: boolean }) { + const h = tall ? "h-[420px]" : "h-[292px]" + const imgH = tall ? "h-[307px]" : "h-[179px]" + + return ( +
+ +
+ +
+
+ ) +} + +/* ---------- CardVariation6: Hero Card ---------- */ + +function HeroCard({ + item, + onPreview, + onFavorite, +}: { + item: ShowcaseItemData + onPreview?: () => void + onFavorite?: () => void +}) { + return ( +
+ +
+ +
+
+ ) +} + +// --------------------------------------------------------------------------- +// View Variations +// --------------------------------------------------------------------------- + +/* ---------- Grid View (3x2) ---------- */ + +function GridView({ + items, + onPreview, + onFavorite, +}: { + items: ShowcaseItemData[] + onPreview?: (item: ShowcaseItemData) => void + onFavorite?: (item: ShowcaseItemData) => void +}) { + return ( +
+
+ {items.slice(0, 6).map((item) => ( + onPreview?.(item)} + onFavorite={() => onFavorite?.(item)} + /> + ))} +
+ +
+ ) +} + +/* ---------- List View (Horizontal Cards) ---------- */ + +function ListView({ + items, + onPreview, + onFavorite, +}: { + items: ShowcaseItemData[] + onPreview?: (item: ShowcaseItemData) => void + onFavorite?: (item: ShowcaseItemData) => void +}) { + return ( +
+
+ {items.slice(0, 6).map((item) => ( + onPreview?.(item)} + onFavorite={() => onFavorite?.(item)} + /> + ))} +
+ +
+ ) +} + +/* ---------- Dense Grid View (4x3) ---------- */ + +function DenseGridView({ + items, + onPreview, + onFavorite, +}: { + items: ShowcaseItemData[] + onPreview?: (item: ShowcaseItemData) => void + onFavorite?: (item: ShowcaseItemData) => void +}) { + return ( +
+
+ {items.slice(0, 12).map((item) => ( + onPreview?.(item)} + onFavorite={() => onFavorite?.(item)} + /> + ))} +
+ +
+ ) +} + +/* ---------- Table View ---------- */ + +function TableView({ + items, + onPreview, + onFavorite, +}: { + items: ShowcaseItemData[] + onPreview?: (item: ShowcaseItemData) => void + onFavorite?: (item: ShowcaseItemData) => void +}) { + return ( +
+
+
+
Preview
+
Title
+
Author
+
Category
+
Actions
+
+
+ {items.slice(0, 10).map((item) => ( +
+
+ +
+
{item.title}
+
{item.author ?? "—"}
+
+ {item.category ? : "—"} +
+
+ + +
+
+ ))} +
+
+ +
+ ) +} + +/* ---------- Categorized View ---------- */ + +function CategorizedView({ + categories, + onPreview, + onFavorite, +}: { + categories: ShowcaseCategoryData[] + onPreview?: (item: ShowcaseItemData) => void + onFavorite?: (item: ShowcaseItemData) => void +}) { + return ( +
+
+ {categories.map((section, idx) => { + const Icon = section.icon ? sectionIcons[section.icon] : null + const iconBg = section.icon ? sectionIconBg[section.icon] : "#f3f4f6" + const sectionItems = section.items.slice(0, 3) + return ( +
+
+
+ {Icon && ( +
+ +
+ )} +

{section.title}

+
+ +
+
+ {sectionItems[0] && ( + onPreview?.(sectionItems[0])} + onFavorite={() => onFavorite?.(sectionItems[0])} + /> + )} + {sectionItems[1] && ( + onPreview?.(sectionItems[1])} + onFavorite={() => onFavorite?.(sectionItems[1])} + /> + )} + {sectionItems[2] && ( + onPreview?.(sectionItems[2])} + onFavorite={() => onFavorite?.(sectionItems[2])} + /> + )} +
+
+ ) + })} +
+ +
+ ) +} + +/* ---------- Featured/Masonry View ---------- */ + +function FeaturedView({ + items, + onPreview, + onFavorite, +}: { + items: ShowcaseItemData[] + onPreview?: (item: ShowcaseItemData) => void + onFavorite?: (item: ShowcaseItemData) => void +}) { + const hero = items[0] + const grid = items.slice(1, 10) + const col1 = [grid[0], grid[3], grid[6]] + const col2 = [grid[1], grid[4], grid[7]] + const col3 = [grid[2], grid[5], grid[8]] + const col1Pattern = [true, false, false] + const col2Pattern = [false, false, true] + const col3Pattern = [false, false, true] + + return ( +
+
+ {hero && ( + onPreview?.(hero)} + onFavorite={() => onFavorite?.(hero)} + /> + )} +
+
+ {col1.map( + (item, i) => + item && + )} +
+
+ {col2.map( + (item, i) => + item && + )} +
+
+ {col3.map( + (item, i) => + item && + )} +
+
+
+ +
+ ) +} + +// --------------------------------------------------------------------------- +// Main Component +// --------------------------------------------------------------------------- + +export interface HAXShowcaseProps extends React.HTMLAttributes { + variant?: ShowcaseArtifact["data"]["variant"] + items?: ShowcaseArtifact["data"]["items"] + categories?: ShowcaseArtifact["data"]["categories"] + onPreview?: (item: ShowcaseItemData) => void + onFavorite?: (item: ShowcaseItemData) => void +} + +export function HAXShowcase({ + variant = "grid", + items = [], + categories = [], + onPreview, + onFavorite, + className, + ...rest +}: HAXShowcaseProps) { + const shared = { items, onPreview, onFavorite } + + return ( +
+ {variant === "grid" && } + {variant === "list" && } + {variant === "dense-grid" && } + {variant === "table" && } + {variant === "categorized" && ( + + )} + {variant === "featured" && } +
+ ) +} \ No newline at end of file diff --git a/hax/artifacts/showcase/types.ts b/hax/artifacts/showcase/types.ts new file mode 100644 index 0000000..89bdd89 --- /dev/null +++ b/hax/artifacts/showcase/types.ts @@ -0,0 +1,66 @@ +/* + * 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 ShowcaseItemZod = z.object({ + id: z.string().describe("Unique identifier for the item"), + title: z.string().describe("Item title/heading"), + description: z.string().describe("Brief description of the item"), + badge: z.string().optional().describe("Category badge label (e.g. 'Template', 'Report')"), + imageUrl: z.string().optional().describe("Preview image URL"), + author: z.string().optional().describe("Author name (used in table view)"), + category: z.string().optional().describe("Category label (used in table view with purple badge)"), +}); + +export const ShowcaseCategoryZod = z.object({ + title: z.string().describe("Section heading (e.g. 'Trending', 'Most Popular')"), + subhead: z.string().optional().describe("Optional section subheading"), + icon: z + .enum(["trending", "star", "clock"]) + .optional() + .describe("Section icon type: trending (TrendingUp), star (Star), clock (Clock)"), + items: z + .array(ShowcaseItemZod) + .describe("Items in this category section"), +}); + +export const ShowcaseVariantZod = z + .enum(["grid", "list", "dense-grid", "table", "categorized", "featured"]) + .describe("Layout variant: grid (3x2 cards), list (horizontal cards with actions), dense-grid (4x3 compact), table (rows with columns), categorized (sections with headers), featured (hero + masonry)"); + +export const ShowcaseArtifactZod = z.object({ + id: z.string(), + type: z.literal("showcase"), + data: z.object({ + variant: ShowcaseVariantZod.optional().describe("Layout variant, defaults to grid"), + items: z + .array(ShowcaseItemZod) + .optional() + .describe("Items to display (used by all variants except categorized)"), + categories: z + .array(ShowcaseCategoryZod) + .optional() + .describe("Category sections (used only by categorized variant)"), + }), +}); + +export type ShowcaseItemData = z.infer; +export type ShowcaseCategoryData = z.infer; +export type ShowcaseVariant = z.infer; +export type ShowcaseArtifact = z.infer; \ No newline at end of file