From 66591862d6a16ed820606bc8e0b96b385bd3f4a3 Mon Sep 17 00:00:00 2001 From: Jaswant Singh Date: Thu, 18 Dec 2025 18:35:08 +0530 Subject: [PATCH 1/4] feat: add findings artifact Signed-off-by: jaswantsingh09 Signed-off-by: Jaswant Singh --- cli/src/registry/default/artifacts.ts | 39 ++ .../registry/github-registry/artifacts.json | 18 + hax/artifacts/findings/README.md | 274 +++++++++++++ hax/artifacts/findings/action.ts | 86 ++++ hax/artifacts/findings/description.ts | 37 ++ hax/artifacts/findings/findings.tsx | 370 ++++++++++++++++++ hax/artifacts/findings/index.ts | 35 ++ hax/artifacts/findings/types.ts | 47 +++ 8 files changed, 906 insertions(+) create mode 100644 hax/artifacts/findings/README.md create mode 100644 hax/artifacts/findings/action.ts create mode 100644 hax/artifacts/findings/description.ts create mode 100644 hax/artifacts/findings/findings.tsx create mode 100644 hax/artifacts/findings/index.ts create mode 100644 hax/artifacts/findings/types.ts diff --git a/cli/src/registry/default/artifacts.ts b/cli/src/registry/default/artifacts.ts index c5fa5ff..da3c0c6 100644 --- a/cli/src/registry/default/artifacts.ts +++ b/cli/src/registry/default/artifacts.ts @@ -352,4 +352,43 @@ export const artifacts: RegistryItem[] = [ }, ], }, + { + name: "findings", + type: "registry:artifacts", + dependencies: [ + "react", + "clsx", + "tailwind-merge", + "zod", + "@copilotkit/react-core", + ], + registryDependencies: [], + files: [ + { + path: "hax/artifacts/findings/findings.tsx", + type: "registry:component", + content: readComponentFile("hax/artifacts/findings/findings.tsx"), + }, + { + path: "hax/artifacts/findings/action.ts", + type: "registry:hook", + content: readComponentFile("hax/artifacts/findings/action.ts"), + }, + { + path: "hax/artifacts/findings/types.ts", + type: "registry:types", + content: readComponentFile("hax/artifacts/findings/types.ts"), + }, + { + path: "hax/artifacts/findings/index.ts", + type: "registry:index", + content: readComponentFile("hax/artifacts/findings/index.ts"), + }, + { + path: "hax/artifacts/findings/description.ts", + type: "registry:description", + content: readComponentFile("hax/artifacts/findings/description.ts"), + }, + ], + }, ] diff --git a/cli/src/registry/github-registry/artifacts.json b/cli/src/registry/github-registry/artifacts.json index f3362df..cd84661 100644 --- a/cli/src/registry/github-registry/artifacts.json +++ b/cli/src/registry/github-registry/artifacts.json @@ -154,5 +154,23 @@ { "name": "index.ts", "type": "registry:index" }, { "name": "description.ts", "type": "registry:description" } ] + }, + "findings": { + "type": "registry:artifacts", + "dependencies": [ + "react", + "clsx", + "tailwind-merge", + "zod", + "@copilotkit/react-core" + ], + "registryDependencies": [], + "files": [ + { "name": "findings.tsx", "type": "registry:component" }, + { "name": "action.ts", "type": "registry:hook" }, + { "name": "types.ts", "type": "registry:types" }, + { "name": "index.ts", "type": "registry:index" }, + { "name": "description.ts", "type": "registry:description" } + ] } } diff --git a/hax/artifacts/findings/README.md b/hax/artifacts/findings/README.md new file mode 100644 index 0000000..f8aa5d6 --- /dev/null +++ b/hax/artifacts/findings/README.md @@ -0,0 +1,274 @@ +# Findings Artifact + +A HAX SDK component for displaying key insights, recommendations, or discoveries with their supporting sources. + +## Installation + +### Via HAX CLI (Recommended) + +First, ensure your project is initialized with HAX: + +```bash +hax init +``` + +Then add the findings artifact: + +```bash +hax add artifact findings +``` + +This will automatically: +- Install the component files to your configured artifacts path +- Install required dependencies +- Update your `hax.config.json` + +### Dependencies + +The following dependencies will be installed automatically: + +```bash +npm install react clsx tailwind-merge zod @copilotkit/react-core +``` + +## Usage + +### Basic Component Usage + +```tsx +import { FindingsPanel, Finding } from "@/artifacts/findings"; + +const findings: Finding[] = [ + { + id: "1", + title: "Security Vulnerability Detected", + description: "SQL injection vulnerability found in user input handling", + sources: [ + { label: "OWASP", href: "https://owasp.org" }, + { label: "CVE-2024-1234" }, + ], + }, + { + id: "2", + title: "Performance Optimization", + description: "Database queries can be optimized with proper indexing", + sources: [{ label: "Analysis Report" }], + }, +]; + +function App() { + return ( + + ); +} +``` + +### With CopilotKit Action + +```tsx +import { useFindingsAction } from "@/artifacts/findings"; + +function MyComponent() { + const addOrUpdateArtifact = (type, data) => { + // Handle artifact creation/update + console.log("Artifact:", type, data); + }; + + // Register the action with CopilotKit + useFindingsAction({ addOrUpdateArtifact }); + + return
Your component
; +} +``` + +### HAX Wrapper Component + +```tsx +import { HAXFindings, Finding } from "@/artifacts/findings"; + +const findings: Finding[] = [ + { + id: "1", + title: "Finding Title", + description: "Finding description", + sources: [{ label: "Source 1" }], + }, +]; + +function App() { + return ( + + ); +} +``` + +## Components + +### FindingsPanel + +Main container component for displaying multiple findings. + +| Prop | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `title` | `string` | Yes | - | Header title for the panel | +| `findings` | `Finding[]` | Yes | - | Array of findings to display | +| `sourcesLabel` | `string` | No | `"Sources:"` | Custom label for sources section | +| `maxVisibleSources` | `number` | No | `2` | Max source chips before showing "+N" | +| `className` | `string` | No | - | Additional CSS classes | + +### FindingsCard + +Individual finding card component. + +| Prop | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `title` | `string` | Yes | - | Title text displayed in bold | +| `description` | `string` | Yes | - | Description text below the title | +| `sources` | `string[]` | No | `[]` | Array of source labels | +| `showSources` | `boolean` | No | `true` | Whether to show sources section | +| `sourcesLabel` | `string` | No | `"Sources:"` | Custom sources label | +| `maxVisibleSources` | `number` | No | - | Max chips before overflow | + +### SourceChips + +Displays a row of source chips with overflow handling. + +| Prop | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `sources` | `string[]` | Yes | - | Array of source labels | +| `label` | `string` | No | `"Sources:"` | Label shown before chips | +| `maxVisible` | `number` | No | - | Max chips before "+N" | +| `maxChipWidth` | `number \| string` | No | - | Max width for individual chips | + +### SourceChip + +Individual source chip component. + +| Prop | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `label` | `string` | Yes | - | Text label to display | +| `maxWidth` | `number \| string` | No | - | Max width (truncates with ellipsis) | +| `isCountChip` | `boolean` | No | `false` | Whether this is a "+N" count chip | +| `truncate` | `boolean` | No | `false` | Allow chip to shrink and truncate | + +## Schema + +### Finding Type + +```typescript +interface Finding { + id: string; // Unique identifier + title: string; // Title of the finding + description: string; // Description/recommendation + sources?: Array<{ // Optional source references + label: string; // Display label + href?: string; // Optional link URL + }>; +} +``` + +### Zod Schema + +```typescript +import { FindingsArtifactZod } from "@/artifacts/findings"; + +// Schema structure: +const FindingsArtifactZod = z.object({ + id: z.string(), + type: z.literal("findings"), + data: z.object({ + title: z.string(), + findings: z.array(z.object({ + id: z.string(), + title: z.string(), + description: z.string(), + sources: z.array(z.object({ + label: z.string(), + href: z.string().optional(), + })).optional(), + })), + sourcesLabel: z.string().optional(), + maxVisibleSources: z.number().optional(), + }), +}); +``` + +## CopilotKit Action + +The `useFindingsAction` hook registers a `create_findings` action with CopilotKit. + +### Action Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `title` | `string` | Yes | Header title for the findings panel | +| `findingsJson` | `string` | Yes | JSON string of findings array | +| `sourcesLabel` | `string` | No | Custom label for sources (default: "Sources:") | +| `maxVisibleSources` | `number` | No | Max source chips before "+N" (default: 2) | + +### findingsJson Format + +```json +[ + { + "id": "unique-id-1", + "title": "Finding Title", + "description": "Detailed description of the finding", + "sources": [ + { "label": "Source Name", "href": "https://example.com" }, + { "label": "Another Source" } + ] + } +] +``` + +## Best Practices + +- Keep finding titles concise and actionable (under 10 words) +- Provide clear, specific descriptions that explain the finding's significance +- Include relevant sources to build credibility and enable follow-up +- Limit to 3-7 findings per panel to maintain readability +- Use consistent source labeling conventions + +## When to Use + +Use findings artifacts for: +- Research summaries +- Analysis results +- Audit findings +- Security assessments +- Any situation where users need to see multiple findings with source attribution + +Avoid using findings for: +- Simple lists without context +- Single-item displays +- Overly long descriptions (break into separate findings instead) + +## Exports + +```typescript +// Components +export { HAXFindings, FindingsPanel, FindingsCard, SourceChips, SourceChip } + +// Types +export type { Finding, FindingsPanelProps, FindingsCardProps, SourceChipsProps, SourceChipProps } + +// Action Hook +export { useFindingsAction } + +// Schema +export { FindingsArtifactZod } +export type { FindingsArtifact } +``` + +## License + +Apache License 2.0 - Copyright 2025 Cisco Systems, Inc. and its affiliates diff --git a/hax/artifacts/findings/action.ts b/hax/artifacts/findings/action.ts new file mode 100644 index 0000000..2f7b156 --- /dev/null +++ b/hax/artifacts/findings/action.ts @@ -0,0 +1,86 @@ +/* + * 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 { ArtifactTab } from "./types" +import { FINDINGS_DESCRIPTION } from "./description" + +interface UseFindingsActionProps { + addOrUpdateArtifact: ( + type: "findings", + data: Extract["data"], + ) => void +} + +export const useFindingsAction = ({ + addOrUpdateArtifact, +}: UseFindingsActionProps) => { + useCopilotAction({ + name: "create_findings", + description: FINDINGS_DESCRIPTION, + parameters: [ + { + name: "title", + type: "string", + description: "Header title for the findings panel", + required: true, + }, + { + name: "findingsJson", + type: "string", + description: + "JSON string of findings array. Each finding must have: id (unique string), title (string), description (string), and optionally sources (array of {label: string, href?: string})", + required: true, + }, + { + name: "sourcesLabel", + type: "string", + description: "Custom label for sources section (default: 'Sources:')", + required: false, + }, + { + name: "maxVisibleSources", + type: "number", + description: + "Maximum number of source chips to show before collapsing into '+N' (default: 2)", + required: false, + }, + ], + handler: async (args) => { + try { + const { title, findingsJson, sourcesLabel, maxVisibleSources } = args + + const findings = JSON.parse(findingsJson) + + addOrUpdateArtifact("findings", { + title, + findings, + sourcesLabel, + maxVisibleSources, + }) + + return `Created findings panel "${title}" with ${findings.length} findings` + } catch (error) { + console.error("Error in create_findings handler:", error) + const errorMessage = + error instanceof Error ? error.message : "Unknown error" + return `Failed to create findings: ${errorMessage}` + } + }, + }) +} diff --git a/hax/artifacts/findings/description.ts b/hax/artifacts/findings/description.ts new file mode 100644 index 0000000..8e3efd1 --- /dev/null +++ b/hax/artifacts/findings/description.ts @@ -0,0 +1,37 @@ +/* + * 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 FINDINGS_DESCRIPTION = + `Use findings artifacts to present a list of key insights, recommendations, or discoveries with their supporting sources. Best for research summaries, analysis results, audit findings, security assessments, and any situation where users need to see multiple findings with source attribution. + +Structure each finding with a clear title, descriptive explanation, and optional source references. The panel displays findings in a clean, scannable format with source chips for quick reference. + +Features: +- Panel header with customizable title +- Individual finding cards with title and description +- Source chips with overflow handling (shows "+N" when exceeding maxVisibleSources) +- Clean, professional styling following design system guidelines + +Best practices: +- Keep finding titles concise and actionable (under 10 words) +- Provide clear, specific descriptions that explain the finding's significance +- Include relevant sources to build credibility and enable follow-up +- Limit to 3-7 findings per panel to maintain readability +- Use consistent source labeling conventions + +Don't use findings for simple lists without context or single-item displays. Avoid overly long descriptions that should be broken into separate findings.` as const diff --git a/hax/artifacts/findings/findings.tsx b/hax/artifacts/findings/findings.tsx new file mode 100644 index 0000000..51ee0ab --- /dev/null +++ b/hax/artifacts/findings/findings.tsx @@ -0,0 +1,370 @@ +/* + * 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" + +// ============================================================================ +// SourceChip Component +// ============================================================================ + +export interface SourceChipProps extends React.HTMLAttributes { + /** The text label to display in the chip */ + label: string + /** Maximum width for the chip - text will truncate with ellipsis if exceeded */ + maxWidth?: number | string + /** Whether this is a count chip (e.g., "+3") - uses slightly different styling */ + isCountChip?: boolean + /** Whether to allow the chip to shrink and truncate text when container is constrained */ + truncate?: boolean +} + +export function SourceChip({ + label, + maxWidth, + isCountChip = false, + truncate = false, + className, + style, + ...props +}: SourceChipProps) { + const chipStyle: React.CSSProperties = { + ...style, + ...(maxWidth + ? { maxWidth: typeof maxWidth === "number" ? `${maxWidth}px` : maxWidth } + : {}), + } + + const shouldTruncate = truncate || !!maxWidth + + return ( +
+ + {label} + +
+ ) +} + +// ============================================================================ +// SourceChips Component +// ============================================================================ + +export interface SourceChipsProps extends React.HTMLAttributes { + /** Array of source labels to display as chips */ + sources: string[] + /** Label shown before the chips (default: "Sources:") */ + label?: string + /** Maximum number of chips to show before collapsing into "+N" */ + maxVisible?: number + /** Maximum width for individual chips (truncates with ellipsis) */ + maxChipWidth?: number | string +} + +export function SourceChips({ + sources, + label = "Sources:", + maxVisible, + maxChipWidth, + className, + ...props +}: SourceChipsProps) { + if (!sources || sources.length === 0) { + return null + } + + const totalChips = sources.length + const effectiveMaxVisible = + maxVisible !== undefined + ? Math.max(totalChips >= 2 ? 2 : 1, maxVisible) + : totalChips + + const visibleSources = sources.slice(0, effectiveMaxVisible) + const overflowCount = totalChips - effectiveMaxVisible + const hasOverflow = overflowCount > 0 + + return ( +
+

+ {label} +

+ +
+ {visibleSources.map((source, index) => ( + + ))} + + {hasOverflow && ( + 1 ? "s" : ""}`} + /> + )} +
+
+ ) +} + +// ============================================================================ +// FindingsCard Component +// ============================================================================ + +export interface FindingsCardProps + extends React.HTMLAttributes { + /** Title text displayed in bold */ + title: string + /** Description text displayed below the title */ + description: string + /** Array of source labels to display as chips */ + sources?: string[] + /** Whether to show the sources section */ + showSources?: boolean + /** Custom sources label */ + sourcesLabel?: string + /** Maximum number of chips to show before collapsing into "+N" */ + maxVisibleSources?: number +} + +export function FindingsCard({ + title, + description, + sources = [], + showSources = true, + sourcesLabel, + maxVisibleSources, + className, + ...props +}: FindingsCardProps) { + return ( +
+
+
+

+ {title} +

+ +

+ {description} +

+
+
+ + {showSources && sources.length > 0 && ( + + )} +
+ ) +} + +// ============================================================================ +// Finding Type +// ============================================================================ + +export interface Finding { + /** Unique identifier for the finding */ + id: string + /** Title of the finding */ + title: string + /** Description/recommendation about the finding */ + description: string + /** Array of source labels with optional links */ + sources?: Array<{ + label: string + href?: string + }> +} + +// ============================================================================ +// FindingsPanel Component +// ============================================================================ + +export interface FindingsPanelProps + extends React.HTMLAttributes { + /** Header title for the panel */ + title: string + /** Array of findings to display */ + findings: Finding[] + /** Custom sources label (default: "Sources:") */ + sourcesLabel?: string + /** Maximum number of source chips to show before collapsing into "+N" */ + maxVisibleSources?: number +} + +export function FindingsPanel({ + title, + findings, + sourcesLabel, + maxVisibleSources, + className, + ...props +}: FindingsPanelProps) { + return ( +
+
+

+ {title} +

+
+ + {findings.map((finding) => { + const sourceLabels = finding.sources?.map((s) => s.label) || [] + return ( + 0} + maxVisibleSources={maxVisibleSources} + className="w-full max-w-none" + /> + ) + })} +
+ ) +} + +// ============================================================================ +// HAX Wrapper Component +// ============================================================================ + +interface HAXFindingsProps { + title: string + findings: Finding[] + sourcesLabel?: string + maxVisibleSources?: number +} + +export function HAXFindings({ + title, + findings, + sourcesLabel, + maxVisibleSources, +}: HAXFindingsProps) { + return ( +
+ +
+ ) +} + +export default FindingsPanel diff --git a/hax/artifacts/findings/index.ts b/hax/artifacts/findings/index.ts new file mode 100644 index 0000000..adcb3e8 --- /dev/null +++ b/hax/artifacts/findings/index.ts @@ -0,0 +1,35 @@ +/* + * 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 { + HAXFindings, + FindingsPanel, + FindingsCard, + SourceChips, + SourceChip, +} from "./findings" +export type { + Finding, + FindingsPanelProps, + FindingsCardProps, + SourceChipsProps, + SourceChipProps, +} from "./findings" +export { useFindingsAction } from "./action" +export type { FindingsArtifact } from "./types" +export { FindingsArtifactZod } from "./types" diff --git a/hax/artifacts/findings/types.ts b/hax/artifacts/findings/types.ts new file mode 100644 index 0000000..5ae467f --- /dev/null +++ b/hax/artifacts/findings/types.ts @@ -0,0 +1,47 @@ +/* + * 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" + +const FindingSourceZod = z.object({ + label: z.string(), + href: z.string().optional(), +}) + +const FindingZod = z.object({ + id: z.string(), + title: z.string(), + description: z.string(), + sources: z.array(FindingSourceZod).optional(), +}) + +export const FindingsArtifactZod = z.object({ + id: z.string(), + type: z.literal("findings"), + data: z.object({ + title: z.string(), + findings: z.array(FindingZod), + sourcesLabel: z.string().optional(), + maxVisibleSources: z.number().optional(), + }), +}) + +export type FindingsArtifact = z.infer + +export const ArtifactTabZod = z.discriminatedUnion("type", [FindingsArtifactZod]) +export type ArtifactTab = z.infer From 0f36d440afd6cae1a847f34454e66481ad3fb432 Mon Sep 17 00:00:00 2001 From: Jaswant Singh Date: Tue, 6 Jan 2026 17:08:06 +0530 Subject: [PATCH 2/4] chore: all sources pop over Signed-off-by: Jaswant Singh --- cli/src/registry/default/artifacts.ts | 231 +++++++++- .../registry/github-registry/artifacts.json | 260 +++++++++--- hax/artifacts/findings/findings.tsx | 401 +++++++++++------- hax/artifacts/findings/index.ts | 13 +- 4 files changed, 692 insertions(+), 213 deletions(-) diff --git a/cli/src/registry/default/artifacts.ts b/cli/src/registry/default/artifacts.ts index da3c0c6..7e11151 100644 --- a/cli/src/registry/default/artifacts.ts +++ b/cli/src/registry/default/artifacts.ts @@ -361,8 +361,10 @@ export const artifacts: RegistryItem[] = [ "tailwind-merge", "zod", "@copilotkit/react-core", + "lucide-react", + "@radix-ui/react-popover", ], - registryDependencies: [], + registryDependencies: ["button", "popover"], files: [ { path: "hax/artifacts/findings/findings.tsx", @@ -391,4 +393,231 @@ export const artifacts: RegistryItem[] = [ }, ], }, + { + name: "inline-rationale", + type: "registry:artifacts", + dependencies: [ + "react", + "clsx", + "tailwind-merge", + "zod", + "@copilotkit/react-core", + ], + registryDependencies: [], + files: [ + { + path: "hax/artifacts/inline-rationale/inline-rationale.tsx", + type: "registry:component", + content: readComponentFile( + "hax/artifacts/inline-rationale/inline-rationale.tsx", + ), + }, + { + path: "hax/artifacts/inline-rationale/action.ts", + type: "registry:hook", + content: readComponentFile("hax/artifacts/inline-rationale/action.ts"), + }, + { + path: "hax/artifacts/inline-rationale/types.ts", + type: "registry:types", + content: readComponentFile("hax/artifacts/inline-rationale/types.ts"), + }, + { + path: "hax/artifacts/inline-rationale/index.ts", + type: "registry:index", + content: readComponentFile("hax/artifacts/inline-rationale/index.ts"), + }, + { + path: "hax/artifacts/inline-rationale/description.ts", + type: "registry:description", + content: readComponentFile( + "hax/artifacts/inline-rationale/description.ts", + ), + }, + ], + }, + { + name: "capability-manifest", + type: "registry:artifacts", + dependencies: [ + "react", + "clsx", + "tailwind-merge", + "zod", + "@copilotkit/react-core", + "lucide-react", + ], + registryDependencies: [], + files: [ + { + path: "hax/artifacts/capability-manifest/capability-manifest.tsx", + type: "registry:component", + content: readComponentFile( + "hax/artifacts/capability-manifest/capability-manifest.tsx", + ), + }, + { + path: "hax/artifacts/capability-manifest/action.ts", + type: "registry:hook", + content: readComponentFile( + "hax/artifacts/capability-manifest/action.ts", + ), + }, + { + path: "hax/artifacts/capability-manifest/types.ts", + type: "registry:types", + content: readComponentFile( + "hax/artifacts/capability-manifest/types.ts", + ), + }, + { + path: "hax/artifacts/capability-manifest/index.ts", + type: "registry:index", + content: readComponentFile( + "hax/artifacts/capability-manifest/index.ts", + ), + }, + { + path: "hax/artifacts/capability-manifest/description.ts", + type: "registry:description", + content: readComponentFile( + "hax/artifacts/capability-manifest/description.ts", + ), + }, + ], + }, + { + name: "co-editing", + type: "registry:artifacts", + dependencies: [ + "react", + "clsx", + "tailwind-merge", + "zod", + "@copilotkit/react-core", + "lucide-react", + ], + registryDependencies: ["button"], + files: [ + { + path: "hax/artifacts/co-editing/co-editing.tsx", + type: "registry:component", + content: readComponentFile("hax/artifacts/co-editing/co-editing.tsx"), + }, + { + path: "hax/artifacts/co-editing/action.ts", + type: "registry:hook", + content: readComponentFile("hax/artifacts/co-editing/action.ts"), + }, + { + path: "hax/artifacts/co-editing/types.ts", + type: "registry:types", + content: readComponentFile("hax/artifacts/co-editing/types.ts"), + }, + { + path: "hax/artifacts/co-editing/index.ts", + type: "registry:index", + content: readComponentFile("hax/artifacts/co-editing/index.ts"), + }, + { + path: "hax/artifacts/co-editing/description.ts", + type: "registry:description", + content: readComponentFile("hax/artifacts/co-editing/description.ts"), + }, + ], + }, + { + name: "event-workshop-card", + type: "registry:artifacts", + dependencies: [ + "react", + "clsx", + "tailwind-merge", + "zod", + "@copilotkit/react-core", + "lucide-react", + ], + registryDependencies: ["avatar", "badge"], + files: [ + { + path: "hax/artifacts/event-workshop-card/event-workshop-card.tsx", + type: "registry:component", + content: readComponentFile( + "hax/artifacts/event-workshop-card/event-workshop-card.tsx", + ), + }, + { + path: "hax/artifacts/event-workshop-card/action.ts", + type: "registry:hook", + content: readComponentFile( + "hax/artifacts/event-workshop-card/action.ts", + ), + }, + { + path: "hax/artifacts/event-workshop-card/types.ts", + type: "registry:types", + content: readComponentFile( + "hax/artifacts/event-workshop-card/types.ts", + ), + }, + { + path: "hax/artifacts/event-workshop-card/index.ts", + type: "registry:index", + content: readComponentFile( + "hax/artifacts/event-workshop-card/index.ts", + ), + }, + { + path: "hax/artifacts/event-workshop-card/description.ts", + type: "registry:description", + content: readComponentFile( + "hax/artifacts/event-workshop-card/description.ts", + ), + }, + ], + }, + { + name: "routing-changes", + type: "registry:artifacts", + dependencies: [ + "react", + "clsx", + "tailwind-merge", + "zod", + "@copilotkit/react-core", + "lucide-react", + ], + registryDependencies: ["button"], + files: [ + { + path: "hax/artifacts/routing-changes/routing-changes.tsx", + type: "registry:component", + content: readComponentFile( + "hax/artifacts/routing-changes/routing-changes.tsx", + ), + }, + { + path: "hax/artifacts/routing-changes/action.ts", + type: "registry:hook", + content: readComponentFile("hax/artifacts/routing-changes/action.ts"), + }, + { + path: "hax/artifacts/routing-changes/types.ts", + type: "registry:types", + content: readComponentFile("hax/artifacts/routing-changes/types.ts"), + }, + { + path: "hax/artifacts/routing-changes/index.ts", + type: "registry:index", + content: readComponentFile("hax/artifacts/routing-changes/index.ts"), + }, + { + path: "hax/artifacts/routing-changes/description.ts", + type: "registry:description", + content: readComponentFile( + "hax/artifacts/routing-changes/description.ts", + ), + }, + ], + }, ] diff --git a/cli/src/registry/github-registry/artifacts.json b/cli/src/registry/github-registry/artifacts.json index cd84661..a1793d7 100644 --- a/cli/src/registry/github-registry/artifacts.json +++ b/cli/src/registry/github-registry/artifacts.json @@ -8,13 +8,32 @@ "zod", "@copilotkit/react-core" ], - "registryDependencies": ["button", "input", "select"], + "registryDependencies": [ + "button", + "input", + "select" + ], "files": [ - { "name": "form.tsx", "type": "registry:component" }, - { "name": "action.ts", "type": "registry:hook" }, - { "name": "types.ts", "type": "registry:types" }, - { "name": "index.ts", "type": "registry:index" }, - { "name": "description.ts", "type": "registry:description" } + { + "name": "form.tsx", + "type": "registry:component" + }, + { + "name": "action.ts", + "type": "registry:hook" + }, + { + "name": "types.ts", + "type": "registry:types" + }, + { + "name": "index.ts", + "type": "registry:index" + }, + { + "name": "description.ts", + "type": "registry:description" + } ] }, "timeline": { @@ -30,11 +49,26 @@ ], "registryDependencies": [], "files": [ - { "name": "timeline.tsx", "type": "registry:component" }, - { "name": "action.ts", "type": "registry:hook" }, - { "name": "types.ts", "type": "registry:types" }, - { "name": "index.ts", "type": "registry:index" }, - { "name": "description.ts", "type": "registry:description" } + { + "name": "timeline.tsx", + "type": "registry:component" + }, + { + "name": "action.ts", + "type": "registry:hook" + }, + { + "name": "types.ts", + "type": "registry:types" + }, + { + "name": "index.ts", + "type": "registry:index" + }, + { + "name": "description.ts", + "type": "registry:description" + } ] }, "mindmap": { @@ -50,11 +84,26 @@ ], "registryDependencies": [], "files": [ - { "name": "mindmap.tsx", "type": "registry:component" }, - { "name": "action.tsx", "type": "registry:hook" }, - { "name": "types.ts", "type": "registry:types" }, - { "name": "index.ts", "type": "registry:index" }, - { "name": "description.ts", "type": "registry:description" } + { + "name": "mindmap.tsx", + "type": "registry:component" + }, + { + "name": "action.tsx", + "type": "registry:hook" + }, + { + "name": "types.ts", + "type": "registry:types" + }, + { + "name": "index.ts", + "type": "registry:index" + }, + { + "name": "description.ts", + "type": "registry:description" + } ] }, "code-editor": { @@ -68,13 +117,31 @@ "@monaco-editor/react", "monaco-editor" ], - "registryDependencies": ["select", "generated-ui-wrapper"], + "registryDependencies": [ + "select", + "generated-ui-wrapper" + ], "files": [ - { "name": "code-editor.tsx", "type": "registry:component" }, - { "name": "action.ts", "type": "registry:hook" }, - { "name": "types.ts", "type": "registry:types" }, - { "name": "index.ts", "type": "registry:index" }, - { "name": "description.ts", "type": "registry:description" } + { + "name": "code-editor.tsx", + "type": "registry:component" + }, + { + "name": "action.ts", + "type": "registry:hook" + }, + { + "name": "types.ts", + "type": "registry:types" + }, + { + "name": "index.ts", + "type": "registry:index" + }, + { + "name": "description.ts", + "type": "registry:description" + } ] }, "details": { @@ -87,13 +154,31 @@ "@copilotkit/react-core", "lucide-react" ], - "registryDependencies": ["table", "card"], + "registryDependencies": [ + "table", + "card" + ], "files": [ - { "name": "details.tsx", "type": "registry:component" }, - { "name": "action.ts", "type": "registry:hook" }, - { "name": "types.ts", "type": "registry:types" }, - { "name": "index.ts", "type": "registry:index" }, - { "name": "description.ts", "type": "registry:description" } + { + "name": "details.tsx", + "type": "registry:component" + }, + { + "name": "action.ts", + "type": "registry:hook" + }, + { + "name": "types.ts", + "type": "registry:types" + }, + { + "name": "index.ts", + "type": "registry:index" + }, + { + "name": "description.ts", + "type": "registry:description" + } ] }, "data-visualizer": { @@ -109,11 +194,26 @@ ], "registryDependencies": [], "files": [ - { "name": "data-visualizer.tsx", "type": "registry:component" }, - { "name": "action.ts", "type": "registry:hook" }, - { "name": "types.ts", "type": "registry:types" }, - { "name": "index.ts", "type": "registry:index" }, - { "name": "description.ts", "type": "registry:description" } + { + "name": "data-visualizer.tsx", + "type": "registry:component" + }, + { + "name": "action.ts", + "type": "registry:hook" + }, + { + "name": "types.ts", + "type": "registry:types" + }, + { + "name": "index.ts", + "type": "registry:index" + }, + { + "name": "description.ts", + "type": "registry:description" + } ] }, "source-attribution": { @@ -126,13 +226,30 @@ "@copilotkit/react-core", "lucide-react" ], - "registryDependencies": ["badge"], + "registryDependencies": [ + "badge" + ], "files": [ - { "name": "source-attribution.tsx", "type": "registry:component" }, - { "name": "action.ts", "type": "registry:hook" }, - { "name": "types.ts", "type": "registry:types" }, - { "name": "index.ts", "type": "registry:index" }, - { "name": "description.ts", "type": "registry:description" } + { + "name": "source-attribution.tsx", + "type": "registry:component" + }, + { + "name": "action.ts", + "type": "registry:hook" + }, + { + "name": "types.ts", + "type": "registry:types" + }, + { + "name": "index.ts", + "type": "registry:index" + }, + { + "name": "description.ts", + "type": "registry:description" + } ] }, "rationale": { @@ -146,13 +263,32 @@ "lucide-react", "class-variance-authority" ], - "registryDependencies": ["badge", "button", "progress"], + "registryDependencies": [ + "badge", + "button", + "progress" + ], "files": [ - { "name": "rationale.tsx", "type": "registry:component" }, - { "name": "action.ts", "type": "registry:hook" }, - { "name": "types.ts", "type": "registry:types" }, - { "name": "index.ts", "type": "registry:index" }, - { "name": "description.ts", "type": "registry:description" } + { + "name": "rationale.tsx", + "type": "registry:component" + }, + { + "name": "action.ts", + "type": "registry:hook" + }, + { + "name": "types.ts", + "type": "registry:types" + }, + { + "name": "index.ts", + "type": "registry:index" + }, + { + "name": "description.ts", + "type": "registry:description" + } ] }, "findings": { @@ -162,15 +298,35 @@ "clsx", "tailwind-merge", "zod", - "@copilotkit/react-core" + "@copilotkit/react-core", + "lucide-react", + "@radix-ui/react-popover" + ], + "registryDependencies": [ + "button", + "popover" ], - "registryDependencies": [], "files": [ - { "name": "findings.tsx", "type": "registry:component" }, - { "name": "action.ts", "type": "registry:hook" }, - { "name": "types.ts", "type": "registry:types" }, - { "name": "index.ts", "type": "registry:index" }, - { "name": "description.ts", "type": "registry:description" } + { + "name": "findings.tsx", + "type": "registry:component" + }, + { + "name": "action.ts", + "type": "registry:hook" + }, + { + "name": "types.ts", + "type": "registry:types" + }, + { + "name": "index.ts", + "type": "registry:index" + }, + { + "name": "description.ts", + "type": "registry:description" + } ] } } diff --git a/hax/artifacts/findings/findings.tsx b/hax/artifacts/findings/findings.tsx index 51ee0ab..e1d39de 100644 --- a/hax/artifacts/findings/findings.tsx +++ b/hax/artifacts/findings/findings.tsx @@ -16,25 +16,49 @@ * SPDX-License-Identifier: Apache-2.0 */ -"use client" +"use client"; -import * as React from "react" +import * as React from "react"; +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; -import { cn } from "@/lib/utils" +// ============================================================================ +// Types +// ============================================================================ + +export interface Finding { + id: string; + title: string; + description: string; + sources?: Array<{ + label: string; + href?: string; + }>; +} + +export interface FindingsPanelProps + extends React.HTMLAttributes { + title: string; + findings: Finding[]; + sourcesLabel?: string; + maxVisibleSources?: number; + onSourceClick?: (source: string, index: number, findingId: string) => void; +} // ============================================================================ // SourceChip Component // ============================================================================ export interface SourceChipProps extends React.HTMLAttributes { - /** The text label to display in the chip */ - label: string - /** Maximum width for the chip - text will truncate with ellipsis if exceeded */ - maxWidth?: number | string - /** Whether this is a count chip (e.g., "+3") - uses slightly different styling */ - isCountChip?: boolean - /** Whether to allow the chip to shrink and truncate text when container is constrained */ - truncate?: boolean + label: string; + maxWidth?: number | string; + isCountChip?: boolean; + truncate?: boolean; } export function SourceChip({ @@ -51,9 +75,9 @@ export function SourceChip({ ...(maxWidth ? { maxWidth: typeof maxWidth === "number" ? `${maxWidth}px` : maxWidth } : {}), - } + }; - const shouldTruncate = truncate || !!maxWidth + const shouldTruncate = truncate || !!maxWidth; return (
{label}
- ) + ); +} + +// ============================================================================ +// AllSourcesPopover Component +// ============================================================================ + +export interface AllSourcesPopoverProps { + sources: string[]; + children: React.ReactNode; + onSourceClick?: (source: string, index: number) => void; + open?: boolean; + onOpenChange?: (open: boolean) => void; +} + +export function AllSourcesPopover({ + sources, + children, + onSourceClick, + open, + onOpenChange, +}: AllSourcesPopoverProps) { + const [internalOpen, setInternalOpen] = React.useState(false); + + const isOpen = open !== undefined ? open : internalOpen; + const setIsOpen = onOpenChange || setInternalOpen; + + const handleClose = () => { + setIsOpen(false); + }; + + return ( + + {children} + +
+ {/* Dialog Header */} +
+
+

+ All Sources +

+
+
+ + {/* Vertical Chips Container */} +
+ {sources.map((source, index) => ( + onSourceClick?.(source, index)} + /> + ))} +
+ + {/* Dialog Footer */} +
+
+ +
+
+
+
+
+ ); } // ============================================================================ @@ -91,14 +191,11 @@ export function SourceChip({ // ============================================================================ export interface SourceChipsProps extends React.HTMLAttributes { - /** Array of source labels to display as chips */ - sources: string[] - /** Label shown before the chips (default: "Sources:") */ - label?: string - /** Maximum number of chips to show before collapsing into "+N" */ - maxVisible?: number - /** Maximum width for individual chips (truncates with ellipsis) */ - maxChipWidth?: number | string + sources: string[]; + label?: string; + maxVisible?: number; + maxChipWidth?: number | string; + onSourceClick?: (source: string, index: number) => void; } export function SourceChips({ @@ -106,46 +203,36 @@ export function SourceChips({ label = "Sources:", maxVisible, maxChipWidth, + onSourceClick, className, ...props }: SourceChipsProps) { if (!sources || sources.length === 0) { - return null + return null; } - const totalChips = sources.length + const totalChips = sources.length; const effectiveMaxVisible = maxVisible !== undefined ? Math.max(totalChips >= 2 ? 2 : 1, maxVisible) - : totalChips + : totalChips; - const visibleSources = sources.slice(0, effectiveMaxVisible) - const overflowCount = totalChips - effectiveMaxVisible - const hasOverflow = overflowCount > 0 + const visibleSources = sources.slice(0, effectiveMaxVisible); + const overflowCount = totalChips - effectiveMaxVisible; + const hasOverflow = overflowCount > 0; return (
-

+ {/* Label */} +

{label}

-
+ {/* Chips container */} +
{visibleSources.map((source, index) => ( ))} + {/* Overflow indicator chip */} {hasOverflow && ( - 1 ? "s" : ""}`} - /> + + + )}
- ) + ); } // ============================================================================ // FindingsCard Component // ============================================================================ -export interface FindingsCardProps - extends React.HTMLAttributes { - /** Title text displayed in bold */ - title: string - /** Description text displayed below the title */ - description: string - /** Array of source labels to display as chips */ - sources?: string[] - /** Whether to show the sources section */ - showSources?: boolean - /** Custom sources label */ - sourcesLabel?: string - /** Maximum number of chips to show before collapsing into "+N" */ - maxVisibleSources?: number +export interface FindingsCardProps extends React.HTMLAttributes { + title: string; + description: string; + sources?: string[]; + showSources?: boolean; + sourcesLabel?: string; + maxVisibleSources?: number; + onSourceClick?: (source: string, index: number) => void; } export function FindingsCard({ @@ -194,6 +285,7 @@ export function FindingsCard({ showSources = true, sourcesLabel, maxVisibleSources, + onSourceClick, className, ...props }: FindingsCardProps) { @@ -205,84 +297,85 @@ export function FindingsCard({ "bg-white border border-solid border-[#e2e8f0]", "rounded-lg", "w-full max-w-[400px] overflow-hidden", - className, + className )} {...props} > -
-
-

+ {/* Content section */} +

+ {/* Text content */} +
+ {/* Title */} +

{title}

-

+ {/* Description */} +

{description}

+ {/* Sources section */} {showSources && sources.length > 0 && ( )}
- ) + ); } // ============================================================================ -// Finding Type +// FindingsPanel Component (Main Export) // ============================================================================ -export interface Finding { - /** Unique identifier for the finding */ - id: string - /** Title of the finding */ - title: string - /** Description/recommendation about the finding */ - description: string - /** Array of source labels with optional links */ +interface FindingsPanelItemProps { + findingId: string; + title: string; + description: string; sources?: Array<{ - label: string - href?: string - }> + label: string; + href?: string; + }>; + sourcesLabel?: string; + maxVisibleSources?: number; + onSourceClick?: (source: string, index: number, findingId: string) => void; } -// ============================================================================ -// FindingsPanel Component -// ============================================================================ +function FindingsPanelItem({ + findingId, + title, + description, + sources, + sourcesLabel, + maxVisibleSources, + onSourceClick, +}: FindingsPanelItemProps) { + const sourceLabels = sources?.map((s) => s.label) || []; -export interface FindingsPanelProps - extends React.HTMLAttributes { - /** Header title for the panel */ - title: string - /** Array of findings to display */ - findings: Finding[] - /** Custom sources label (default: "Sources:") */ - sourcesLabel?: string - /** Maximum number of source chips to show before collapsing into "+N" */ - maxVisibleSources?: number + const handleSourceClick = onSourceClick + ? (source: string, index: number) => onSourceClick(source, index, findingId) + : undefined; + + return ( + 0} + maxVisibleSources={maxVisibleSources} + onSourceClick={handleSourceClick} + className="w-full max-w-none" + /> + ); } export function FindingsPanel({ @@ -290,6 +383,7 @@ export function FindingsPanel({ findings, sourcesLabel, maxVisibleSources, + onSourceClick, className, ...props }: FindingsPanelProps) { @@ -300,71 +394,68 @@ export function FindingsPanel({ "p-6 gap-2", "bg-white border border-solid border-[#e2e8f0]", "rounded-lg", - "shadow-[0px_1px_3px_0px_rgba(0,0,0,0.1),0px_1px_2px_-1px_rgba(0,0,0,0.1)]", + "shadow-sm", "w-full", - className, + className )} {...props} > -
-

+ {/* Panel Header */} +

+

{title}

- {findings.map((finding) => { - const sourceLabels = finding.sources?.map((s) => s.label) || [] - return ( - 0} - maxVisibleSources={maxVisibleSources} - className="w-full max-w-none" - /> - ) - })} + {/* Findings List */} + {findings.map((finding) => ( + + ))}
- ) + ); } // ============================================================================ -// HAX Wrapper Component +// HAX Wrapper (Default Export for HAX SDK) // ============================================================================ -interface HAXFindingsProps { - title: string - findings: Finding[] - sourcesLabel?: string - maxVisibleSources?: number +export interface HAXFindingsProps { + sourcesLabel?: string; + maxVisibleSources?: number; + title: string; + findings: Finding[]; + onSourceClick?: (source: string, index: number, findingId: string) => void; } export function HAXFindings({ - title, - findings, sourcesLabel, maxVisibleSources, + title, + findings, + onSourceClick, }: HAXFindingsProps) { return ( -
+
- ) + ); } -export default FindingsPanel +export default HAXFindings; diff --git a/hax/artifacts/findings/index.ts b/hax/artifacts/findings/index.ts index adcb3e8..ec230da 100644 --- a/hax/artifacts/findings/index.ts +++ b/hax/artifacts/findings/index.ts @@ -22,14 +22,17 @@ export { FindingsCard, SourceChips, SourceChip, -} from "./findings" + AllSourcesPopover, +} from "./findings"; export type { Finding, FindingsPanelProps, FindingsCardProps, SourceChipsProps, SourceChipProps, -} from "./findings" -export { useFindingsAction } from "./action" -export type { FindingsArtifact } from "./types" -export { FindingsArtifactZod } from "./types" + AllSourcesPopoverProps, + HAXFindingsProps, +} from "./findings"; +export { useFindingsAction } from "./action"; +export type { FindingsArtifact, Source } from "./types"; +export { FindingsArtifactZod, FindingZod, SourceZod } from "./types"; From b90d7b76584605ef38b426f95343f9e726c20a92 Mon Sep 17 00:00:00 2001 From: Jaswant Singh Date: Tue, 20 Jan 2026 16:47:23 +0530 Subject: [PATCH 3/4] Merge upstream/main with findings component Signed-off-by: Jaswant Singh --- cli/src/registry/default/artifacts.ts | 159 +---- cli/src/registry/default/ui.ts | 12 + .../registry/github-registry/artifacts.json | 63 ++ cli/src/registry/github-registry/ui.json | 6 + hax/artifacts/capability-manifest/README.md | 331 +++++++++++ hax/artifacts/capability-manifest/action.ts | 270 +++++++++ .../capability-manifest.tsx | 550 ++++++++++++++++++ .../capability-manifest/description.ts | 65 +++ hax/artifacts/capability-manifest/index.ts | 50 ++ hax/artifacts/capability-manifest/types.ts | 150 +++++ hax/artifacts/inline-rationale/README.md | 224 +++++++ hax/artifacts/inline-rationale/action.ts | 174 ++++++ hax/artifacts/inline-rationale/description.ts | 45 ++ hax/artifacts/inline-rationale/index.ts | 33 ++ .../inline-rationale/inline-rationale.tsx | 341 +++++++++++ hax/artifacts/inline-rationale/types.ts | 111 ++++ hax/artifacts/workshop-card/README.md | 200 +++++++ hax/artifacts/workshop-card/action.ts | 185 ++++++ hax/artifacts/workshop-card/description.ts | 32 + hax/artifacts/workshop-card/index.ts | 22 + hax/artifacts/workshop-card/types.ts | 88 +++ hax/artifacts/workshop-card/workshop-card.tsx | 402 +++++++++++++ hax/components/ui/avatar.tsx | 71 +++ package.json | 1 + 24 files changed, 3440 insertions(+), 145 deletions(-) create mode 100644 hax/artifacts/capability-manifest/README.md create mode 100644 hax/artifacts/capability-manifest/action.ts create mode 100644 hax/artifacts/capability-manifest/capability-manifest.tsx create mode 100644 hax/artifacts/capability-manifest/description.ts create mode 100644 hax/artifacts/capability-manifest/index.ts create mode 100644 hax/artifacts/capability-manifest/types.ts create mode 100644 hax/artifacts/inline-rationale/README.md create mode 100644 hax/artifacts/inline-rationale/action.ts create mode 100644 hax/artifacts/inline-rationale/description.ts create mode 100644 hax/artifacts/inline-rationale/index.ts create mode 100644 hax/artifacts/inline-rationale/inline-rationale.tsx create mode 100644 hax/artifacts/inline-rationale/types.ts create mode 100644 hax/artifacts/workshop-card/README.md create mode 100644 hax/artifacts/workshop-card/action.ts create mode 100644 hax/artifacts/workshop-card/description.ts create mode 100644 hax/artifacts/workshop-card/index.ts create mode 100644 hax/artifacts/workshop-card/types.ts create mode 100644 hax/artifacts/workshop-card/workshop-card.tsx create mode 100644 hax/components/ui/avatar.tsx diff --git a/cli/src/registry/default/artifacts.ts b/cli/src/registry/default/artifacts.ts index 7e11151..0b0c784 100644 --- a/cli/src/registry/default/artifacts.ts +++ b/cli/src/registry/default/artifacts.ts @@ -393,49 +393,6 @@ export const artifacts: RegistryItem[] = [ }, ], }, - { - name: "inline-rationale", - type: "registry:artifacts", - dependencies: [ - "react", - "clsx", - "tailwind-merge", - "zod", - "@copilotkit/react-core", - ], - registryDependencies: [], - files: [ - { - path: "hax/artifacts/inline-rationale/inline-rationale.tsx", - type: "registry:component", - content: readComponentFile( - "hax/artifacts/inline-rationale/inline-rationale.tsx", - ), - }, - { - path: "hax/artifacts/inline-rationale/action.ts", - type: "registry:hook", - content: readComponentFile("hax/artifacts/inline-rationale/action.ts"), - }, - { - path: "hax/artifacts/inline-rationale/types.ts", - type: "registry:types", - content: readComponentFile("hax/artifacts/inline-rationale/types.ts"), - }, - { - path: "hax/artifacts/inline-rationale/index.ts", - type: "registry:index", - content: readComponentFile("hax/artifacts/inline-rationale/index.ts"), - }, - { - path: "hax/artifacts/inline-rationale/description.ts", - type: "registry:description", - content: readComponentFile( - "hax/artifacts/inline-rationale/description.ts", - ), - }, - ], - }, { name: "capability-manifest", type: "registry:artifacts", @@ -487,97 +444,7 @@ export const artifacts: RegistryItem[] = [ ], }, { - name: "co-editing", - type: "registry:artifacts", - dependencies: [ - "react", - "clsx", - "tailwind-merge", - "zod", - "@copilotkit/react-core", - "lucide-react", - ], - registryDependencies: ["button"], - files: [ - { - path: "hax/artifacts/co-editing/co-editing.tsx", - type: "registry:component", - content: readComponentFile("hax/artifacts/co-editing/co-editing.tsx"), - }, - { - path: "hax/artifacts/co-editing/action.ts", - type: "registry:hook", - content: readComponentFile("hax/artifacts/co-editing/action.ts"), - }, - { - path: "hax/artifacts/co-editing/types.ts", - type: "registry:types", - content: readComponentFile("hax/artifacts/co-editing/types.ts"), - }, - { - path: "hax/artifacts/co-editing/index.ts", - type: "registry:index", - content: readComponentFile("hax/artifacts/co-editing/index.ts"), - }, - { - path: "hax/artifacts/co-editing/description.ts", - type: "registry:description", - content: readComponentFile("hax/artifacts/co-editing/description.ts"), - }, - ], - }, - { - name: "event-workshop-card", - type: "registry:artifacts", - dependencies: [ - "react", - "clsx", - "tailwind-merge", - "zod", - "@copilotkit/react-core", - "lucide-react", - ], - registryDependencies: ["avatar", "badge"], - files: [ - { - path: "hax/artifacts/event-workshop-card/event-workshop-card.tsx", - type: "registry:component", - content: readComponentFile( - "hax/artifacts/event-workshop-card/event-workshop-card.tsx", - ), - }, - { - path: "hax/artifacts/event-workshop-card/action.ts", - type: "registry:hook", - content: readComponentFile( - "hax/artifacts/event-workshop-card/action.ts", - ), - }, - { - path: "hax/artifacts/event-workshop-card/types.ts", - type: "registry:types", - content: readComponentFile( - "hax/artifacts/event-workshop-card/types.ts", - ), - }, - { - path: "hax/artifacts/event-workshop-card/index.ts", - type: "registry:index", - content: readComponentFile( - "hax/artifacts/event-workshop-card/index.ts", - ), - }, - { - path: "hax/artifacts/event-workshop-card/description.ts", - type: "registry:description", - content: readComponentFile( - "hax/artifacts/event-workshop-card/description.ts", - ), - }, - ], - }, - { - name: "routing-changes", + name: "workshop-card", type: "registry:artifacts", dependencies: [ "react", @@ -586,36 +453,38 @@ export const artifacts: RegistryItem[] = [ "zod", "@copilotkit/react-core", "lucide-react", + "class-variance-authority", + "@radix-ui/react-avatar", ], - registryDependencies: ["button"], + registryDependencies: ["button", "badge", "avatar", "generated-ui-wrapper"], files: [ { - path: "hax/artifacts/routing-changes/routing-changes.tsx", + path: "hax/artifacts/workshop-card/workshop-card.tsx", type: "registry:component", content: readComponentFile( - "hax/artifacts/routing-changes/routing-changes.tsx", + "hax/artifacts/workshop-card/workshop-card.tsx", ), }, { - path: "hax/artifacts/routing-changes/action.ts", + path: "hax/artifacts/workshop-card/action.ts", type: "registry:hook", - content: readComponentFile("hax/artifacts/routing-changes/action.ts"), + content: readComponentFile("hax/artifacts/workshop-card/action.ts"), }, { - path: "hax/artifacts/routing-changes/types.ts", + path: "hax/artifacts/workshop-card/types.ts", type: "registry:types", - content: readComponentFile("hax/artifacts/routing-changes/types.ts"), + content: readComponentFile("hax/artifacts/workshop-card/types.ts"), }, { - path: "hax/artifacts/routing-changes/index.ts", + path: "hax/artifacts/workshop-card/index.ts", type: "registry:index", - content: readComponentFile("hax/artifacts/routing-changes/index.ts"), + content: readComponentFile("hax/artifacts/workshop-card/index.ts"), }, { - path: "hax/artifacts/routing-changes/description.ts", + path: "hax/artifacts/workshop-card/description.ts", type: "registry:description", content: readComponentFile( - "hax/artifacts/routing-changes/description.ts", + "hax/artifacts/workshop-card/description.ts", ), }, ], diff --git a/cli/src/registry/default/ui.ts b/cli/src/registry/default/ui.ts index ae6ac5d..bbb37dd 100644 --- a/cli/src/registry/default/ui.ts +++ b/cli/src/registry/default/ui.ts @@ -195,4 +195,16 @@ export const uiComponents: RegistryItem[] = [ }, ], }, + { + name: "avatar", + type: "registry:ui", + dependencies: ["@radix-ui/react-avatar", "clsx", "tailwind-merge"], + files: [ + { + path: "hax/components/ui/avatar.tsx", + type: "registry:component", + content: readComponentFile("hax/components/ui/avatar.tsx"), + }, + ], + }, ] diff --git a/cli/src/registry/github-registry/artifacts.json b/cli/src/registry/github-registry/artifacts.json index a1793d7..dc7cb30 100644 --- a/cli/src/registry/github-registry/artifacts.json +++ b/cli/src/registry/github-registry/artifacts.json @@ -328,5 +328,68 @@ "type": "registry:description" } ] + }, + "inline-rationale": { + "type": "registry:artifacts", + "dependencies": [ + "react", + "clsx", + "tailwind-merge", + "zod", + "@copilotkit/react-core" + ], + "registryDependencies": [], + "files": [ + { "name": "inline-rationale.tsx", "type": "registry:component" }, + { "name": "action.ts", "type": "registry:hook" }, + { "name": "types.ts", "type": "registry:types" }, + { "name": "index.ts", "type": "registry:index" }, + { "name": "description.ts", "type": "registry:description" } + ] + }, + "capability-manifest": { + "type": "registry:artifacts", + "dependencies": [ + "react", + "clsx", + "tailwind-merge", + "zod", + "@copilotkit/react-core", + "lucide-react" + ], + "registryDependencies": [], + "files": [ + { "name": "capability-manifest.tsx", "type": "registry:component" }, + { "name": "action.ts", "type": "registry:hook" }, + { "name": "types.ts", "type": "registry:types" }, + { "name": "index.ts", "type": "registry:index" }, + { "name": "description.ts", "type": "registry:description" } + ] + }, + "workshop-card": { + "type": "registry:artifacts", + "dependencies": [ + "react", + "clsx", + "tailwind-merge", + "zod", + "@copilotkit/react-core", + "lucide-react", + "class-variance-authority", + "@radix-ui/react-avatar" + ], + "registryDependencies": [ + "button", + "badge", + "avatar", + "generated-ui-wrapper" + ], + "files": [ + { "name": "workshop-card.tsx", "type": "registry:component" }, + { "name": "action.ts", "type": "registry:hook" }, + { "name": "types.ts", "type": "registry:types" }, + { "name": "index.ts", "type": "registry:index" }, + { "name": "description.ts", "type": "registry:description" } + ] } } diff --git a/cli/src/registry/github-registry/ui.json b/cli/src/registry/github-registry/ui.json index 068299a..c36692a 100644 --- a/cli/src/registry/github-registry/ui.json +++ b/cli/src/registry/github-registry/ui.json @@ -95,5 +95,11 @@ "dependencies": ["@radix-ui/react-progress", "clsx", "tailwind-merge"], "registryDependencies": [], "files": [{ "name": "progress.tsx", "type": "registry:component" }] + }, + "avatar": { + "type": "registry:ui", + "dependencies": ["@radix-ui/react-avatar", "clsx", "tailwind-merge"], + "registryDependencies": [], + "files": [{ "name": "avatar.tsx", "type": "registry:component" }] } } diff --git a/hax/artifacts/capability-manifest/README.md b/hax/artifacts/capability-manifest/README.md new file mode 100644 index 0000000..b223573 --- /dev/null +++ b/hax/artifacts/capability-manifest/README.md @@ -0,0 +1,331 @@ +# Capability Manifest Artifact + +A component for displaying AI agent capabilities, constraints, and connection status. This artifact helps set user expectations about what an AI agent can and cannot do, preventing the "Mental Model Mismatch" where users assume the agent has capabilities it doesn't possess. + +## Installation + +Install using the HAX CLI: + +```bash +hax add artifact capability-manifest +``` + +This will: +1. Copy the component files to your `src/hax/artifacts/capability-manifest/` directory +2. Install required dependencies (`lucide-react`, `zod`) +3. Configure path aliases in your TypeScript/JavaScript config + +## Usage + +### Basic Usage + +```tsx +import { HAXCapabilityManifest } from "@/hax/artifacts/capability-manifest"; + +function AgentHandshake() { + return ( + + ); +} +``` + +### Using the CopilotKit Action Hook + +The artifact includes a CopilotKit action hook for AI agents to dynamically create capability manifests: + +```tsx +import { useCapabilityManifestAction } from "@/hax/artifacts/capability-manifest"; + +function MyApp() { + const addOrUpdateArtifact = (type, data) => { + // Handle artifact creation/update + console.log("Creating artifact:", type, data); + }; + + useCapabilityManifestAction({ addOrUpdateArtifact }); + + return
Your app content
; +} +``` + +## Schema + +### CapabilityManifestData + +The main data structure for the component: + +```typescript +interface CapabilityManifestData { + // Agent Header + agentName?: string; // Name of the AI agent + agentRole?: string; // Role or type (e.g., "Agent", "AI") + statusText?: string; // Status message below agent name + agentTags?: AgentTag[]; // Tags/badges to display + + // Capabilities + capabilities?: Capability[]; // List of capabilities (flat list) + capabilityGroups?: CapabilityGroup[]; // Grouped capabilities + capabilitiesLabel?: string; // Section label (default: "Capabilities") + showCapabilities?: boolean; // Show/hide section (default: true) + + // Alerts + alerts?: Alert[]; // Array of alerts to display + + // Status + connectionStatus?: ConnectionStatus; // Connection state + connectionLabel?: string; // Custom status label + sessionId?: string; // Session identifier + statusMetadata?: Record; // Additional metadata + + // Customization + showSeparator?: boolean; // Show separator line (default: true) + showStatus?: boolean; // Show status footer (default: true) + + // Styling + variant?: "default" | "outline" | "ghost"; // Card style + size?: "sm" | "md" | "lg"; // Size variant + showShadow?: boolean; // Show card shadow (default: true) +} +``` + +### Capability + +```typescript +interface Capability { + id: string; // Unique identifier + name: string; // Display name + description?: string; // Description of the capability + status: CapabilityStatus; // "enabled" | "disabled" | "pending" | "error" + iconColor?: string; // Custom icon color + metadata?: Record; // Additional info +} +``` + +### Alert + +```typescript +interface Alert { + id: string; // Unique identifier + title: string; // Alert title + description: string; // Alert message + variant: AlertVariant; // "warning" | "error" | "info" | "success" + dismissible?: boolean; // Allow dismissal +} +``` + +### CapabilityGroup + +```typescript +interface CapabilityGroup { + id: string; // Unique identifier + label: string; // Group label/title + capabilities: Capability[]; // Capabilities in this group + collapsible?: boolean; // Allow collapse/expand + defaultCollapsed?: boolean; // Initially collapsed +} +``` + +### Status Types + +```typescript +type CapabilityStatus = "enabled" | "disabled" | "pending" | "error"; +type AlertVariant = "warning" | "error" | "info" | "success"; +type ConnectionStatus = "connected" | "connecting" | "disconnected" | "error"; +``` + +## Action Parameters + +When using the CopilotKit action hook, the AI can call `create_capability_manifest` with these parameters: + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `agentName` | string | Yes | Name of the AI agent | +| `agentRole` | string | No | Role or type of the agent | +| `statusText` | string | No | Status message displayed below name | +| `agentTagsJson` | string | No | JSON array of tags | +| `capabilitiesJson` | string | No | JSON array of capabilities | +| `capabilityGroupsJson` | string | No | JSON array of capability groups | +| `capabilitiesLabel` | string | No | Label for capabilities section | +| `showCapabilities` | boolean | No | Show/hide capabilities | +| `alertsJson` | string | No | JSON array of alerts | +| `connectionStatus` | string | No | Connection status | +| `connectionLabel` | string | No | Custom status label | +| `sessionId` | string | No | Session identifier | +| `statusMetadataJson` | string | No | JSON object of status metadata | +| `showSeparator` | boolean | No | Show separator line | +| `showStatus` | boolean | No | Show status footer | +| `variant` | string | No | Card style variant | +| `size` | string | No | Size variant | +| `showShadow` | boolean | No | Show card shadow | + +## Examples + +### With Capability Groups + +```tsx + +``` + +### With Multiple Alerts + +```tsx + +``` + +### Different Styling Variants + +```tsx +// Outline variant + + +// Ghost variant (transparent) + + +// Small size + +``` + +### With Event Handlers + +```tsx + console.log("Clicked:", cap.name)} + onAlertDismiss={(alert) => console.log("Dismissed:", alert.title)} + onStatusClick={() => console.log("Status clicked")} +/> +``` + +## Best Practices + +1. **Display at Session Start**: Show the capability manifest during agent initialization (Phase 1) before user interaction begins. + +2. **Keep Capability Names Concise**: Use clear, descriptive names that users can quickly understand. + +3. **Use Alerts Sparingly**: Only include alerts for important constraints or warnings that affect user expectations. + +4. **Include Session ID**: Add session identifiers for debugging and tracking purposes. + +5. **Group Related Capabilities**: When there are many capabilities, use `capabilityGroups` to organize them logically. + +6. **Set Appropriate Status**: Reflect the actual agent state with the correct connection status. + +## File Structure + +``` +capability-manifest/ +├── capability-manifest.tsx # Main React component +├── action.ts # CopilotKit action hook +├── types.ts # TypeScript definitions & Zod schemas +├── description.ts # AI agent instructions +├── index.ts # Exports +└── README.md # This file +``` + +## Dependencies + +- `react` >= 18.0.0 +- `lucide-react` - Icons +- `zod` - Schema validation +- `@copilotkit/react-core` - CopilotKit integration + +## License + +Apache License 2.0 - See [LICENSE](../../../LICENSE) for details. diff --git a/hax/artifacts/capability-manifest/action.ts b/hax/artifacts/capability-manifest/action.ts new file mode 100644 index 0000000..bf7f75d --- /dev/null +++ b/hax/artifacts/capability-manifest/action.ts @@ -0,0 +1,270 @@ +/* + * 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 { + CapabilityManifestArtifact, + AgentTagZod, + CapabilityZod, + CapabilityGroupZod, + AlertZod, +} from "./types"; +import { CAPABILITY_MANIFEST_DESCRIPTION } from "./description"; + +interface UseCapabilityManifestActionProps { + addOrUpdateArtifact: ( + type: "capability-manifest", + data: CapabilityManifestArtifact["data"], + ) => void; +} + +export const useCapabilityManifestAction = ({ + addOrUpdateArtifact, +}: UseCapabilityManifestActionProps) => { + useCopilotAction({ + name: "create_capability_manifest", + description: CAPABILITY_MANIFEST_DESCRIPTION, + parameters: [ + // Agent Header + { + name: "agentName", + type: "string", + description: + "Name of the AI agent (e.g., 'Data Analyst', 'Code Assistant')", + required: true, + }, + { + name: "agentRole", + type: "string", + description: + "Role or type of the agent (e.g., 'Agent', 'AI', 'Enterprise AI')", + required: false, + }, + { + name: "statusText", + type: "string", + description: + "Status message displayed below agent name (e.g., 'Ready for interaction')", + required: false, + }, + { + name: "agentTagsJson", + type: "string", + description: + "JSON array of tags: [{label: string, variant?: 'default'|'outline'|'filled', color?: string}]", + required: false, + }, + + // Capabilities + { + name: "capabilitiesJson", + type: "string", + description: + "JSON array of capabilities: [{id: string, name: string, status: 'enabled'|'disabled'|'pending'|'error', description?: string, iconColor?: string}]", + required: false, + }, + { + name: "capabilityGroupsJson", + type: "string", + description: + "JSON array of capability groups for organized display: [{id: string, label: string, capabilities: [...], collapsible?: boolean, defaultCollapsed?: boolean}]", + required: false, + }, + { + name: "capabilitiesLabel", + type: "string", + description: + "Custom label for capabilities section (default: 'Capabilities')", + required: false, + }, + { + name: "showCapabilities", + type: "boolean", + description: "Whether to show capabilities section (default: true)", + required: false, + }, + + // Alerts + { + name: "alertsJson", + type: "string", + description: + "JSON array of alerts: [{id: string, title: string, description: string, variant: 'warning'|'error'|'info'|'success', dismissible?: boolean}]", + required: false, + }, + + // Status + { + name: "connectionStatus", + type: "string", + description: + "Connection status: 'connected', 'connecting', 'disconnected', or 'error'", + required: false, + }, + { + name: "connectionLabel", + type: "string", + description: + "Custom label for connection status (e.g., 'Handshake Complete')", + required: false, + }, + { + name: "sessionId", + type: "string", + description: + "Session identifier for tracking (e.g., 'HAX-2024-DA-001')", + required: false, + }, + { + name: "statusMetadataJson", + type: "string", + description: + 'JSON object of additional status metadata: {key: value} (e.g., {"Region": "US-EAST", "Uptime": "99.9%"})', + required: false, + }, + + // Customization + { + name: "showSeparator", + type: "boolean", + description: + "Whether to show separator line before status (default: true)", + required: false, + }, + { + name: "showStatus", + type: "boolean", + description: "Whether to show status footer (default: true)", + required: false, + }, + + // Styling + { + name: "variant", + type: "string", + description: + "Card style variant: 'default' (with shadow), 'outline' (border only), 'ghost' (transparent)", + required: false, + }, + { + name: "size", + type: "string", + description: + "Size variant: 'sm' (compact), 'md' (default), 'lg' (expanded)", + required: false, + }, + { + name: "showShadow", + type: "boolean", + description: "Whether to show card shadow (default: true)", + required: false, + }, + ], + handler: async (args) => { + try { + const { + agentName, + agentRole, + statusText, + agentTagsJson, + capabilitiesJson, + capabilityGroupsJson, + capabilitiesLabel, + showCapabilities, + alertsJson, + connectionStatus, + connectionLabel, + sessionId, + statusMetadataJson, + showSeparator, + showStatus, + variant, + size, + showShadow, + } = args; + + // Parse and validate JSON strings with Zod + let agentTags: z.infer[] | undefined; + if (agentTagsJson) { + const parsed = JSON.parse(agentTagsJson); + agentTags = z.array(AgentTagZod).parse(parsed); + } + + let capabilities: z.infer[] | undefined; + if (capabilitiesJson) { + const parsed = JSON.parse(capabilitiesJson); + capabilities = z.array(CapabilityZod).parse(parsed); + } + + let capabilityGroups: z.infer[] | undefined; + if (capabilityGroupsJson) { + const parsed = JSON.parse(capabilityGroupsJson); + capabilityGroups = z.array(CapabilityGroupZod).parse(parsed); + } + + let alerts: z.infer[] | undefined; + if (alertsJson) { + const parsed = JSON.parse(alertsJson); + alerts = z.array(AlertZod).parse(parsed); + } + + let statusMetadata: Record | undefined; + if (statusMetadataJson) { + const parsed = JSON.parse(statusMetadataJson); + statusMetadata = z + .record(z.union([z.string(), z.number()])) + .parse(parsed); + } + + addOrUpdateArtifact("capability-manifest", { + agentName, + agentRole, + statusText, + agentTags, + capabilities, + capabilityGroups, + capabilitiesLabel, + showCapabilities, + alerts, + connectionStatus: connectionStatus as + | "connected" + | "connecting" + | "disconnected" + | "error" + | undefined, + connectionLabel, + sessionId, + statusMetadata, + showSeparator, + showStatus, + variant: variant as "default" | "outline" | "ghost" | undefined, + size: size as "sm" | "md" | "lg" | undefined, + showShadow, + }); + + return `Created capability manifest for "${agentName}"`; + } catch (error) { + console.error("Error in create_capability_manifest handler:", error); + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + return `Failed to create capability manifest: ${errorMessage}`; + } + }, + }); +}; diff --git a/hax/artifacts/capability-manifest/capability-manifest.tsx b/hax/artifacts/capability-manifest/capability-manifest.tsx new file mode 100644 index 0000000..8dd3657 --- /dev/null +++ b/hax/artifacts/capability-manifest/capability-manifest.tsx @@ -0,0 +1,550 @@ +/* + * 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 { + AlertTriangle, + CheckCircle2, + Circle, + XCircle, + Info, + AlertCircle, + ChevronRight, + ChevronDown, + type LucideIcon, +} from "lucide-react"; +import type { + CapabilityManifestData, + Capability, + CapabilityStatus, + CapabilityGroup, + Alert, + AlertVariant, + AgentTag, + ConnectionStatus, +} from "./types"; + +// ============================================================================ +// Type Definitions for Internal Use +// ============================================================================ + +interface ResolvedAgentHeader { + name: string; + role?: string; + statusText?: string; + tags?: AgentTag[]; +} + +interface ResolvedStatusInfo { + status: ConnectionStatus; + label?: string; + sessionId?: string; + metadata?: Record; + color?: string; +} + +export interface HAXCapabilityManifestProps + extends React.HTMLAttributes { + data: CapabilityManifestData; + onCapabilityClick?: (capability: Capability) => void; + onAlertDismiss?: (alert: Alert) => void; + onStatusClick?: () => void; +} + +// ============================================================================ +// Helper Components +// ============================================================================ + +const alertVariantStyles: Record< + AlertVariant, + { bg: string; border: string; text: string; icon: LucideIcon } +> = { + warning: { + bg: "bg-[#fef3c7]", + border: "border-[#fcd34d]", + text: "text-[#b45309]", + icon: AlertTriangle, + }, + error: { + bg: "bg-[#fee2e2]", + border: "border-[#fca5a5]", + text: "text-[#dc2626]", + icon: XCircle, + }, + info: { + bg: "bg-[#dbeafe]", + border: "border-[#93c5fd]", + text: "text-[#1d4ed8]", + icon: Info, + }, + success: { + bg: "bg-[#dcfce7]", + border: "border-[#86efac]", + text: "text-[#16a34a]", + icon: CheckCircle2, + }, +}; + +const statusColors: Record = { + connected: "#2ca02c", + connecting: "#f59e0b", + disconnected: "#64748b", + error: "#dc2626", +}; + +const statusLabels: Record = { + connected: "Connected", + connecting: "Connecting...", + disconnected: "Disconnected", + error: "Connection Error", +}; + +const capabilityStatusIcons: Record< + CapabilityStatus, + { icon: LucideIcon; color: string } +> = { + enabled: { icon: CheckCircle2, color: "#2ca02c" }, + disabled: { icon: Circle, color: "#64748b" }, + pending: { icon: AlertCircle, color: "#f59e0b" }, + error: { icon: XCircle, color: "#dc2626" }, +}; + +const sizeStyles = { + sm: { padding: "p-4", gap: "gap-3", text: "text-sm" }, + md: { padding: "p-6", gap: "gap-4", text: "text-base" }, + lg: { padding: "p-8", gap: "gap-5", text: "text-lg" }, +}; + +const variantStyles = { + default: + "bg-white border border-solid border-[#e2e8f0] shadow-[0px_4px_6px_-1px_rgba(0,0,0,0.1),0px_2px_4px_-2px_rgba(0,0,0,0.1)]", + outline: "bg-transparent border border-solid border-[#e2e8f0]", + ghost: "bg-transparent", +}; + +// ============================================================================ +// Sub-Components +// ============================================================================ + +interface AlertItemProps { + alert: Alert; + onDismiss?: (alert: Alert) => void; +} + +function AlertItem({ alert, onDismiss }: AlertItemProps) { + const styles = alertVariantStyles[alert.variant]; + const IconComponent = styles.icon; + + return ( +
+
+ +
+
+

{alert.title}

+

{alert.description}

+
+ {alert.dismissible && ( + + )} +
+ ); +} + +interface CapabilityItemProps { + capability: Capability; + onClick?: (capability: Capability) => void; +} + +function CapabilityItem({ capability, onClick }: CapabilityItemProps) { + const statusConfig = capabilityStatusIcons[capability.status]; + const IconComponent = statusConfig.icon; + const iconColor = capability.iconColor || statusConfig.color; + + const handleClick = () => { + onClick?.(capability); + }; + + return ( +
+
+ +
+
+

+ {capability.name} +

+ {capability.description && ( +

+ {capability.description} +

+ )} +
+ {capability.metadata && ( +
+ {Object.entries(capability.metadata) + .slice(0, 2) + .map(([key, value]) => ( + + {String(value)} + + ))} +
+ )} +
+ ); +} + +interface CapabilityGroupComponentProps { + group: CapabilityGroup; + onCapabilityClick?: (capability: Capability) => void; +} + +function CapabilityGroupComponent({ + group, + onCapabilityClick, +}: CapabilityGroupComponentProps) { + const [isCollapsed, setIsCollapsed] = React.useState( + group.defaultCollapsed ?? false, + ); + + return ( +
+
group.collapsible && setIsCollapsed(!isCollapsed)} + > +

+ {group.label} +

+ {group.collapsible && ( + + {isCollapsed ? ( + + ) : ( + + )} + + )} +
+ {!isCollapsed && ( +
+ {group.capabilities.map((capability) => ( + + ))} +
+ )} +
+ ); +} + +// ============================================================================ +// Main Component +// ============================================================================ + +/** + * HAXCapabilityManifest - A component for displaying AI agent capabilities, + * constraints, and connection status. + * + * Designed for AI agents to dynamically render their capabilities, tools, + * constraints, and session information in a standardized format. + * + * @example + * ```tsx + * + * ``` + */ +export function HAXCapabilityManifest({ + data, + onCapabilityClick, + onAlertDismiss, + onStatusClick, + className, + ...props +}: HAXCapabilityManifestProps) { + const { + // Agent header + agentName = "Agent", + agentRole, + statusText = "Capability handshake initiated • Ready for interaction", + agentTags = [], + + // Capabilities + capabilities = [], + capabilityGroups, + capabilitiesLabel = "Capabilities", + showCapabilities = true, + + // Alerts + alerts = [], + + // Status + connectionStatus = "connected", + connectionLabel, + sessionId, + statusMetadata, + + // Customization + showSeparator = true, + showStatus = true, + + // Styling + variant = "default", + size = "md", + showShadow = true, + } = data; + + // Resolve agent header + const resolvedAgent: ResolvedAgentHeader = { + name: agentName, + role: agentRole, + statusText, + tags: agentTags, + }; + + // Resolve status info + const resolvedStatus: ResolvedStatusInfo = { + status: connectionStatus, + label: connectionLabel, + sessionId, + metadata: statusMetadata, + }; + + // Convert flat capabilities to groups if no groups provided + const resolvedGroups: CapabilityGroup[] = capabilityGroups ?? [ + { + id: "__default__", + label: capabilitiesLabel, + capabilities: capabilities, + }, + ]; + + const hasCapabilities = resolvedGroups.some((g) => g.capabilities.length > 0); + + const sizeConfig = sizeStyles[size]; + const statusColor = + resolvedStatus.color ?? statusColors[resolvedStatus.status]; + const statusLabel = + resolvedStatus.label ?? statusLabels[resolvedStatus.status]; + + return ( +
+
+ {/* Agent Header */} +
+
+
+
+

+ {resolvedAgent.role + ? `${resolvedAgent.role}: ${resolvedAgent.name}` + : resolvedAgent.name} +

+ {resolvedAgent.tags?.map((tag, idx) => ( + + {tag.label} + + ))} +
+
+
+ {resolvedAgent.statusText && ( +

+ {resolvedAgent.statusText} +

+ )} +
+ + {/* Capabilities Section */} + {showCapabilities && hasCapabilities && ( +
+ {resolvedGroups.map((group) => + group.id === "__default__" ? ( + +
+

+ {group.label} +

+
+
+ {group.capabilities.map((capability) => ( + + ))} +
+
+ ) : ( + + ), + )} +
+ )} + + {/* Alerts Section */} + {alerts.length > 0 && ( +
+ {alerts.map((alert) => ( + + ))} +
+ )} + + {/* Separator */} + {showSeparator && showStatus && ( +
+ )} + + {/* Status Footer */} + {showStatus && ( +
+
+
+
+
+

+ {statusLabel} +

+
+
+ {resolvedStatus.metadata && + Object.entries(resolvedStatus.metadata).map(([key, value]) => ( +

+ {key}: {value} +

+ ))} + {resolvedStatus.sessionId && ( +

+ Session: {resolvedStatus.sessionId} +

+ )} +
+
+ )} +
+
+ ); +} + +export default HAXCapabilityManifest; diff --git a/hax/artifacts/capability-manifest/description.ts b/hax/artifacts/capability-manifest/description.ts new file mode 100644 index 0000000..6c866ea --- /dev/null +++ b/hax/artifacts/capability-manifest/description.ts @@ -0,0 +1,65 @@ +/* + * 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 CAPABILITY_MANIFEST_DESCRIPTION = + `Use capability manifest artifacts to display AI agent capabilities, constraints, and connection status to users. This component is essential for setting user expectations about what an AI agent can and cannot do, preventing the "Mental Model Mismatch" where users assume the agent has capabilities it doesn't possess. + +Best used for: +- Agent initialization and handshake displays +- Showing available tools and capabilities +- Displaying runtime constraints and policies +- Session status and connection information +- Multi-agent capability comparison + +Structure your manifest with: +1. Agent Header: Name, role, and status text to identify the agent +2. Capabilities: List of available tools/features with their status (enabled, disabled, pending, error) +3. Alerts: Runtime constraints, warnings, or important information +4. Status Footer: Connection state and session identifier + +Capability statuses: +- "enabled": Feature is available and ready to use (green checkmark) +- "disabled": Feature is not available (gray circle) +- "pending": Feature is loading or awaiting activation (yellow alert) +- "error": Feature encountered an error (red X) + +Alert variants: +- "warning": Orange - for constraints and cautions (e.g., "Requires approval for external API calls") +- "error": Red - for critical issues or failures +- "info": Blue - for informational notices +- "success": Green - for positive confirmations + +Connection statuses: +- "connected": Agent is online and ready (green dot) +- "connecting": Establishing connection (pulsing yellow dot) +- "disconnected": Agent is offline (gray dot) +- "error": Connection failed (red dot) + +Best practices: +- Always display capability manifest at session start (Phase 1) before user interaction +- Keep capability names concise but descriptive +- Use alerts sparingly - only for important constraints or warnings +- Include session ID for debugging and tracking purposes +- Group related capabilities when there are many (use capabilityGroups) +- Set appropriate connection status to reflect actual agent state + +Don't use capability manifest for: +- Displaying conversation history or messages +- Showing detailed technical documentation +- Complex data visualizations +- Form inputs or user interactions` as const diff --git a/hax/artifacts/capability-manifest/index.ts b/hax/artifacts/capability-manifest/index.ts new file mode 100644 index 0000000..10b4681 --- /dev/null +++ b/hax/artifacts/capability-manifest/index.ts @@ -0,0 +1,50 @@ +/* + * 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 { HAXCapabilityManifest } from "./capability-manifest"; +export { useCapabilityManifestAction } from "./action"; +export type { + CapabilityManifestArtifact, + CapabilityManifestData, + Capability, + CapabilityStatus, + CapabilityGroup, + Alert, + AlertVariant, + AgentHeader, + AgentTag, + StatusInfo, + ConnectionStatus, + CardVariant, + SizeVariant, +} from "./types"; +export { + CapabilityManifestArtifactZod, + CapabilityManifestDataZod, + CapabilityZod, + CapabilityStatusZod, + CapabilityGroupZod, + AlertZod, + AlertVariantZod, + AgentHeaderZod, + AgentTagZod, + StatusInfoZod, + ConnectionStatusZod, + CardVariantZod, + SizeVariantZod, +} from "./types"; diff --git a/hax/artifacts/capability-manifest/types.ts b/hax/artifacts/capability-manifest/types.ts new file mode 100644 index 0000000..92b83fb --- /dev/null +++ b/hax/artifacts/capability-manifest/types.ts @@ -0,0 +1,150 @@ +/* + * 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"; + +// Capability status enum +export const CapabilityStatusZod = z.enum([ + "enabled", + "disabled", + "pending", + "error", +]); +export type CapabilityStatus = z.infer; + +// Alert variant enum +export const AlertVariantZod = z.enum(["warning", "error", "info", "success"]); +export type AlertVariant = z.infer; + +// Connection status enum +export const ConnectionStatusZod = z.enum([ + "connected", + "connecting", + "disconnected", + "error", +]); +export type ConnectionStatus = z.infer; + +// Card variant enum +export const CardVariantZod = z.enum(["default", "outline", "ghost"]); +export type CardVariant = z.infer; + +// Size variant enum +export const SizeVariantZod = z.enum(["sm", "md", "lg"]); +export type SizeVariant = z.infer; + +// Capability schema +export const CapabilityZod = z.object({ + id: z.string(), + name: z.string(), + description: z.string().optional(), + status: CapabilityStatusZod, + iconColor: z.string().optional(), + metadata: z.record(z.union([z.string(), z.number(), z.boolean()])).optional(), +}); +export type Capability = z.infer; + +// Alert schema +export const AlertZod = z.object({ + id: z.string(), + title: z.string(), + description: z.string(), + variant: AlertVariantZod, + dismissible: z.boolean().optional(), +}); +export type Alert = z.infer; + +// Capability group schema +export const CapabilityGroupZod = z.object({ + id: z.string(), + label: z.string(), + capabilities: z.array(CapabilityZod), + collapsible: z.boolean().optional(), + defaultCollapsed: z.boolean().optional(), +}); +export type CapabilityGroup = z.infer; + +// Agent header schema +export const AgentHeaderZod = z.object({ + name: z.string(), + role: z.string().optional(), + statusText: z.string().optional(), +}); +export type AgentHeader = z.infer; + +// Agent tag schema +export const AgentTagZod = z.object({ + label: z.string(), + color: z.string().optional(), + variant: z.enum(["default", "outline", "filled"]).optional(), +}); +export type AgentTag = z.infer; + +// Status info schema +export const StatusInfoZod = z.object({ + status: ConnectionStatusZod, + label: z.string().optional(), + sessionId: z.string().optional(), + metadata: z.record(z.union([z.string(), z.number()])).optional(), + color: z.string().optional(), +}); +export type StatusInfo = z.infer; + +// Main artifact data schema +export const CapabilityManifestDataZod = z.object({ + // Agent header + agentName: z.string().optional(), + agentRole: z.string().optional(), + statusText: z.string().optional(), + agentTags: z.array(AgentTagZod).optional(), + + // Capabilities + capabilities: z.array(CapabilityZod).optional(), + capabilityGroups: z.array(CapabilityGroupZod).optional(), + capabilitiesLabel: z.string().optional(), + showCapabilities: z.boolean().optional(), + + // Alerts + alerts: z.array(AlertZod).optional(), + + // Status + connectionStatus: ConnectionStatusZod.optional(), + connectionLabel: z.string().optional(), + sessionId: z.string().optional(), + statusMetadata: z.record(z.union([z.string(), z.number()])).optional(), + + // Customization + showSeparator: z.boolean().optional(), + showStatus: z.boolean().optional(), + + // Styling + variant: CardVariantZod.optional(), + size: SizeVariantZod.optional(), + showShadow: z.boolean().optional(), +}); +export type CapabilityManifestData = z.infer; + +// Full artifact schema +export const CapabilityManifestArtifactZod = z.object({ + id: z.string(), + type: z.literal("capability-manifest"), + data: CapabilityManifestDataZod, +}); +export type CapabilityManifestArtifact = z.infer< + typeof CapabilityManifestArtifactZod +>; diff --git a/hax/artifacts/inline-rationale/README.md b/hax/artifacts/inline-rationale/README.md new file mode 100644 index 0000000..bdc0baa --- /dev/null +++ b/hax/artifacts/inline-rationale/README.md @@ -0,0 +1,224 @@ +# Inline Rationale Component + +A React component for displaying AI-driven assessments, decisions, and explanations with intent-based visual theming. Ideal for security assessments, code reviews, policy decisions, and any AI-generated rationale that needs clear visual distinction. + +## Installation + +### Prerequisites + +- React 18+ +- Tailwind CSS configured in your project +- HAX CLI installed globally + +### Initialize HAX in Your Project + +```bash +hax init +``` + +This sets up the necessary configuration and dependencies in your project. + +### Add the Inline Rationale Component + +```bash +hax add artifact inline-rationale +``` + +This command will: +- Install the component files to your project +- Add required dependencies (`zod`, `@copilotkit/react-core`) +- Set up the `cn` utility if not already present + +## Usage + +### Basic Usage + +```tsx +import { InlineRationale } from "@/artifacts/inline-rationale" + +function App() { + return ( + + ) +} +``` + +### HAX Wrapper Component + +Use `HAXInlineRationale` for a pre-styled wrapper with margin: + +```tsx +import { HAXInlineRationale } from "@/artifacts/inline-rationale" + +function App() { + return ( + + ) +} +``` + +### Collapsible Content + +Enable collapse/expand functionality: + +```tsx + console.log("Collapsed:", collapsed)} +/> +``` + +### CopilotKit Integration + +Use the action hook for AI-driven rationale creation: + +```tsx +import { useInlineRationaleAction } from "@/artifacts/inline-rationale" + +function ChatComponent() { + const [artifacts, setArtifacts] = useState([]) + + useInlineRationaleAction({ + addOrUpdateArtifact: (type, data) => { + setArtifacts(prev => [...prev, { type, data, id: Date.now() }]) + } + }) + + return ( + // Your chat UI that renders artifacts + ) +} +``` + +## Props + +### InlineRationaleProps + +| Prop | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `id` | `string` | Yes | - | Unique identifier | +| `assessmentType` | `string` | Yes | - | Type of assessment (e.g., "security_assessment", "code_review") | +| `intent` | `Intent` | Yes | - | Visual theme: "warn", "approve", "block", "inform" | +| `title` | `string` | Yes | - | Display title | +| `description` | `string` | Yes | - | Main description paragraph | +| `summary` | `AssessmentSummary` | Yes | - | Summary with impact and exploitability | +| `rationale` | `RationaleItem[]` | Yes | - | Detail items as label/value pairs | +| `confidence` | `number` | Yes | - | Confidence score (0-100) | +| `metadata` | `Metadata` | No | - | Optional tracking metadata | +| `collapsed` | `boolean` | No | `false` | Initial collapsed state | +| `collapsible` | `boolean` | No | `false` | Enable collapse toggle | +| `onCollapseChange` | `(collapsed: boolean) => void` | No | - | Collapse change callback | +| `className` | `string` | No | - | Additional CSS classes | + +## Types + +### Intent + +```typescript +type Intent = "warn" | "approve" | "block" | "inform" +``` + +Visual themes: +- `block` (red): Security vulnerabilities, access denials, critical issues +- `warn` (yellow): Performance issues, warnings that need attention +- `approve` (green): Approvals, successful validations +- `inform` (blue): General information, neutral notifications + +### ImpactLevel + +```typescript +type ImpactLevel = "low" | "medium" | "high" | "critical" +``` + +### ExploitabilityLevel + +```typescript +type ExploitabilityLevel = "none" | "low" | "medium" | "high" +``` + +### AssessmentSummary + +```typescript +interface AssessmentSummary { + impact: ImpactLevel + exploitability: ExploitabilityLevel + tags?: string[] +} +``` + +### RationaleItem + +```typescript +interface RationaleItem { + label: string + value: string +} +``` + +### Metadata + +```typescript +interface Metadata { + generated_by: "ai" | "human" | "hybrid" + model?: string + version?: string + timestamp?: string +} +``` + +## Files + +| File | Description | +|------|-------------| +| `inline-rationale.tsx` | Main React component with `InlineRationale` and `HAXInlineRationale` | +| `types.ts` | Zod schemas and TypeScript type definitions | +| `action.ts` | CopilotKit integration hook (`useInlineRationaleAction`) | +| `description.ts` | AI prompt guidance constant | +| `index.ts` | Module exports | diff --git a/hax/artifacts/inline-rationale/action.ts b/hax/artifacts/inline-rationale/action.ts new file mode 100644 index 0000000..1600fa7 --- /dev/null +++ b/hax/artifacts/inline-rationale/action.ts @@ -0,0 +1,174 @@ +/* + * 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 { INLINE_RATIONALE_DESCRIPTION } from "./description"; +import type { + InlineRationaleArtifact, + Intent, + ImpactLevel, + ExploitabilityLevel, +} from "./types"; + +const RationaleItemZod = z.object({ + label: z.string(), + value: z.string(), +}); + +interface UseInlineRationaleActionProps { + addOrUpdateArtifact: ( + type: "inline-rationale", + data: InlineRationaleArtifact["data"], + ) => void; +} + +export const useInlineRationaleAction = ({ + addOrUpdateArtifact, +}: UseInlineRationaleActionProps) => { + useCopilotAction({ + name: "create_inline_rationale", + description: INLINE_RATIONALE_DESCRIPTION, + parameters: [ + { + name: "title", + type: "string", + description: "Display title for the rationale card", + required: true, + }, + { + name: "description", + type: "string", + description: "Main paragraph text explaining the assessment", + required: true, + }, + { + name: "intent", + type: "string", + description: + "Visual theme intent: 'block' (red, critical issues), 'warn' (yellow, warnings), 'approve' (green, approvals), 'inform' (blue, information)", + required: true, + }, + { + name: "assessmentType", + type: "string", + description: + "Type of assessment (e.g., 'security_assessment', 'code_review', 'policy_decision', 'performance_alert')", + required: true, + }, + { + name: "impact", + type: "string", + description: "Impact level: 'low', 'medium', 'high', 'critical'", + required: true, + }, + { + name: "exploitability", + type: "string", + description: "Exploitability level: 'none', 'low', 'medium', 'high'", + required: true, + }, + { + name: "confidence", + type: "number", + description: "Confidence score 0-100 (badge color derived from intent)", + required: true, + }, + { + name: "rationaleJson", + type: "string", + description: + "JSON string of rationale array with {label, value} objects for detail items", + required: true, + }, + { + name: "tagsJson", + type: "string", + description: + "Optional JSON string of tags array (e.g., ['v2.4.0', 'Production'])", + required: false, + }, + { + name: "collapsible", + type: "boolean", + description: "Whether the content can be collapsed/expanded", + required: false, + }, + { + name: "collapsed", + type: "boolean", + description: "Initial collapsed state (default: false)", + required: false, + }, + ], + handler: async (args) => { + try { + const { + title, + description, + intent, + assessmentType, + impact, + exploitability, + confidence, + rationaleJson, + tagsJson, + collapsible, + collapsed, + } = args; + + // Parse and validate rationale JSON with Zod + let rationale: z.infer[] = []; + if (rationaleJson) { + const parsed = JSON.parse(rationaleJson); + rationale = z.array(RationaleItemZod).parse(parsed); + } + + // Parse and validate tags JSON with Zod + let tags: string[] | undefined; + if (tagsJson) { + const parsed = JSON.parse(tagsJson); + tags = z.array(z.string()).parse(parsed); + } + + addOrUpdateArtifact("inline-rationale", { + assessmentType, + intent: intent as Intent, + title, + description, + summary: { + impact: impact as ImpactLevel, + exploitability: exploitability as ExploitabilityLevel, + tags, + }, + rationale, + confidence, + collapsible, + collapsed, + }); + + return `Created inline rationale "${title}"`; + } catch (error) { + console.error("Error in create_inline_rationale handler:", error); + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + return `Failed to create inline rationale: ${errorMessage}`; + } + }, + }); +}; diff --git a/hax/artifacts/inline-rationale/description.ts b/hax/artifacts/inline-rationale/description.ts new file mode 100644 index 0000000..d487bb4 --- /dev/null +++ b/hax/artifacts/inline-rationale/description.ts @@ -0,0 +1,45 @@ +/* + * 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 INLINE_RATIONALE_DESCRIPTION = + `Use inline-rationale artifacts to display AI-driven assessments, decisions, and explanations with intent-based visual theming. Best for security assessments, code reviews, policy decisions, and any AI-generated rationale that needs clear visual distinction. + +Schema (Single Source of Truth): +- intent: Drives the visual theme (warn=yellow, block=red, approve=green, inform=blue) +- description: Main paragraph text explaining the assessment +- summary: { impact, exploitability } for generating badges +- rationale: Detail items as { label, value } pairs +- confidence: 0-100 score (badge color comes from intent) + +Choose the appropriate intent based on the nature of the assessment: +- "block" (red): Security vulnerabilities, access denials, critical issues requiring immediate action +- "warn" (yellow): Performance issues, potential problems, warnings that need attention +- "approve" (green): Code review approvals, successful validations, positive outcomes +- "inform" (blue): General information, deployment summaries, neutral notifications + +Impact levels: "low", "medium", "high", "critical" +Exploitability levels: "none", "low", "medium", "high" + +Best practices: +- Use clear, actionable titles +- Keep description focused on the key message +- Limit rationale items to 3-6 most important factors +- Match intent to the severity/nature of the assessment +- Include confidence when the AI has varying certainty + +Don't use inline-rationale for simple text responses. Use it when you need to communicate structured AI reasoning with visual distinction based on the nature of the decision.` as const diff --git a/hax/artifacts/inline-rationale/index.ts b/hax/artifacts/inline-rationale/index.ts new file mode 100644 index 0000000..03a2b8f --- /dev/null +++ b/hax/artifacts/inline-rationale/index.ts @@ -0,0 +1,33 @@ +/* + * 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 { InlineRationale, HAXInlineRationale } from "./inline-rationale" +export type { InlineRationaleProps } from "./inline-rationale" +export { useInlineRationaleAction } from "./action" +export type { + InlineRationaleArtifact, + Intent, + ImpactLevel, + ExploitabilityLevel, + GeneratedBy, + AssessmentSummary, + RationaleItem, + Metadata, +} from "./types" +export { InlineRationaleArtifactZod } from "./types" +export { INLINE_RATIONALE_DESCRIPTION } from "./description" diff --git a/hax/artifacts/inline-rationale/inline-rationale.tsx b/hax/artifacts/inline-rationale/inline-rationale.tsx new file mode 100644 index 0000000..ea355d1 --- /dev/null +++ b/hax/artifacts/inline-rationale/inline-rationale.tsx @@ -0,0 +1,341 @@ +/* + * 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 type { + Intent, + ImpactLevel, + ExploitabilityLevel, + AssessmentSummary, + RationaleItem, + Metadata, +} from "./types"; + +// ============================================================================= +// Component Props +// ============================================================================= + +export interface InlineRationaleProps { + /** Unique identifier */ + id: string; + /** Assessment type - flexible string (e.g., "security_assessment", "code_review") */ + assessmentType: string; + /** Intent drives visual theme: warn=yellow, block=red, approve=green, inform=blue */ + intent: Intent; + /** Display title */ + title: string; + /** Main description paragraph */ + description: string; + /** Structured summary for badges (impact + exploitability) */ + summary: AssessmentSummary; + /** Detail items displayed as "Label: Value" pairs */ + rationale: RationaleItem[]; + /** Confidence score 0-100 (badge color derived from intent) */ + confidence: number; + /** Optional metadata */ + metadata?: Metadata; + /** Optional: collapsed state */ + collapsed?: boolean; + /** Optional: enable collapse toggle */ + collapsible?: boolean; + /** Optional: collapse change callback */ + onCollapseChange?: (collapsed: boolean) => void; + /** Optional: additional CSS classes */ + className?: string; +} + +// ============================================================================= +// Internal Style Mappings +// ============================================================================= + +type VariantKey = "critical" | "warning" | "success" | "info"; +type BadgeVariant = "default" | "success" | "warning" | "critical"; + +/** Intent → card variant (background/border) */ +const INTENT_TO_VARIANT: Record = { + warn: "warning", + approve: "success", + block: "critical", + inform: "info", +}; + +/** Intent → confidence badge color */ +const INTENT_TO_BADGE_VARIANT: Record = { + warn: "warning", + approve: "success", + block: "critical", + inform: "success", +}; + +/** Card variant styles */ +const VARIANT_STYLES: Record< + VariantKey, + { background: string; border: string } +> = { + critical: { + background: "bg-[#fff1f2]", + border: "border-[#e11d48]", + }, + warning: { + background: "bg-[#fffbeb]", + border: "border-[#f59e0b]", + }, + success: { + background: "bg-[#ecfdf5]", + border: "border-[#059669]", + }, + info: { + background: "bg-[#eff6ff]", + border: "border-[#3b82f6]", + }, +}; + +/** Badge variant styles */ +const BADGE_STYLES: Record = { + default: "bg-[#f1f5f9] text-[#0f172a]", + success: "bg-[#a7f3d0] text-[#047857]", + warning: "bg-[#fde68a] text-[#b45309]", + critical: "bg-[#fecaca] text-[#dc2626]", +}; + +/** Impact level labels */ +const IMPACT_LABELS: Record = { + low: "Low Impact", + medium: "Medium Impact", + high: "High Impact", + critical: "Critical Impact", +}; + +/** Exploitability level labels */ +const EXPLOITABILITY_LABELS: Record = { + none: "Exploitability : None", + low: "Exploitability : Low", + medium: "Exploitability : Medium", + high: "Exploitability : High", +}; + +// ============================================================================= +// Component +// ============================================================================= + +export function InlineRationale({ + id, + assessmentType, + intent, + title, + description, + summary, + rationale, + confidence, + collapsed = false, + collapsible = false, + onCollapseChange, + className, +}: InlineRationaleProps) { + const [isCollapsed, setIsCollapsed] = React.useState(collapsed); + + React.useEffect(() => { + setIsCollapsed(collapsed); + }, [collapsed]); + + const handleToggleCollapse = () => { + if (collapsible) { + const newState = !isCollapsed; + setIsCollapsed(newState); + onCollapseChange?.(newState); + } + }; + + // Get card variant from intent + const variant = INTENT_TO_VARIANT[intent]; + const variantStyle = VARIANT_STYLES[variant]; + + // Build badges: Impact (gray) → Exploitability (gray) → Confidence (colored) → Tags (gray) + const badges = [ + { + label: IMPACT_LABELS[summary.impact], + variant: "default" as BadgeVariant, + }, + { + label: EXPLOITABILITY_LABELS[summary.exploitability], + variant: "default" as BadgeVariant, + }, + { + label: `${confidence}% Confidence`, + variant: INTENT_TO_BADGE_VARIANT[intent], + }, + ...(summary.tags?.map((tag) => ({ + label: tag, + variant: "default" as BadgeVariant, + })) || []), + ]; + + return ( +
+ {/* Header */} +
+ {/* Title */} +

+ {title} +

+ + {/* Badges */} + {badges.length > 0 && ( +
+ {badges.map((badge, index) => ( + + {badge.label} + + ))} +
+ )} + + {/* Collapse indicator */} + {collapsible && ( + + + + )} +
+ + {/* Content (collapsible) */} + {!isCollapsed && ( + <> + {/* Separator - violet color as per Figma */} +
+ + {/* Body */} +
+
+
+ {/* Main description */} + {description && ( +

{description}

+ )} + + {/* Detail items */} + {rationale.map((item, index) => ( +

+ {item.label}: + {item.value} +

+ ))} +
+
+
+ + )} +
+ ); +} + +// ============================================================================= +// HAX Wrapper Component +// ============================================================================= + +interface HAXInlineRationaleProps { + assessmentType: string; + intent: Intent; + title: string; + description: string; + summary: AssessmentSummary; + rationale: RationaleItem[]; + confidence: number; + metadata?: Metadata; + collapsed?: boolean; + collapsible?: boolean; +} + +export function HAXInlineRationale({ + assessmentType, + intent, + title, + description, + summary, + rationale, + confidence, + metadata, + collapsed, + collapsible, +}: HAXInlineRationaleProps) { + const id = React.useId(); + return ( +
+ +
+ ); +} + +export default InlineRationale; diff --git a/hax/artifacts/inline-rationale/types.ts b/hax/artifacts/inline-rationale/types.ts new file mode 100644 index 0000000..f48016e --- /dev/null +++ b/hax/artifacts/inline-rationale/types.ts @@ -0,0 +1,111 @@ +/* + * 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" + +/** + * Intent drives the visual theme of the inline rationale + * - warn: yellow/amber theme for warnings + * - approve: green theme for approvals + * - block: red theme for blocks/denials + * - inform: blue theme for informational + */ +const IntentZod = z.enum(["warn", "approve", "block", "inform"]) + +/** + * Impact level for summary badge + */ +const ImpactLevelZod = z.enum(["low", "medium", "high", "critical"]) + +/** + * Exploitability level for summary badge + */ +const ExploitabilityLevelZod = z.enum(["none", "low", "medium", "high"]) + +/** + * Source of the assessment + */ +const GeneratedByZod = z.enum(["ai", "human", "hybrid"]) + +/** + * Assessment summary with enum-based fields + */ +const AssessmentSummaryZod = z.object({ + impact: ImpactLevelZod, + exploitability: ExploitabilityLevelZod, + tags: z.array(z.string()).optional(), +}) + +/** + * A single rationale detail item + */ +const RationaleItemZod = z.object({ + label: z.string(), + value: z.string(), +}) + +/** + * Optional metadata for tracking + */ +const MetadataZod = z.object({ + generated_by: GeneratedByZod, + model: z.string().optional(), + version: z.string().optional(), + timestamp: z.string().optional(), +}) + +/** + * Inline Rationale Artifact Data Schema + */ +export const InlineRationaleArtifactZod = z.object({ + id: z.string(), + type: z.literal("inline-rationale"), + data: z.object({ + /** Assessment type - flexible string (e.g., "security_assessment", "code_review") */ + assessmentType: z.string(), + /** Intent drives visual theme: warn=yellow, block=red, approve=green, inform=blue */ + intent: IntentZod, + /** Display title */ + title: z.string(), + /** Main description paragraph */ + description: z.string(), + /** Structured summary for badges (impact + exploitability) */ + summary: AssessmentSummaryZod, + /** Detail items displayed as "Label: Value" pairs */ + rationale: z.array(RationaleItemZod), + /** Confidence score 0-100 (badge color derived from intent) */ + confidence: z.number().min(0).max(100), + /** Optional metadata */ + metadata: MetadataZod.optional(), + /** Optional: collapsed state */ + collapsed: z.boolean().optional(), + /** Optional: enable collapse toggle */ + collapsible: z.boolean().optional(), + }), +}) + +export type InlineRationaleArtifact = z.infer + +// Re-export individual types for convenience +export type Intent = z.infer +export type ImpactLevel = z.infer +export type ExploitabilityLevel = z.infer +export type GeneratedBy = z.infer +export type AssessmentSummary = z.infer +export type RationaleItem = z.infer +export type Metadata = z.infer diff --git a/hax/artifacts/workshop-card/README.md b/hax/artifacts/workshop-card/README.md new file mode 100644 index 0000000..a8c3ff6 --- /dev/null +++ b/hax/artifacts/workshop-card/README.md @@ -0,0 +1,200 @@ +# Workshop Card Artifact + +A rich event/workshop card component for displaying scheduled events, meetings, webinars, and workshops with attendee information and action buttons. + +## Installation + +Install the workshop card artifact using the HAX CLI: + +```bash +npx hax add artifact workshop-card +``` + +Or if you have HAX SDK installed globally: + +```bash +hax add artifact workshop-card +``` + +## Dependencies + +This artifact requires the following peer dependencies: + +- `@copilotkit/react-core` - For the action hook +- `lucide-react` - For icons +- `zod` - For schema validation +- `class-variance-authority` - For button variants + +All required UI components (Button, Badge, Avatar) are included locally and will be installed automatically with the artifact. + +## Usage + +### 1. Import the components + +```tsx +import { + HAXWorkshopCard, + useWorkshopCardAction, + WorkshopCardArtifactZod, +} from "@hax/artifacts/workshop-card"; +``` + +### 2. Register the action hook + +In your main app or artifact provider component: + +```tsx +import { useWorkshopCardAction } from "@hax/artifacts/workshop-card"; + +function ArtifactProvider({ children }) { + const [artifacts, setArtifacts] = useState([]); + + const addOrUpdateArtifact = (type, data) => { + const newArtifact = { + id: `${type}-${Date.now()}`, + type, + data, + }; + setArtifacts((prev) => [...prev, newArtifact]); + }; + + // Register the workshop card action + useWorkshopCardAction({ addOrUpdateArtifact }); + + return <>{children}; +} +``` + +### 3. Render the component + +```tsx +import { HAXWorkshopCard } from "@hax/artifacts/workshop-card"; + +function WorkshopCardArtifact({ data }) { + return ( + console.log("Joined!")} + onDecline={() => console.log("Declined")} + onMaybe={() => console.log("Maybe")} + onAddToCalendar={() => console.log("Added to calendar")} + /> + ); +} +``` + +## Schema + +The workshop card uses the following Zod schema for validation: + +```typescript +import { z } from "zod"; + +const AttendeeZod = z.object({ + id: z.string().optional(), + name: z.string(), + avatarUrl: z.string().optional(), +}); + +const WorkshopCardArgsZod = z.object({ + title: z.string(), + description: z.string().optional(), + eventType: z.string().optional().default("Online Event"), + status: z.enum(["confirmed", "pending", "cancelled"]).optional().default("confirmed"), + date: z.string().optional(), + time: z.string().optional(), + duration: z.string().optional(), + location: z.string().optional(), + attendeeCount: z.number().optional().default(0), + attendees: z.array(AttendeeZod).optional().default([]), + maxDisplayedAttendees: z.number().optional().default(3), + showJoinButton: z.boolean().optional().default(true), + showDeclineButton: z.boolean().optional().default(true), + showMaybeButton: z.boolean().optional().default(true), +}); +``` + +## Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `title` | `string` | **required** | The title of the workshop/event | +| `description` | `string` | `undefined` | A brief description of the event | +| `eventType` | `string` | `"Online Event"` | Type of event (e.g., 'Workshop', 'Webinar') | +| `status` | `"confirmed" \| "pending" \| "cancelled"` | `"confirmed"` | Current status of the event | +| `date` | `string` | `undefined` | The date of the event | +| `time` | `string` | `undefined` | The time of the event | +| `duration` | `string` | `undefined` | Duration of the event | +| `location` | `string` | `undefined` | Location or platform name | +| `attendeeCount` | `number` | `0` | Total number of attendees | +| `attendees` | `Attendee[]` | `[]` | List of attendees for avatar stack | +| `maxDisplayedAttendees` | `number` | `3` | Max avatars to display | +| `showJoinButton` | `boolean` | `true` | Show the Join Event button | +| `showDeclineButton` | `boolean` | `true` | Show the Decline button | +| `showMaybeButton` | `boolean` | `true` | Show the Maybe button | +| `onJoin` | `() => void` | `undefined` | Callback when Join is clicked | +| `onDecline` | `() => void` | `undefined` | Callback when Decline is clicked | +| `onMaybe` | `() => void` | `undefined` | Callback when Maybe is clicked | +| `onAddToCalendar` | `() => void` | `undefined` | Callback for add to calendar | + +## Action + +The `useWorkshopCardAction` hook registers a CopilotKit action named `show_workshop_card` that allows the AI to display workshop cards. + +### Action Name + +`show_workshop_card` + +### Action Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `title` | `string` | Yes | Event title | +| `description` | `string` | No | Event description | +| `eventType` | `string` | No | Type of event | +| `status` | `string` | No | Event status | +| `date` | `string` | No | Event date | +| `time` | `string` | No | Event time | +| `duration` | `string` | No | Event duration | +| `location` | `string` | No | Event location/platform | +| `attendeeCount` | `number` | No | Number of attendees | +| `attendees` | `object[]` | No | Attendee list | +| `showJoinButton` | `boolean` | No | Show join button | +| `showDeclineButton` | `boolean` | No | Show decline button | +| `showMaybeButton` | `boolean` | No | Show maybe button | + +## Example + +```tsx + window.open("https://zoom.us/j/123456789")} + onDecline={() => alert("You declined the invitation")} + onAddToCalendar={() => { + // Add to calendar logic + }} +/> +``` + +## Exported Components + +- `HAXWorkshopCard` - Main workshop card component +- `EventDetailItem` - Individual detail row component +- `AvatarStack` - Attendee avatar stack component +- `StatusBadge` - Status badge component +- `ActionButton` - Action button component +- `defaultStatusConfigs` - Default status configurations + diff --git a/hax/artifacts/workshop-card/action.ts b/hax/artifacts/workshop-card/action.ts new file mode 100644 index 0000000..b5169a4 --- /dev/null +++ b/hax/artifacts/workshop-card/action.ts @@ -0,0 +1,185 @@ +/* + * 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 { WorkshopCardArtifact, AttendeeZod } from "./types"; +import { WORKSHOP_CARD_DESCRIPTION } from "./description"; + +interface UseWorkshopCardActionProps { + addOrUpdateArtifact: ( + type: "workshopCard", + data: WorkshopCardArtifact["data"], + ) => void; +} + +export const useWorkshopCardAction = ({ + addOrUpdateArtifact, +}: UseWorkshopCardActionProps) => { + useCopilotAction({ + name: "show_workshop_card", + description: WORKSHOP_CARD_DESCRIPTION, + parameters: [ + { + name: "title", + type: "string", + description: "The title of the workshop or event", + required: true, + }, + { + name: "description", + type: "string", + description: "A brief description of the workshop or event", + required: false, + }, + { + name: "eventType", + type: "string", + description: + "Type of event (e.g., 'Online Event', 'Workshop', 'Webinar', 'Conference')", + required: false, + default: "Online Event", + }, + { + name: "status", + type: "string", + description: "Current status: 'confirmed', 'pending', or 'cancelled'", + required: false, + default: "confirmed", + }, + { + name: "date", + type: "string", + description: + "The date of the event (e.g., 'Tuesday, January 15, 2025')", + required: false, + }, + { + name: "time", + type: "string", + description: "The time of the event (e.g., '10:00 AM - 11:30 AM PST')", + required: false, + }, + { + name: "duration", + type: "string", + description: "Duration of the event (e.g., '1.5 hours', '2 hours')", + required: false, + }, + { + name: "location", + type: "string", + description: + "Location or platform (e.g., 'Zoom Meeting', 'Google Meet', 'Microsoft Teams')", + required: false, + }, + { + name: "attendeeCount", + type: "number", + description: "Total number of attendees", + required: false, + default: 0, + }, + { + name: "attendees", + type: "object[]", + description: "Array of attendees with id, name, and optional avatarUrl", + required: false, + }, + { + name: "maxDisplayedAttendees", + type: "number", + description: "Maximum number of attendee avatars to display", + required: false, + default: 3, + }, + { + name: "showJoinButton", + type: "boolean", + description: "Whether to show the Join Event button", + required: false, + default: true, + }, + { + name: "showDeclineButton", + type: "boolean", + description: "Whether to show the Decline button", + required: false, + default: true, + }, + { + name: "showMaybeButton", + type: "boolean", + description: "Whether to show the Maybe button", + required: false, + default: true, + }, + ], + handler: async (args) => { + try { + const { + title, + description, + eventType, + status, + date, + time, + duration, + location, + attendeeCount, + attendees, + maxDisplayedAttendees, + showJoinButton, + showDeclineButton, + showMaybeButton, + } = args; + + // Validate attendees array with Zod if provided + let validatedAttendees: z.infer[] | undefined; + if (attendees && Array.isArray(attendees)) { + validatedAttendees = z.array(AttendeeZod).parse(attendees); + } + + addOrUpdateArtifact("workshopCard", { + title: title ?? "Untitled Event", + description: description, + eventType: eventType ?? "Online Event", + status: + (status as "confirmed" | "pending" | "cancelled") ?? "confirmed", + date: date, + time: time, + duration: duration, + location: location, + attendeeCount: attendeeCount ?? 0, + attendees: validatedAttendees ?? [], + maxDisplayedAttendees: maxDisplayedAttendees ?? 3, + showJoinButton: showJoinButton ?? true, + showDeclineButton: showDeclineButton ?? true, + showMaybeButton: showMaybeButton ?? true, + }); + + return `Displayed workshop card for "${title}"${date ? ` on ${date}` : ""}${location ? ` at ${location}` : ""}.`; + } catch (error) { + console.error("Error in show_workshop_card handler:", error); + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + return `Failed to create workshop card: ${errorMessage}`; + } + }, + }); +}; diff --git a/hax/artifacts/workshop-card/description.ts b/hax/artifacts/workshop-card/description.ts new file mode 100644 index 0000000..a13c7d2 --- /dev/null +++ b/hax/artifacts/workshop-card/description.ts @@ -0,0 +1,32 @@ +/* + * 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 WORKSHOP_CARD_DESCRIPTION = + `Use this action to display an event or workshop card with rich details including title, description, date, time, location, attendees, and action buttons. Best for presenting scheduled events, workshops, webinars, meetings, and conferences in a visually appealing card format. + +This component is ideal for: +- Displaying upcoming events or workshops +- Showing meeting invitations with RSVP options +- Presenting webinar or conference details +- Calendar event summaries with attendee information + +Include a clear, descriptive title and relevant event details. Use appropriate status values ('confirmed', 'pending', 'cancelled') to indicate event status. Provide date and time in human-readable formats. Include attendee information when relevant to show social proof or team participation. + +Event types should be descriptive: 'Online Event', 'Workshop', 'Webinar', 'Conference', 'Team Meeting', 'Training Session', etc. Location can be a physical address or virtual meeting platform name. + +Don't create workshop cards for simple text information that doesn't involve scheduled events. Avoid using this for general content display - use it specifically for event-related information with dates, times, and actionable elements.` as const; diff --git a/hax/artifacts/workshop-card/index.ts b/hax/artifacts/workshop-card/index.ts new file mode 100644 index 0000000..9029fb4 --- /dev/null +++ b/hax/artifacts/workshop-card/index.ts @@ -0,0 +1,22 @@ +/* + * 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 { HAXWorkshopCard } from "./workshop-card"; +export { useWorkshopCardAction } from "./action"; +export type { WorkshopCardArtifact, WorkshopCardData, Attendee } from "./types"; +export { WorkshopCardArtifactZod, WorkshopCardArgsZod, AttendeeZod } from "./types"; diff --git a/hax/artifacts/workshop-card/types.ts b/hax/artifacts/workshop-card/types.ts new file mode 100644 index 0000000..a3ff49c --- /dev/null +++ b/hax/artifacts/workshop-card/types.ts @@ -0,0 +1,88 @@ +/* + * 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 AttendeeZod = z.object({ + id: z.string().optional(), + name: z.string(), + avatarUrl: z.string().optional(), +}); + +export const WorkshopCardArgsZod = z.object({ + title: z.string().describe("The title of the workshop/event"), + description: z + .string() + .optional() + .describe("A brief description of the workshop/event"), + eventType: z + .string() + .optional() + .describe("Type of event (e.g., 'Online Event', 'Workshop', 'Webinar')"), + status: z + .enum(["confirmed", "pending", "cancelled"]) + .optional() + .describe("Current status of the event"), + date: z + .string() + .optional() + .describe("The date of the event (e.g., 'Tuesday, January 15, 2025')"), + time: z + .string() + .optional() + .describe("The time of the event (e.g., '10:00 AM - 11:30 AM PST')"), + duration: z + .string() + .optional() + .describe("Duration of the event (e.g., '1.5 hours')"), + location: z + .string() + .optional() + .describe("Location or platform (e.g., 'Zoom Meeting', 'Google Meet')"), + attendeeCount: z.number().optional().describe("Total number of attendees"), + attendees: z + .array(AttendeeZod) + .optional() + .describe("List of attendees to display in avatar stack"), + maxDisplayedAttendees: z + .number() + .optional() + .describe("Maximum number of avatars to display"), + showJoinButton: z + .boolean() + .optional() + .describe("Whether to show the Join Event button"), + showDeclineButton: z + .boolean() + .optional() + .describe("Whether to show the Decline button"), + showMaybeButton: z + .boolean() + .optional() + .describe("Whether to show the Maybe button"), +}); + +export const WorkshopCardArtifactZod = z.object({ + id: z.string(), + type: z.literal("workshopCard"), + data: WorkshopCardArgsZod, +}); + +export type Attendee = z.infer; +export type WorkshopCardData = z.infer; +export type WorkshopCardArtifact = z.infer; diff --git a/hax/artifacts/workshop-card/workshop-card.tsx b/hax/artifacts/workshop-card/workshop-card.tsx new file mode 100644 index 0000000..6d5a383 --- /dev/null +++ b/hax/artifacts/workshop-card/workshop-card.tsx @@ -0,0 +1,402 @@ +/* + * 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 * as React from "react"; +import { cn } from "@/lib/utils"; +import { Button, buttonVariants } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; +import { GeneratedUiWrapper } from "@/components/generated-ui-wrapper"; +import { CalendarIcon, ClockIcon, VideoIcon, UsersIcon } from "lucide-react"; +import type { VariantProps } from "class-variance-authority"; +import type { WorkshopCardData, Attendee } from "./types"; + +// ============================================================================ +// Types +// ============================================================================ + +export interface EventDetailItemProps { + icon?: React.ReactNode; + label: React.ReactNode; + sublabel?: React.ReactNode; + className?: string; +} + +export interface ActionButtonConfig { + label: string; + variant?: VariantProps["variant"]; + size?: VariantProps["size"]; + onClick?: () => void; + disabled?: boolean; + className?: string; + icon?: React.ReactNode; + iconAfter?: React.ReactNode; + hidden?: boolean; +} + +export interface BadgeConfig { + label: string; + variant?: "default" | "secondary" | "destructive" | "outline"; + className?: string; + hidden?: boolean; +} + +export interface StatusConfig { + key: string; + label: string; + className: string; +} + +export interface AvatarStackProps { + attendees: Attendee[]; + totalCount: number; + maxDisplay?: number; + size?: "sm" | "md" | "lg"; + className?: string; + renderOverflow?: (count: number) => React.ReactNode; +} + +// ============================================================================ +// Sub-components +// ============================================================================ + +function EventDetailItem({ + icon, + label, + sublabel, + className, +}: EventDetailItemProps) { + return ( +
+ {icon && ( +
{icon}
+ )} +
+ + {label} + + {sublabel && ( + + {sublabel} + + )} +
+
+ ); +} + +const avatarSizes = { + sm: "size-6", + md: "size-8", + lg: "size-10", +}; + +function AvatarStack({ + attendees, + totalCount, + maxDisplay = 3, + size = "md", + className, + renderOverflow, +}: AvatarStackProps) { + const displayedAttendees = attendees.slice(0, maxDisplay); + const remainingCount = totalCount - displayedAttendees.length; + + return ( +
+ {displayedAttendees.map((attendee, index) => ( + 0 && "-ml-2", + )} + > + {attendee.avatarUrl ? ( + + ) : ( + + {attendee.name.slice(0, 2).toUpperCase()} + + )} + + ))} + {remainingCount > 0 && + (renderOverflow ? ( + renderOverflow(remainingCount) + ) : ( +
+ +{remainingCount} +
+ ))} +
+ ); +} + +const defaultStatusConfigs: Record = { + confirmed: { + key: "confirmed", + label: "Confirmed", + className: "bg-emerald-50 text-emerald-700 border-emerald-200", + }, + pending: { + key: "pending", + label: "Pending", + className: "bg-yellow-50 text-yellow-700 border-yellow-200", + }, + cancelled: { + key: "cancelled", + label: "Cancelled", + className: "bg-red-50 text-red-700 border-red-200", + }, +}; + +function StatusBadge({ + status, + customConfigs, +}: { + status: string | BadgeConfig; + customConfigs?: Record; +}) { + if (typeof status === "object") { + if (status.hidden) return null; + return ( + + {status.label} + + ); + } + + const configs = { ...defaultStatusConfigs, ...customConfigs }; + const config = configs[status]; + + if (!config) return null; + + return ( + + {config.label} + + ); +} + +function ActionButton({ config }: { config: ActionButtonConfig }) { + if (config.hidden) return null; + + return ( + + ); +} + +// ============================================================================ +// Main Component +// ============================================================================ + +export interface HAXWorkshopCardProps extends WorkshopCardData { + className?: string; + onJoin?: () => void; + onDecline?: () => void; + onMaybe?: () => void; + onAddToCalendar?: () => void; +} + +export function HAXWorkshopCard({ + className, + title, + description, + eventType = "Online Event", + status = "confirmed", + date, + time, + duration, + location, + attendeeCount = 0, + attendees = [], + maxDisplayedAttendees = 3, + showJoinButton = true, + showDeclineButton = true, + showMaybeButton = true, + onJoin, + onDecline, + onMaybe, + onAddToCalendar, +}: HAXWorkshopCardProps) { + const eventTypeBadge: BadgeConfig = { + label: eventType, + className: "bg-violet-50 text-violet-700 border-violet-200", + }; + + const parsedAttendees: Attendee[] = attendees.map((a, idx) => ({ + id: a.id || `attendee-${idx}`, + name: a.name, + avatarUrl: a.avatarUrl, + })); + + return ( + +
+ {/* Header with badges */} +
+
+
+ +
+ + {eventTypeBadge.label} + +
+ {status && } +
+ + {/* Title and description */} +
+

+ {title} +

+ {description && ( +

+ {description} +

+ )} +
+ + {/* Event details */} +
+ {date && ( + } + label={date} + sublabel={ + onAddToCalendar ? ( + + ) : undefined + } + /> + )} + {time && ( + } + label={time} + sublabel={duration} + /> + )} + {location && ( + } + label={location} + /> + )} + {attendeeCount > 0 && ( +
+
+ +
+
+ + {attendeeCount} attendee{attendeeCount !== 1 ? "s" : ""} + + {parsedAttendees.length > 0 && ( + + )} +
+
+ )} +
+ + {/* Action buttons */} +
+ {showDeclineButton && ( + + )} + {showMaybeButton && ( + + )} + {showJoinButton && ( + + )} +
+
+
+ ); +} + +export { + EventDetailItem, + AvatarStack, + StatusBadge, + ActionButton, + defaultStatusConfigs, +}; diff --git a/hax/components/ui/avatar.tsx b/hax/components/ui/avatar.tsx new file mode 100644 index 0000000..cb9c816 --- /dev/null +++ b/hax/components/ui/avatar.tsx @@ -0,0 +1,71 @@ +"use client" + +/* + * 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 * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +function Avatar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/package.json b/package.json index cb7d864..02db782 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@copilotkit/runtime": "^1.10.1", "@copilotkit/shared": "^1.0.0", "@monaco-editor/react": "^4.7.0", + "@radix-ui/react-avatar": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-progress": "^1.1.7", From 1c51db1bda41797d389a28cce97a500f62112445 Mon Sep 17 00:00:00 2001 From: Jaswant Singh Date: Tue, 20 Jan 2026 17:28:23 +0530 Subject: [PATCH 4/4] feat: update findings artifact with component library - Simplified findings artifact to use @/components/findings - Added FindingsPanel, FindingsCard, SourceChips, SourceChip, AllSourcesPopover components - Added popover UI component - Updated registry dependencies for CLI integration - Updated description for research/factual queries Signed-off-by: Jaswant Singh --- cli/src/registry/default/artifacts.ts | 2 +- cli/src/registry/default/ui.ts | 50 ++ .../registry/github-registry/artifacts.json | 3 +- cli/src/registry/github-registry/ui.json | 19 + hax/artifacts/findings/action.ts | 58 +-- hax/artifacts/findings/description.ts | 28 +- hax/artifacts/findings/findings.tsx | 426 +----------------- hax/artifacts/findings/index.ts | 20 +- hax/artifacts/findings/types.ts | 23 +- hax/components/findings/AllSourcesPopover.tsx | 133 ++++++ hax/components/findings/FindingsCard.tsx | 129 ++++++ hax/components/findings/FindingsPanel.tsx | 165 +++++++ hax/components/findings/SourceChip.tsx | 89 ++++ hax/components/findings/SourceChips.tsx | 136 ++++++ hax/components/findings/index.ts | 25 + hax/components/ui/popover.tsx | 48 ++ 16 files changed, 840 insertions(+), 514 deletions(-) create mode 100644 hax/components/findings/AllSourcesPopover.tsx create mode 100644 hax/components/findings/FindingsCard.tsx create mode 100644 hax/components/findings/FindingsPanel.tsx create mode 100644 hax/components/findings/SourceChip.tsx create mode 100644 hax/components/findings/SourceChips.tsx create mode 100644 hax/components/findings/index.ts create mode 100644 hax/components/ui/popover.tsx diff --git a/cli/src/registry/default/artifacts.ts b/cli/src/registry/default/artifacts.ts index 0b0c784..db3e6fc 100644 --- a/cli/src/registry/default/artifacts.ts +++ b/cli/src/registry/default/artifacts.ts @@ -364,7 +364,7 @@ export const artifacts: RegistryItem[] = [ "lucide-react", "@radix-ui/react-popover", ], - registryDependencies: ["button", "popover"], + registryDependencies: ["button", "popover", "findings"], files: [ { path: "hax/artifacts/findings/findings.tsx", diff --git a/cli/src/registry/default/ui.ts b/cli/src/registry/default/ui.ts index bbb37dd..5599b3f 100644 --- a/cli/src/registry/default/ui.ts +++ b/cli/src/registry/default/ui.ts @@ -207,4 +207,54 @@ export const uiComponents: RegistryItem[] = [ }, ], }, + { + name: "popover", + type: "registry:ui", + dependencies: ["@radix-ui/react-popover", "clsx", "tailwind-merge"], + files: [ + { + path: "hax/components/ui/popover.tsx", + type: "registry:component", + content: readComponentFile("hax/components/ui/popover.tsx"), + }, + ], + }, + { + name: "findings", + type: "registry:ui", + dependencies: ["clsx", "tailwind-merge", "@radix-ui/react-popover"], + registryDependencies: ["button", "popover"], + files: [ + { + path: "hax/components/findings/index.ts", + type: "registry:index", + content: readComponentFile("hax/components/findings/index.ts"), + }, + { + path: "hax/components/findings/FindingsPanel.tsx", + type: "registry:component", + content: readComponentFile("hax/components/findings/FindingsPanel.tsx"), + }, + { + path: "hax/components/findings/FindingsCard.tsx", + type: "registry:component", + content: readComponentFile("hax/components/findings/FindingsCard.tsx"), + }, + { + path: "hax/components/findings/SourceChips.tsx", + type: "registry:component", + content: readComponentFile("hax/components/findings/SourceChips.tsx"), + }, + { + path: "hax/components/findings/SourceChip.tsx", + type: "registry:component", + content: readComponentFile("hax/components/findings/SourceChip.tsx"), + }, + { + path: "hax/components/findings/AllSourcesPopover.tsx", + type: "registry:component", + content: readComponentFile("hax/components/findings/AllSourcesPopover.tsx"), + }, + ], + }, ] diff --git a/cli/src/registry/github-registry/artifacts.json b/cli/src/registry/github-registry/artifacts.json index dc7cb30..472c6f1 100644 --- a/cli/src/registry/github-registry/artifacts.json +++ b/cli/src/registry/github-registry/artifacts.json @@ -304,7 +304,8 @@ ], "registryDependencies": [ "button", - "popover" + "popover", + "findings" ], "files": [ { diff --git a/cli/src/registry/github-registry/ui.json b/cli/src/registry/github-registry/ui.json index c36692a..f582518 100644 --- a/cli/src/registry/github-registry/ui.json +++ b/cli/src/registry/github-registry/ui.json @@ -101,5 +101,24 @@ "dependencies": ["@radix-ui/react-avatar", "clsx", "tailwind-merge"], "registryDependencies": [], "files": [{ "name": "avatar.tsx", "type": "registry:component" }] + }, + "popover": { + "type": "registry:ui", + "dependencies": ["@radix-ui/react-popover", "clsx", "tailwind-merge"], + "registryDependencies": [], + "files": [{ "name": "popover.tsx", "type": "registry:component" }] + }, + "findings": { + "type": "registry:ui", + "dependencies": ["clsx", "tailwind-merge", "@radix-ui/react-popover"], + "registryDependencies": ["button", "popover"], + "files": [ + { "name": "index.ts", "type": "registry:index", "path": "hax/components/findings/index.ts" }, + { "name": "FindingsPanel.tsx", "type": "registry:component", "path": "hax/components/findings/FindingsPanel.tsx" }, + { "name": "FindingsCard.tsx", "type": "registry:component", "path": "hax/components/findings/FindingsCard.tsx" }, + { "name": "SourceChips.tsx", "type": "registry:component", "path": "hax/components/findings/SourceChips.tsx" }, + { "name": "SourceChip.tsx", "type": "registry:component", "path": "hax/components/findings/SourceChip.tsx" }, + { "name": "AllSourcesPopover.tsx", "type": "registry:component", "path": "hax/components/findings/AllSourcesPopover.tsx" } + ] } } diff --git a/hax/artifacts/findings/action.ts b/hax/artifacts/findings/action.ts index 2f7b156..4f0c29c 100644 --- a/hax/artifacts/findings/action.ts +++ b/hax/artifacts/findings/action.ts @@ -16,20 +16,15 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { useCopilotAction } from "@copilotkit/react-core" -import { ArtifactTab } from "./types" -import { FINDINGS_DESCRIPTION } from "./description" +import { useCopilotAction } from "@copilotkit/react-core"; +import { FindingsArtifact } from "./types"; +import { FINDINGS_DESCRIPTION } from "./description"; interface UseFindingsActionProps { - addOrUpdateArtifact: ( - type: "findings", - data: Extract["data"], - ) => void + addOrUpdateArtifact: (type: "findings", data: FindingsArtifact["data"]) => void; } -export const useFindingsAction = ({ - addOrUpdateArtifact, -}: UseFindingsActionProps) => { +export const useFindingsAction = ({ addOrUpdateArtifact }: UseFindingsActionProps) => { useCopilotAction({ name: "create_findings", description: FINDINGS_DESCRIPTION, @@ -37,50 +32,23 @@ export const useFindingsAction = ({ { name: "title", type: "string", - description: "Header title for the findings panel", + description: "Panel title for the findings", required: true, }, { name: "findingsJson", type: "string", - description: - "JSON string of findings array. Each finding must have: id (unique string), title (string), description (string), and optionally sources (array of {label: string, href?: string})", + description: "JSON string of findings array: [{id, title, description, sources?: [{label, href?}]}]", required: true, }, - { - name: "sourcesLabel", - type: "string", - description: "Custom label for sources section (default: 'Sources:')", - required: false, - }, - { - name: "maxVisibleSources", - type: "number", - description: - "Maximum number of source chips to show before collapsing into '+N' (default: 2)", - required: false, - }, ], handler: async (args) => { - try { - const { title, findingsJson, sourcesLabel, maxVisibleSources } = args - - const findings = JSON.parse(findingsJson) + const { title, findingsJson } = args; + const findings = JSON.parse(findingsJson); - addOrUpdateArtifact("findings", { - title, - findings, - sourcesLabel, - maxVisibleSources, - }) + addOrUpdateArtifact("findings", { title, findings }); - return `Created findings panel "${title}" with ${findings.length} findings` - } catch (error) { - console.error("Error in create_findings handler:", error) - const errorMessage = - error instanceof Error ? error.message : "Unknown error" - return `Failed to create findings: ${errorMessage}` - } + return `Created findings panel "${title}" with ${findings.length} findings`; }, - }) -} + }); +}; diff --git a/hax/artifacts/findings/description.ts b/hax/artifacts/findings/description.ts index 8e3efd1..c3d8aa3 100644 --- a/hax/artifacts/findings/description.ts +++ b/hax/artifacts/findings/description.ts @@ -17,21 +17,21 @@ */ export const FINDINGS_DESCRIPTION = - `Use findings artifacts to present a list of key insights, recommendations, or discoveries with their supporting sources. Best for research summaries, analysis results, audit findings, security assessments, and any situation where users need to see multiple findings with source attribution. + `Use findings panels for answering factual questions, research queries, and displaying analysis results with source attribution. This is the PRIMARY artifact for questions like "Is X safe?", "What is Y?", "How does Z work?". -Structure each finding with a clear title, descriptive explanation, and optional source references. The panel displays findings in a clean, scannable format with source chips for quick reference. +PREFER findings over thinking-process for simple research/factual questions. Use thinking-process only when user explicitly asks to see reasoning steps. -Features: -- Panel header with customizable title -- Individual finding cards with title and description -- Source chips with overflow handling (shows "+N" when exceeding maxVisibleSources) -- Clean, professional styling following design system guidelines +Best for: +- Factual questions and research queries +- AI research results and analysis summaries +- Security findings and audit results +- Recommendations with supporting evidence -Best practices: -- Keep finding titles concise and actionable (under 10 words) -- Provide clear, specific descriptions that explain the finding's significance -- Include relevant sources to build credibility and enable follow-up -- Limit to 3-7 findings per panel to maintain readability -- Use consistent source labeling conventions +Include a panel title and one or more findings. Each finding should have: +- Clear title summarizing the key insight +- Detailed description with the answer/information +- Relevant sources for credibility and verification -Don't use findings for simple lists without context or single-item displays. Avoid overly long descriptions that should be broken into separate findings.` as const +Write clear, actionable finding descriptions. Include all relevant sources for each finding. Group related findings together. Keep findings focused and specific. Ensure each finding has a unique ID. + +Don't use for: Complex multi-step debugging workflows (use thinking-process), code examples (use code-editor), or simple status updates.` as const; diff --git a/hax/artifacts/findings/findings.tsx b/hax/artifacts/findings/findings.tsx index e1d39de..6183a26 100644 --- a/hax/artifacts/findings/findings.tsx +++ b/hax/artifacts/findings/findings.tsx @@ -16,431 +16,17 @@ * 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 { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; - -// ============================================================================ -// Types -// ============================================================================ - -export interface Finding { - id: string; - title: string; - description: string; - sources?: Array<{ - label: string; - href?: string; - }>; -} - -export interface FindingsPanelProps - extends React.HTMLAttributes { - title: string; - findings: Finding[]; - sourcesLabel?: string; - maxVisibleSources?: number; - onSourceClick?: (source: string, index: number, findingId: string) => void; -} - -// ============================================================================ -// SourceChip Component -// ============================================================================ - -export interface SourceChipProps extends React.HTMLAttributes { - label: string; - maxWidth?: number | string; - isCountChip?: boolean; - truncate?: boolean; -} - -export function SourceChip({ - label, - maxWidth, - isCountChip = false, - truncate = false, - className, - style, - ...props -}: SourceChipProps) { - const chipStyle: React.CSSProperties = { - ...style, - ...(maxWidth - ? { maxWidth: typeof maxWidth === "number" ? `${maxWidth}px` : maxWidth } - : {}), - }; - - const shouldTruncate = truncate || !!maxWidth; - - return ( -
- - {label} - -
- ); -} - -// ============================================================================ -// AllSourcesPopover Component -// ============================================================================ - -export interface AllSourcesPopoverProps { - sources: string[]; - children: React.ReactNode; - onSourceClick?: (source: string, index: number) => void; - open?: boolean; - onOpenChange?: (open: boolean) => void; -} - -export function AllSourcesPopover({ - sources, - children, - onSourceClick, - open, - onOpenChange, -}: AllSourcesPopoverProps) { - const [internalOpen, setInternalOpen] = React.useState(false); - - const isOpen = open !== undefined ? open : internalOpen; - const setIsOpen = onOpenChange || setInternalOpen; - - const handleClose = () => { - setIsOpen(false); - }; - - return ( - - {children} - -
- {/* Dialog Header */} -
-
-

- All Sources -

-
-
- - {/* Vertical Chips Container */} -
- {sources.map((source, index) => ( - onSourceClick?.(source, index)} - /> - ))} -
- - {/* Dialog Footer */} -
-
- -
-
-
-
-
- ); -} - -// ============================================================================ -// SourceChips Component -// ============================================================================ - -export interface SourceChipsProps extends React.HTMLAttributes { - sources: string[]; - label?: string; - maxVisible?: number; - maxChipWidth?: number | string; - onSourceClick?: (source: string, index: number) => void; -} - -export function SourceChips({ - sources, - label = "Sources:", - maxVisible, - maxChipWidth, - onSourceClick, - className, - ...props -}: SourceChipsProps) { - if (!sources || sources.length === 0) { - return null; - } - - const totalChips = sources.length; - const effectiveMaxVisible = - maxVisible !== undefined - ? Math.max(totalChips >= 2 ? 2 : 1, maxVisible) - : totalChips; - - const visibleSources = sources.slice(0, effectiveMaxVisible); - const overflowCount = totalChips - effectiveMaxVisible; - const hasOverflow = overflowCount > 0; - - return ( -
- {/* Label */} -

- {label} -

- - {/* Chips container */} -
- {visibleSources.map((source, index) => ( - - ))} - - {/* Overflow indicator chip */} - {hasOverflow && ( - - - - )} -
-
- ); -} - -// ============================================================================ -// FindingsCard Component -// ============================================================================ - -export interface FindingsCardProps extends React.HTMLAttributes { - title: string; - description: string; - sources?: string[]; - showSources?: boolean; - sourcesLabel?: string; - maxVisibleSources?: number; - onSourceClick?: (source: string, index: number) => void; -} - -export function FindingsCard({ - title, - description, - sources = [], - showSources = true, - sourcesLabel, - maxVisibleSources, - onSourceClick, - className, - ...props -}: FindingsCardProps) { - return ( -
- {/* Content section */} -
- {/* Text content */} -
- {/* Title */} -

- {title} -

- - {/* Description */} -

- {description} -

-
-
- - {/* Sources section */} - {showSources && sources.length > 0 && ( - - )} -
- ); -} - -// ============================================================================ -// FindingsPanel Component (Main Export) -// ============================================================================ - -interface FindingsPanelItemProps { - findingId: string; - title: string; - description: string; - sources?: Array<{ - label: string; - href?: string; - }>; - sourcesLabel?: string; - maxVisibleSources?: number; - onSourceClick?: (source: string, index: number, findingId: string) => void; -} - -function FindingsPanelItem({ - findingId, - title, - description, - sources, - sourcesLabel, - maxVisibleSources, - onSourceClick, -}: FindingsPanelItemProps) { - const sourceLabels = sources?.map((s) => s.label) || []; - - const handleSourceClick = onSourceClick - ? (source: string, index: number) => onSourceClick(source, index, findingId) - : undefined; - - return ( - 0} - maxVisibleSources={maxVisibleSources} - onSourceClick={handleSourceClick} - className="w-full max-w-none" - /> - ); -} - -export function FindingsPanel({ - title, - findings, - sourcesLabel, - maxVisibleSources, - onSourceClick, - className, - ...props -}: FindingsPanelProps) { - return ( -
- {/* Panel Header */} -
-

- {title} -

-
- - {/* Findings List */} - {findings.map((finding) => ( - - ))} -
- ); -} - -// ============================================================================ -// HAX Wrapper (Default Export for HAX SDK) -// ============================================================================ +import React from "react"; +import { FindingsPanel } from "@/components/findings"; +import type { FindingsArtifact } from "./types"; export interface HAXFindingsProps { - sourcesLabel?: string; - maxVisibleSources?: number; title: string; - findings: Finding[]; + findings: FindingsArtifact["data"]["findings"]; onSourceClick?: (source: string, index: number, findingId: string) => void; } export function HAXFindings({ - sourcesLabel, - maxVisibleSources, title, findings, onSourceClick, @@ -451,11 +37,7 @@ export function HAXFindings({ title={title} findings={findings} onSourceClick={onSourceClick} - sourcesLabel={sourcesLabel} - maxVisibleSources={maxVisibleSources} />
); } - -export default HAXFindings; diff --git a/hax/artifacts/findings/index.ts b/hax/artifacts/findings/index.ts index ec230da..e44c2b0 100644 --- a/hax/artifacts/findings/index.ts +++ b/hax/artifacts/findings/index.ts @@ -16,23 +16,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -export { - HAXFindings, - FindingsPanel, - FindingsCard, - SourceChips, - SourceChip, - AllSourcesPopover, -} from "./findings"; -export type { - Finding, - FindingsPanelProps, - FindingsCardProps, - SourceChipsProps, - SourceChipProps, - AllSourcesPopoverProps, - HAXFindingsProps, -} from "./findings"; +export { HAXFindings } from "./findings"; export { useFindingsAction } from "./action"; -export type { FindingsArtifact, Source } from "./types"; +export type { FindingsArtifact, Finding, Source } from "./types"; export { FindingsArtifactZod, FindingZod, SourceZod } from "./types"; diff --git a/hax/artifacts/findings/types.ts b/hax/artifacts/findings/types.ts index 5ae467f..156c547 100644 --- a/hax/artifacts/findings/types.ts +++ b/hax/artifacts/findings/types.ts @@ -16,19 +16,19 @@ * SPDX-License-Identifier: Apache-2.0 */ -import z from "zod" +import { z } from "zod"; -const FindingSourceZod = z.object({ +export const SourceZod = z.object({ label: z.string(), href: z.string().optional(), -}) +}); -const FindingZod = z.object({ +export const FindingZod = z.object({ id: z.string(), title: z.string(), description: z.string(), - sources: z.array(FindingSourceZod).optional(), -}) + sources: z.array(SourceZod).optional(), +}); export const FindingsArtifactZod = z.object({ id: z.string(), @@ -36,12 +36,9 @@ export const FindingsArtifactZod = z.object({ data: z.object({ title: z.string(), findings: z.array(FindingZod), - sourcesLabel: z.string().optional(), - maxVisibleSources: z.number().optional(), }), -}) +}); -export type FindingsArtifact = z.infer - -export const ArtifactTabZod = z.discriminatedUnion("type", [FindingsArtifactZod]) -export type ArtifactTab = z.infer +export type FindingsArtifact = z.infer; +export type Finding = z.infer; +export type Source = z.infer; diff --git a/hax/components/findings/AllSourcesPopover.tsx b/hax/components/findings/AllSourcesPopover.tsx new file mode 100644 index 0000000..f671d7d --- /dev/null +++ b/hax/components/findings/AllSourcesPopover.tsx @@ -0,0 +1,133 @@ +"use client"; + +import * as React from "react"; +import { cn } from "@/lib/utils"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Button } from "@/components/ui/button"; +import { SourceChip } from "./SourceChip"; + +export interface AllSourcesPopoverProps { + /** All source labels to display in the popover */ + sources: string[]; + /** The trigger element (typically the +N chip) */ + children: React.ReactNode; + /** Callback when a source chip is clicked */ + onSourceClick?: (source: string, index: number) => void; + /** Whether the popover is open (controlled mode) */ + open?: boolean; + /** Callback when open state changes (controlled mode) */ + onOpenChange?: (open: boolean) => void; +} + +/** + * AllSourcesPopover - A popover component that displays all source citations + * + * Based on Figma design (node 655:4736): + * - Dialog with white background and border + * - Header with "All Sources" title + * - Vertical list of source chips + * - Cancel button at the footer + * + * Uses Figma design system variables: + * - Background: general/background (white) + * - Border: general/border (#e2e8f0) + * - Border radius: semantic/rounded-xl (12px) + * - Shadow: lg shadow + */ +export function AllSourcesPopover({ + sources, + children, + onSourceClick, + open, + onOpenChange, +}: AllSourcesPopoverProps) { + const [internalOpen, setInternalOpen] = React.useState(false); + + // Support both controlled and uncontrolled modes + const isOpen = open !== undefined ? open : internalOpen; + const setIsOpen = onOpenChange || setInternalOpen; + + const handleClose = () => { + setIsOpen(false); + }; + + return ( + + {children} + +
+ {/* Dialog Header */} +
+
+

+ All Sources +

+
+
+ + {/* Vertical Chips Container */} +
+ {sources.map((source, index) => ( + onSourceClick?.(source, index)} + /> + ))} +
+ + {/* Dialog Footer */} +
+
+ +
+
+
+
+
+ ); +} + +export default AllSourcesPopover; diff --git a/hax/components/findings/FindingsCard.tsx b/hax/components/findings/FindingsCard.tsx new file mode 100644 index 0000000..d99107e --- /dev/null +++ b/hax/components/findings/FindingsCard.tsx @@ -0,0 +1,129 @@ +"use client"; + +import * as React from "react"; +import { cn } from "@/lib/utils"; +import { SourceChips } from "./SourceChips"; + +export interface FindingsCardProps + extends React.HTMLAttributes { + /** Title text displayed in bold */ + title: string; + /** Description text displayed below the title */ + description: string; + /** Array of source labels to display as chips */ + sources?: string[]; + /** Whether to show the sources section */ + showSources?: boolean; + /** Custom sources label */ + sourcesLabel?: string; + /** Maximum number of chips to show before collapsing into "+N" */ + maxVisibleSources?: number; + /** Callback when a source chip is clicked in the popover */ + onSourceClick?: (source: string, index: number) => void; +} + +/** + * FindingsCard - A card component for displaying findings with optional source chips + * + * Uses Figma design system variables: + * - Background: card/card (#ffffff) + * - Border: general/border (#e2e8f0) + * - Border radius: semantic/rounded-lg (8px) + * - Padding: semantic/md (16px) + * - Gap: semantic/md (16px) + * - Title: paragraph/small bold (14px, semibold) + * - Description: paragraph/small regular (14px, regular) + */ +export function FindingsCard({ + title, + description, + sources = [], + showSources = true, + sourcesLabel, + maxVisibleSources, + onSourceClick, + className, + ...props +}: FindingsCardProps) { + return ( +
+ {/* Content section */} +
+ {/* Text content */} +
+ {/* Title */} +

+ {title} +

+ + {/* Description */} +

+ {description} +

+
+
+ + {/* Sources section - single line with overflow */} + {showSources && sources.length > 0 && ( + + )} +
+ ); +} + +export default FindingsCard; diff --git a/hax/components/findings/FindingsPanel.tsx b/hax/components/findings/FindingsPanel.tsx new file mode 100644 index 0000000..5b1f045 --- /dev/null +++ b/hax/components/findings/FindingsPanel.tsx @@ -0,0 +1,165 @@ +"use client"; + +import * as React from "react"; +import { cn } from "@/lib/utils"; +import { FindingsCard } from "./FindingsCard"; + +export interface Finding { + /** Unique identifier for the finding */ + id: string; + /** Title of the finding */ + title: string; + /** Description/recommendation about the finding */ + description: string; + /** Array of source labels with optional links */ + sources?: Array<{ + label: string; + href?: string; + }>; +} + +export interface FindingsPanelProps + extends React.HTMLAttributes { + /** Header title for the panel */ + title: string; + /** Array of findings to display */ + findings: Finding[]; + /** Custom sources label (default: "Sources:") */ + sourcesLabel?: string; + /** Maximum number of source chips to show before collapsing into "+N" */ + maxVisibleSources?: number; + /** Callback when a source chip is clicked in the popover */ + onSourceClick?: (source: string, index: number, findingId: string) => void; +} + +/** + * FindingsPanel - A container component for displaying multiple findings + * + * Uses Figma design system variables: + * - Background: card/card (#ffffff) + * - Border: general/border (#e2e8f0) + * - Border radius: semantic/rounded-lg (8px) + * - Padding: 24px (panel padding) + * - Gap between items: semantic/xs (8px) + * - Shadow: sm shadow + * - Header: paragraph/regular bold (16px, semibold, neutral-500) + */ +export function FindingsPanel({ + title, + findings, + sourcesLabel, + maxVisibleSources, + onSourceClick, + className, + ...props +}: FindingsPanelProps) { + return ( +
+ {/* Panel Header */} +
+

+ {title} +

+
+ + {/* Findings List */} + {findings.map((finding) => ( + + ))} +
+ ); +} + +/** + * FindingsPanelItem - Individual finding item within the panel + * This is an internal component that wraps FindingsCard with panel-specific styling + */ +interface FindingsPanelItemProps { + findingId: string; + title: string; + description: string; + sources?: Array<{ + label: string; + href?: string; + }>; + sourcesLabel?: string; + maxVisibleSources?: number; + onSourceClick?: (source: string, index: number, findingId: string) => void; +} + +function FindingsPanelItem({ + findingId, + title, + description, + sources, + sourcesLabel, + maxVisibleSources, + onSourceClick, +}: FindingsPanelItemProps) { + // Convert sources with links to simple strings for FindingsCard + // The SourceChip component can be enhanced later to support links + const sourceLabels = sources?.map((s) => s.label) || []; + + // Wrap the onSourceClick to include findingId + const handleSourceClick = onSourceClick + ? (source: string, index: number) => onSourceClick(source, index, findingId) + : undefined; + + return ( + 0} + maxVisibleSources={maxVisibleSources} + onSourceClick={handleSourceClick} + className="w-full max-w-none" + /> + ); +} + +export default FindingsPanel; diff --git a/hax/components/findings/SourceChip.tsx b/hax/components/findings/SourceChip.tsx new file mode 100644 index 0000000..ac732e0 --- /dev/null +++ b/hax/components/findings/SourceChip.tsx @@ -0,0 +1,89 @@ +"use client"; + +import * as React from "react"; +import { cn } from "@/lib/utils"; + +export interface SourceChipProps extends React.HTMLAttributes { + /** The text label to display in the chip */ + label: string; + /** Maximum width for the chip - text will truncate with ellipsis if exceeded */ + maxWidth?: number | string; + /** Whether this is a count chip (e.g., "+3") - uses slightly different styling */ + isCountChip?: boolean; + /** Whether to allow the chip to shrink and truncate text when container is constrained */ + truncate?: boolean; +} + +/** + * SourceChip - An atomic chip component for displaying source labels + * + * Uses Figma design system variables: + * - Font: paragraph/mini (12px, 16px line-height, semibold) + * - Border: unofficial/border-3 (#cbd5e1) + * - Background: unofficial/outline (rgba(255,255,255,0.1)) + * - Border radius: semantic/rounded-lg (8px) + * - Shadow: xs shadow + * + * Features: + * - Truncation support: Set maxWidth to truncate long labels with ellipsis + * - Count chip variant: For "+N" overflow indicators + */ +export function SourceChip({ + label, + maxWidth, + isCountChip = false, + truncate = false, + className, + style, + ...props +}: SourceChipProps) { + const chipStyle: React.CSSProperties = { + ...style, + ...(maxWidth ? { maxWidth: typeof maxWidth === "number" ? `${maxWidth}px` : maxWidth } : {}), + }; + + // Determine if truncation should be applied + const shouldTruncate = truncate || !!maxWidth; + + return ( +
+ + {label} + +
+ ); +} + +export default SourceChip; diff --git a/hax/components/findings/SourceChips.tsx b/hax/components/findings/SourceChips.tsx new file mode 100644 index 0000000..1e0561f --- /dev/null +++ b/hax/components/findings/SourceChips.tsx @@ -0,0 +1,136 @@ +"use client"; + +import * as React from "react"; +import { cn } from "@/lib/utils"; +import { SourceChip } from "./SourceChip"; +import { AllSourcesPopover } from "./AllSourcesPopover"; + +export interface SourceChipsProps extends React.HTMLAttributes { + /** Array of source labels to display as chips */ + sources: string[]; + /** Label shown before the chips (default: "Sources:") */ + label?: string; + /** + * Maximum number of chips to show before collapsing into "+N" + * - If not set, all chips are shown + * - Minimum visible is 2 when sources.length >= 2 + */ + maxVisible?: number; + /** + * Maximum width for individual chips (truncates with ellipsis) + * - Only applies when there's a single chip + * - When there are 2+ chips, they show fully (no truncation on individual chips) + */ + maxChipWidth?: number | string; + /** Callback when a source chip is clicked in the popover */ + onSourceClick?: (source: string, index: number) => void; +} + +/** + * SourceChips - A container component that displays a list of source chips + * + * Uses Figma design system variables: + * - Label typography: paragraph/mini (12px, regular) + * - Label color: general/muted-foreground (#64748b) + * - Gap between label and chips: 16px + * - Gap between chips: semantic/xs (8px) + * + * Features: + * - Single line layout: Chips are always displayed on one line + * - Overflow handling: Shows "+N" chip when chips exceed maxVisible + * - Truncation: Single long chip can be truncated with maxChipWidth + * - Always shows at least 2 chips fully when sources.length >= 2 + */ +export function SourceChips({ + sources, + label = "Sources:", + maxVisible, + maxChipWidth, + onSourceClick, + className, + ...props +}: SourceChipsProps) { + if (!sources || sources.length === 0) { + return null; + } + + // Calculate visible chips and overflow count + const totalChips = sources.length; + + // Ensure we show at least 2 chips when there are 2+ sources + const effectiveMaxVisible = maxVisible !== undefined + ? Math.max(totalChips >= 2 ? 2 : 1, maxVisible) + : totalChips; + + const visibleSources = sources.slice(0, effectiveMaxVisible); + const overflowCount = totalChips - effectiveMaxVisible; + const hasOverflow = overflowCount > 0; + + return ( +
+ {/* Label */} +

+ {label} +

+ + {/* Chips container */} +
+ {visibleSources.map((source, index) => ( + + ))} + + {/* Overflow indicator chip - clickable to show all sources */} + {hasOverflow && ( + + + + )} +
+
+ ); +} + +export default SourceChips; diff --git a/hax/components/findings/index.ts b/hax/components/findings/index.ts new file mode 100644 index 0000000..0031db1 --- /dev/null +++ b/hax/components/findings/index.ts @@ -0,0 +1,25 @@ +/** + * Findings Components + * + * A set of reusable components for displaying findings with source chips. + * These components use Tailwind CSS for styling. + * + * Usage: + * Import the components you need: + * import { FindingsPanel, FindingsCard, SourceChips, SourceChip } from "@/components/findings"; + */ + +export { SourceChip } from "./SourceChip"; +export type { SourceChipProps } from "./SourceChip"; + +export { SourceChips } from "./SourceChips"; +export type { SourceChipsProps } from "./SourceChips"; + +export { AllSourcesPopover } from "./AllSourcesPopover"; +export type { AllSourcesPopoverProps } from "./AllSourcesPopover"; + +export { FindingsCard } from "./FindingsCard"; +export type { FindingsCardProps } from "./FindingsCard"; + +export { FindingsPanel } from "./FindingsPanel"; +export type { FindingsPanelProps, Finding } from "./FindingsPanel"; diff --git a/hax/components/ui/popover.tsx b/hax/components/ui/popover.tsx new file mode 100644 index 0000000..01e468b --- /dev/null +++ b/hax/components/ui/popover.tsx @@ -0,0 +1,48 @@ +"use client" + +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "@/lib/utils" + +function Popover({ + ...props +}: React.ComponentProps) { + return +} + +function PopoverTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function PopoverContent({ + className, + align = "center", + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function PopoverAnchor({ + ...props +}: React.ComponentProps) { + return +} + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }