ai-design-system 0.1.73 → 0.1.74

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/components/ai-elements/node.tsx +9 -9
  2. package/components/ai-elements/prompt-input.tsx +2 -2
  3. package/components/blocks/AIConversation/AIConversation.tsx +36 -3
  4. package/components/blocks/BmcCanvas/bmc-layout.ts +115 -0
  5. package/components/blocks/BmcCanvas/bmc-tooltips.ts +63 -0
  6. package/components/blocks/BmcCanvas/index.ts +3 -0
  7. package/components/blocks/BmcCanvas/interfaces.ts +58 -0
  8. package/components/blocks/WorkflowCanvas/WorkflowCanvas.tsx +1 -1
  9. package/components/blocks/index.ts +3 -0
  10. package/components/composites/AgentIndicator/AgentIndicator.tsx +1 -1
  11. package/components/composites/SpatialContainerNode/SpatialContainerNode.tsx +84 -21
  12. package/components/composites/SpecialistMessage/SpecialistMessage.tsx +1 -1
  13. package/components/composites/SystemMessage/SystemMessage.stories.tsx +46 -0
  14. package/components/composites/SystemMessage/SystemMessage.tsx +54 -0
  15. package/components/composites/SystemMessage/index.ts +2 -0
  16. package/components/composites/ToolCallDisplay/ToolCallDisplay.tsx +1 -1
  17. package/components/composites/WorkflowToolbar/WorkflowToolbar.tsx +8 -9
  18. package/components/composites/WorkflowToolbar/interfaces.ts +1 -0
  19. package/components/composites/index.ts +4 -0
  20. package/components/features/ChatPanel/ChatPanel.tsx +18 -2
  21. package/components/features/ChatPanel/useChatPanel.d.ts +1 -1
  22. package/components/features/NodeEditor/NodeEditor.mocks.ts +51 -123
  23. package/components/features/NodeEditor/NodeEditor.stories.tsx +1 -1
  24. package/components/features/NodeEditor/NodeEditor.tsx +13 -6
  25. package/components/features/NodeEditor/useNodeEditor.d.ts +1 -0
  26. package/components/features/NodeEditor/useNodeEditor.mock.ts +5 -0
  27. package/components/index.ts +1 -1
  28. package/components/ui/button.tsx +2 -0
  29. package/dist/index.cjs +285 -44
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.css +202 -23
  32. package/dist/index.d.ts +117 -21
  33. package/dist/index.js +285 -45
  34. package/dist/index.js.map +1 -1
  35. package/package.json +1 -1
@@ -23,7 +23,7 @@ export type NodeProps = ComponentProps<typeof Card> & {
23
23
  export const Node = ({ handles, className, status, ...props }: NodeProps) => (
24
24
  <Card
25
25
  className={cn(
26
- "node-container relative h-[52px] w-[180px] gap-0 overflow-hidden rounded-md bg-card p-0 transition-all duration-200",
26
+ "node-container group relative h-[52px] w-[180px] gap-0 overflow-hidden rounded-md bg-card p-0 transition-all duration-200",
27
27
  status === "success" && "border-green-500 border-2",
28
28
  status === "error" && "border-red-500 border-2",
29
29
  className
@@ -33,18 +33,18 @@ export const Node = ({ handles, className, status, ...props }: NodeProps) => (
33
33
  {status === "running" && <AnimatedBorder />}
34
34
  {handles.target && (
35
35
  <>
36
- <Handle id="target-top" position={Position.Top} type="target" />
37
- <Handle id="target-right" position={Position.Right} type="target" />
38
- <Handle id="target-bottom" position={Position.Bottom} type="target" />
39
- <Handle id="target-left" position={Position.Left} type="target" />
36
+ <Handle id="target-top" position={Position.Top} type="target" className="opacity-0 group-hover:opacity-100 group-[.selected]:opacity-100 transition-opacity duration-150" />
37
+ <Handle id="target-right" position={Position.Right} type="target" className="opacity-0 group-hover:opacity-100 group-[.selected]:opacity-100 transition-opacity duration-150" />
38
+ <Handle id="target-bottom" position={Position.Bottom} type="target" className="opacity-0 group-hover:opacity-100 group-[.selected]:opacity-100 transition-opacity duration-150" />
39
+ <Handle id="target-left" position={Position.Left} type="target" className="opacity-0 group-hover:opacity-100 group-[.selected]:opacity-100 transition-opacity duration-150" />
40
40
  </>
41
41
  )}
42
42
  {handles.source && (
43
43
  <>
44
- <Handle id="source-top" position={Position.Top} type="source" />
45
- <Handle id="source-right" position={Position.Right} type="source" />
46
- <Handle id="source-bottom" position={Position.Bottom} type="source" />
47
- <Handle id="source-left" position={Position.Left} type="source" />
44
+ <Handle id="source-top" position={Position.Top} type="source" className="opacity-0 group-hover:opacity-100 group-[.selected]:opacity-100 transition-opacity duration-150" />
45
+ <Handle id="source-right" position={Position.Right} type="source" className="opacity-0 group-hover:opacity-100 group-[.selected]:opacity-100 transition-opacity duration-150" />
46
+ <Handle id="source-bottom" position={Position.Bottom} type="source" className="opacity-0 group-hover:opacity-100 group-[.selected]:opacity-100 transition-opacity duration-150" />
47
+ <Handle id="source-left" position={Position.Left} type="source" className="opacity-0 group-hover:opacity-100 group-[.selected]:opacity-100 transition-opacity duration-150" />
48
48
  </>
49
49
  )}
50
50
  {props.children}
@@ -969,8 +969,8 @@ export const PromptInputSubmit = ({
969
969
  if (status === "submitted") {
970
970
  Icon = <Loader2Icon className="size-4 animate-spin" />;
971
971
  } else if (status === "streaming") {
972
- Icon = <SquareIcon className="size-3.5 fill-current" />;
973
- buttonVariant = "default";
972
+ Icon = <SquareIcon className="size-3.5 fill-white text-white" />;
973
+ buttonVariant = "outline-primary";
974
974
  customClass = cn("rounded-full", className);
975
975
  } else if (status === "error") {
976
976
  Icon = <XIcon className="size-4" />;
@@ -10,6 +10,7 @@ import type { ToolCall, SubAgent } from "@/components/composites"
10
10
  import { UserMessage } from "@/components/composites/UserMessage"
11
11
  import { SpecialistMessage } from "@/components/composites/SpecialistMessage"
12
12
  import { OrchestratorMessage } from "@/components/composites/OrchestratorMessage"
13
+ import { SystemMessage } from "@/components/composites/SystemMessage"
13
14
  import { ToolCallDisplay } from "@/components/composites/ToolCallDisplay"
14
15
  import { ReasoningDisplay } from "@/components/composites/ReasoningDisplay"
15
16
 
@@ -71,6 +72,11 @@ export interface AIConversationProps
71
72
  * Callback fired when a tool action (like a link click) is triggered
72
73
  */
73
74
  onToolAction?: (toolCall: ToolCall, action: string) => void
75
+ /**
76
+ * Custom renderer for system messages.
77
+ * Receives the raw content string and returns a ReactNode.
78
+ */
79
+ renderSystemMessage?: (content: string) => React.ReactNode
74
80
  }
75
81
 
76
82
  /**
@@ -84,6 +90,7 @@ export const AIConversation = React.memo<AIConversationProps>(
84
90
  selectedSubAgent,
85
91
  emptyState,
86
92
  onToolAction,
93
+ renderSystemMessage,
87
94
  ...conversationProps
88
95
  }) => {
89
96
  const isEmpty = React.useMemo(
@@ -117,6 +124,15 @@ export const AIConversation = React.memo<AIConversationProps>(
117
124
  (!msg.toolCalls || msg.toolCalls.length === 0) &&
118
125
  (!msg.subAgents || msg.subAgents.length === 0);
119
126
 
127
+ const currentGroupHasTools =
128
+ currentGroup &&
129
+ ((currentGroup.toolCalls && currentGroup.toolCalls.length > 0) ||
130
+ (currentGroup.subAgents && currentGroup.subAgents.length > 0));
131
+
132
+ if (currentGroupHasTools && isFinalResponse) {
133
+ currentGroup = null;
134
+ }
135
+
120
136
  if (currentGroup) {
121
137
  if (msg.content) {
122
138
  currentGroup.content = currentGroup.content
@@ -221,6 +237,23 @@ export const AIConversation = React.memo<AIConversationProps>(
221
237
  const contentStr = toMessageString(message.content)
222
238
 
223
239
  // Render based on role field
240
+ if (message.role === "system") {
241
+ if (!contentStr || !contentStr.trim()) {
242
+ return null;
243
+ }
244
+ return (
245
+ <SystemMessage
246
+ key={message.id}
247
+ message={{
248
+ id: message.id,
249
+ content: contentStr,
250
+ avatarName: message.avatarName,
251
+ }}
252
+ renderContent={renderSystemMessage}
253
+ />
254
+ )
255
+ }
256
+
224
257
  if (message.role === "user") {
225
258
  return (
226
259
  <UserMessage
@@ -252,9 +285,9 @@ export const AIConversation = React.memo<AIConversationProps>(
252
285
  const directToolCalls = allToolCalls.filter((tc) => tc.visibility !== "reasoning")
253
286
 
254
287
  const hasReasoning = reasoningCalls.length > 0 || subAgents.length > 0 || directToolCalls.length > 0 || (message.isLoading && contentStr.trim() !== "");
255
- const reasoningText = (hasReasoning && (!message.blocks || message.blocks.length === 0)) ? contentStr : undefined
256
- const displayContentStr = hasReasoning ? "" : contentStr
257
- const hasDisplayContent = displayContentStr.trim() !== ""
288
+ const reasoningText = (hasReasoning && (!message.blocks || message.blocks.length === 0)) ? contentStr : undefined;
289
+ const displayContentStr = hasReasoning ? "" : contentStr;
290
+ const hasDisplayContent = displayContentStr.trim() !== "";
258
291
 
259
292
  if (!hasDisplayContent && directToolCalls.length === 0 && reasoningCalls.length === 0 && subAgents.length === 0 && !message.isLoading && (!message.blocks || message.blocks.length === 0)) {
260
293
  return null;
@@ -0,0 +1,115 @@
1
+ import type { WorkflowNode, WorkflowEdge } from "@/components/blocks/WorkflowCanvas";
2
+ import type { SpatialContainerColorTheme, SpatialContainerVariant } from "@/components/composites/SpatialContainerNode";
3
+ import type { BmcBlock, BmcBlockId, BmcCanvasView, CompiledBmc } from "./interfaces";
4
+
5
+ interface BlockLayout {
6
+ id: string;
7
+ themeColor: SpatialContainerColorTheme;
8
+ variant: SpatialContainerVariant;
9
+ icon: string;
10
+ position: { x: number; y: number };
11
+ size: { width: number; height: number };
12
+ layout: 'column' | 'row';
13
+ }
14
+
15
+ const BLOCK_LAYOUTS: Record<BmcBlockId, BlockLayout> = {
16
+ key_partnerships: { id: 'cp', themeColor: 'blue', variant: 'extruded3d', icon: 'handshake', position: { x: 40, y: 40 }, size: { width: 280, height: 400 }, layout: 'column' },
17
+ key_activities: { id: 'ka', themeColor: 'emerald', variant: 'extruded3d', icon: 'activity', position: { x: 350, y: 40 }, size: { width: 280, height: 190 }, layout: 'column' },
18
+ key_resources: { id: 'kr', themeColor: 'emerald', variant: 'extruded3d', icon: 'database', position: { x: 350, y: 250 }, size: { width: 280, height: 190 }, layout: 'column' },
19
+ value_propositions: { id: 'vp', themeColor: 'indigo', variant: 'extruded3d', icon: 'box', position: { x: 660, y: 40 }, size: { width: 300, height: 400 }, layout: 'column' },
20
+ customer_relationships: { id: 'cr', themeColor: 'purple', variant: 'extruded3d', icon: 'heart', position: { x: 990, y: 40 }, size: { width: 300, height: 190 }, layout: 'column' },
21
+ channels: { id: 'ch', themeColor: 'amber', variant: 'extruded3d', icon: 'truck', position: { x: 990, y: 250 }, size: { width: 300, height: 190 }, layout: 'column' },
22
+ customer_segments: { id: 'cs', themeColor: 'purple', variant: 'extruded3d', icon: 'users', position: { x: 1320, y: 40 }, size: { width: 280, height: 400 }, layout: 'column' },
23
+ cost_structure: { id: 'cost', themeColor: 'rose', variant: 'extruded3d', icon: 'wallet', position: { x: 40, y: 465 }, size: { width: 760, height: 180 }, layout: 'row' },
24
+ revenue_streams: { id: 'rev', themeColor: 'cyan', variant: 'extruded3d', icon: 'wallet', position: { x: 840, y: 465 }, size: { width: 760, height: 180 }, layout: 'row' },
25
+ };
26
+
27
+ function blockEntries(bmc: CompiledBmc): Array<{ block: BmcBlock; layout: BlockLayout }> {
28
+ return [
29
+ { block: bmc.keyPartnerships, layout: BLOCK_LAYOUTS.key_partnerships },
30
+ { block: bmc.keyActivities, layout: BLOCK_LAYOUTS.key_activities },
31
+ { block: bmc.keyResources, layout: BLOCK_LAYOUTS.key_resources },
32
+ { block: bmc.valuePropositions, layout: BLOCK_LAYOUTS.value_propositions },
33
+ { block: bmc.customerRelationships, layout: BLOCK_LAYOUTS.customer_relationships },
34
+ { block: bmc.channels, layout: BLOCK_LAYOUTS.channels },
35
+ { block: bmc.customerSegments, layout: BLOCK_LAYOUTS.customer_segments },
36
+ { block: bmc.costStructure, layout: BLOCK_LAYOUTS.cost_structure },
37
+ { block: bmc.revenueStreams, layout: BLOCK_LAYOUTS.revenue_streams },
38
+ ];
39
+ }
40
+
41
+ import { BMC_TOOLTIPS } from "./bmc-tooltips";
42
+
43
+ export function bmcToCanvas(bmc: CompiledBmc | null, message?: string): BmcCanvasView {
44
+ if (!bmc) {
45
+ return {
46
+ canvasName: null,
47
+ nodes: [],
48
+ edges: [],
49
+ emptyBlocks: [],
50
+ message: message ?? 'No business model data available for this session',
51
+ };
52
+ }
53
+
54
+ const nodes: WorkflowNode[] = [];
55
+
56
+ for (const { block, layout } of blockEntries(bmc)) {
57
+ const itemCount = block.items.length;
58
+ const badgeText = block.empty || itemCount === 0 ? 'empty' : `${itemCount} item${itemCount === 1 ? '' : 's'}`;
59
+
60
+ const items = block.items.map((item, idx) => ({
61
+ id: `${layout.id}-${idx + 1}`,
62
+ label: item.name,
63
+ description: item.description,
64
+ }));
65
+
66
+ const blockId = layout.id === 'cp' ? 'key_partnerships'
67
+ : layout.id === 'ka' ? 'key_activities'
68
+ : layout.id === 'kr' ? 'key_resources'
69
+ : layout.id === 'vp' ? 'value_propositions'
70
+ : layout.id === 'cr' ? 'customer_relationships'
71
+ : layout.id === 'ch' ? 'channels'
72
+ : layout.id === 'cs' ? 'customer_segments'
73
+ : layout.id === 'cost' ? 'cost_structure'
74
+ : layout.id === 'rev' ? 'revenue_streams'
75
+ : '';
76
+
77
+ const tooltipInfo = BMC_TOOLTIPS[blockId];
78
+ const description = tooltipInfo
79
+ ? `${tooltipInfo.description}\n\nKey Question: "${tooltipInfo.question}"\n\nExamples: ${tooltipInfo.examples.join(', ')}`
80
+ : block.name;
81
+
82
+ nodes.push({
83
+ id: layout.id,
84
+ type: 'spatialContainer',
85
+ position: layout.position,
86
+ style: layout.size,
87
+ data: {
88
+ label: block.name,
89
+ description,
90
+ type: 'spatialContainer',
91
+ themeColor: layout.themeColor,
92
+ variant: layout.variant,
93
+ icon: layout.icon,
94
+ badgeText,
95
+ items,
96
+ },
97
+ });
98
+ }
99
+
100
+ const edges: WorkflowEdge[] = [
101
+ { id: 'e-ka-vp', source: 'ka', target: 'vp', type: 'animated' },
102
+ { id: 'e-vp-cr', source: 'vp', target: 'cr', type: 'animated' },
103
+ { id: 'e-cr-cs', source: 'cr', target: 'cs', type: 'animated' },
104
+ { id: 'e-vp-ch', source: 'vp', target: 'ch', type: 'straight' },
105
+ { id: 'e-ch-cs', source: 'ch', target: 'cs', type: 'animated' },
106
+ ];
107
+
108
+ return {
109
+ canvasName: bmc.canvas.name,
110
+ nodes,
111
+ edges,
112
+ emptyBlocks: bmc.emptyBlocks,
113
+ message,
114
+ };
115
+ }
@@ -0,0 +1,63 @@
1
+ export interface BmcTooltipContent {
2
+ title: string;
3
+ description: string;
4
+ question: string;
5
+ examples: string[];
6
+ }
7
+
8
+ export const BMC_TOOLTIPS: Record<string, BmcTooltipContent> = {
9
+ key_partnerships: {
10
+ title: "Key Partnerships",
11
+ description: "The external companies, suppliers, or vendors that help your business operate and grow.",
12
+ question: "Who do we rely on outside our company to make this business work?",
13
+ examples: ["Cloud hosting providers", "Payment gateways", "Material suppliers", "Strategic alliances"]
14
+ },
15
+ key_activities: {
16
+ title: "Key Activities",
17
+ description: "The most critical daily tasks and operations your team must execute to deliver your offer.",
18
+ question: "What core things must we do every day to operate and succeed?",
19
+ examples: ["Software development", "AI model training", "Content creation", "Customer support"]
20
+ },
21
+ key_resources: {
22
+ title: "Key Resources",
23
+ description: "The most vital assets—physical, intellectual, human, or financial—needed to run your business.",
24
+ question: "What essential assets do we need to own or access to deliver our value?",
25
+ examples: ["Skilled engineers", "Proprietary algorithms", "Brand identity", "Capital"]
26
+ },
27
+ value_propositions: {
28
+ title: "Value Propositions",
29
+ description: "The collection of products, services, and benefits that solve customer problems or satisfy their needs.",
30
+ question: "Why do customers choose us over competitors? What problem are we solving?",
31
+ examples: ["Time savings", "Cost reduction", "Ease of use", "Premium quality"]
32
+ },
33
+ customer_relationships: {
34
+ title: "Customer Relationships",
35
+ description: "The type of interaction and ongoing relationship you establish with each customer group.",
36
+ question: "How do we interact with our customers across their lifecycle?",
37
+ examples: ["Self-service onboarding", "Dedicated account management", "Automated support"]
38
+ },
39
+ channels: {
40
+ title: "Channels",
41
+ description: "The touchpoints and pathways you use to communicate with, sell to, and deliver value to customers.",
42
+ question: "How do customers find, evaluate, buy, and receive our product or service?",
43
+ examples: ["Direct web platform", "Sales team", "Mobile app", "Partner marketplaces"]
44
+ },
45
+ customer_segments: {
46
+ title: "Customer Segments",
47
+ description: "The specific groups of people or organizations your business aims to reach and serve.",
48
+ question: "Who are we creating value for? Who is our ideal buyer?",
49
+ examples: ["Enterprise B2B teams", "Thought leaders", "Small business owners"]
50
+ },
51
+ cost_structure: {
52
+ title: "Cost Structure",
53
+ description: "All major fixed and variable expenses required to operate your business model.",
54
+ question: "What are the biggest costs inherent in running our business?",
55
+ examples: ["Server/compute costs", "Team salaries", "R&D licensing", "Marketing"]
56
+ },
57
+ revenue_streams: {
58
+ title: "Revenue Streams",
59
+ description: "The different ways your business earns cash and captures value from each customer segment.",
60
+ question: "How does money flow into the business? What are customers paying for?",
61
+ examples: ["Monthly SaaS subscriptions", "Usage-based fees", "One-time setup charges"]
62
+ }
63
+ };
@@ -0,0 +1,3 @@
1
+ export { bmcToCanvas } from "./bmc-layout";
2
+ export { BMC_TOOLTIPS, type BmcTooltipContent } from "./bmc-tooltips";
3
+ export type { BmcBlockId, BmcItem, BmcBlock, CompiledBmc, BmcCanvasView } from "./interfaces";
@@ -0,0 +1,58 @@
1
+ import type { WorkflowEdge, WorkflowNode } from "@/components/blocks/WorkflowCanvas";
2
+
3
+ export type BmcBlockId =
4
+ | 'customer_segments'
5
+ | 'value_propositions'
6
+ | 'channels'
7
+ | 'customer_relationships'
8
+ | 'revenue_streams'
9
+ | 'key_resources'
10
+ | 'key_activities'
11
+ | 'key_partnerships'
12
+ | 'cost_structure';
13
+
14
+ export interface BmcItem {
15
+ id: string;
16
+ name: string;
17
+ description?: string;
18
+ sourceRefs: string[];
19
+ [k: string]: unknown;
20
+ }
21
+
22
+ export interface BmcBlock {
23
+ key: BmcBlockId;
24
+ name: string;
25
+ items: BmcItem[];
26
+ empty: boolean;
27
+ derived: boolean;
28
+ sourceRefs: string[];
29
+ }
30
+
31
+ export interface CompiledBmc {
32
+ canvas: {
33
+ id: string;
34
+ name: string;
35
+ description: string;
36
+ derived: boolean;
37
+ source: { businessDocId?: string; productDocId?: string };
38
+ generatedAt: string;
39
+ };
40
+ customerSegments: BmcBlock;
41
+ valuePropositions: BmcBlock;
42
+ channels: BmcBlock;
43
+ customerRelationships: BmcBlock;
44
+ revenueStreams: BmcBlock;
45
+ keyResources: BmcBlock;
46
+ keyActivities: BmcBlock;
47
+ keyPartnerships: BmcBlock;
48
+ costStructure: BmcBlock & { strategicFocus?: string; drivers: string[] };
49
+ emptyBlocks: BmcBlockId[];
50
+ }
51
+
52
+ export interface BmcCanvasView {
53
+ canvasName: string | null;
54
+ nodes: WorkflowNode[];
55
+ edges: WorkflowEdge[];
56
+ emptyBlocks: BmcBlockId[];
57
+ message?: string;
58
+ }
@@ -81,7 +81,7 @@ function WorkflowCanvasInner({
81
81
  }}
82
82
  >
83
83
  <Canvas
84
- className="bg-background"
84
+ className="bg-background [&_.react-flow\_\_handle]:opacity-0 [&_.react-flow\_\_handle]:pointer-events-none [&_.react-flow\_\_node:hover_.react-flow\_\_handle]:opacity-100 [&_.react-flow\_\_node:hover_.react-flow\_\_handle]:pointer-events-auto [&_.react-flow\_\_node.selected_.react-flow\_\_handle]:opacity-100 [&_.react-flow\_\_node.selected_.react-flow\_\_handle]:pointer-events-auto [&_.react-flow\_\_handle]:transition-opacity [&_.react-flow\_\_handle]:duration-150"
85
85
  connectionLineComponent={ConnectionLine}
86
86
  connectionMode={ConnectionMode.Strict}
87
87
  edges={edges}
@@ -16,6 +16,9 @@ export * from './SectionLayout'
16
16
  export { WorkflowCanvas, getLayoutedElements } from './WorkflowCanvas'
17
17
  export type { WorkflowCanvasProps, WorkflowNode, WorkflowEdge, WorkflowNodeData } from './WorkflowCanvas'
18
18
 
19
+ export { bmcToCanvas } from './BmcCanvas'
20
+ export type { BmcBlockId, BmcItem, BmcBlock, CompiledBmc, BmcCanvasView } from './BmcCanvas'
21
+
19
22
  export { DashboardMetrics } from './DashboardMetrics'
20
23
  export type { DashboardMetricsProps } from './DashboardMetrics'
21
24
 
@@ -16,7 +16,7 @@ export interface SubAgent {
16
16
  subAgentName: string
17
17
  input: string | Record<string, unknown>
18
18
  output?: string | Record<string, unknown>
19
- status: "pending" | "active" | "completed" | "error"
19
+ status: "pending" | "active" | "completed" | "error" | "running" | "in_progress"
20
20
  }
21
21
 
22
22
  export interface AgentIndicatorProps {
@@ -12,6 +12,12 @@ import {
12
12
  NodeTitle,
13
13
  getHighlightIconColor,
14
14
  } from "@/components/ai-elements/node";
15
+ import {
16
+ Tooltip,
17
+ TooltipContent,
18
+ TooltipProvider,
19
+ TooltipTrigger,
20
+ } from "@/components/primitives/Tooltip";
15
21
  import { cn } from "@/lib/utils";
16
22
  import type { ToolbarAction } from "@/components/composites/WorkflowToolbar";
17
23
 
@@ -27,6 +33,14 @@ export type SpatialContainerColorTheme =
27
33
 
28
34
  export type SpatialContainerVariant = "extruded3d" | "flat" | "glassmorphism";
29
35
 
36
+ export type SpatialContainerItem = {
37
+ id: string;
38
+ label: string;
39
+ description?: string;
40
+ status?: "idle" | "running" | "success" | "error";
41
+ icon?: string;
42
+ };
43
+
30
44
  export type SpatialContainerNodeData = {
31
45
  label: string;
32
46
  description?: string;
@@ -41,6 +55,8 @@ export type SpatialContainerNodeData = {
41
55
  actions?: ToolbarAction[];
42
56
  /** Badge count or sub-item summary tag */
43
57
  badgeText?: string;
58
+ /** Sub-items rendered as HTML card elements inside spatial container */
59
+ items?: SpatialContainerItem[];
44
60
  };
45
61
 
46
62
  type SpatialContainerNodeProps = NodeProps & {
@@ -149,8 +165,8 @@ export const SpatialContainerNode = memo(
149
165
 
150
166
  return (
151
167
  <div className="relative group" style={{ width, height }}>
152
- {/* Node Resizer */}
153
- <NodeResizer minWidth={180} minHeight={140} isVisible={selected} />
168
+ {/* Node Resizer — disabled for clean spatial sections */}
169
+ <NodeResizer minWidth={180} minHeight={140} isVisible={false} />
154
170
 
155
171
  {/* 2.5D Extrusion Side & Bottom Panels for extruded3d visual mode */}
156
172
  {variant === "extruded3d" && (
@@ -158,14 +174,14 @@ export const SpatialContainerNode = memo(
158
174
  {/* Bottom Extrusion Shadow Face */}
159
175
  <div
160
176
  className={cn(
161
- "absolute -bottom-3 left-3 right-0 h-3 rounded-b-md transition-all duration-200",
177
+ "absolute -bottom-3.5 left-3.5 right-0 h-3.5 rounded-b-md transition-all duration-200 border-b border-r border-black/30 dark:border-black/60 shadow-md",
162
178
  theme.extrusionBottom
163
179
  )}
164
180
  />
165
181
  {/* Right Side Extrusion Face */}
166
182
  <div
167
183
  className={cn(
168
- "absolute -right-3 top-3 bottom-0 w-3 rounded-r-md transition-all duration-200",
184
+ "absolute -right-3.5 top-3.5 bottom-0 w-3.5 rounded-r-md transition-all duration-200 border-t border-r border-black/30 dark:border-black/60 shadow-md",
169
185
  theme.extrusionSide
170
186
  )}
171
187
  />
@@ -174,18 +190,20 @@ export const SpatialContainerNode = memo(
174
190
 
175
191
  <Node
176
192
  className={cn(
177
- "relative h-full w-full flex flex-col border transition-all duration-200 ease-out overflow-hidden rounded-lg",
193
+ "relative h-full w-full flex flex-col border transition-all duration-200 ease-out overflow-hidden rounded-lg shadow-sm",
178
194
  theme.border,
179
195
  theme.bg,
180
196
  variant === "glassmorphism" && "backdrop-blur-md bg-opacity-70 dark:bg-opacity-40",
181
- variant === "extruded3d" && "shadow-lg hover:shadow-xl",
182
- selected ? "border-primary border-2 shadow-primary/20 ring-2 ring-primary/30" : "shadow-sm",
197
+ variant === "extruded3d" && "shadow-xl border-t border-l border-white/20 dark:border-white/10",
183
198
  isDisabled && "opacity-50"
184
199
  )}
185
200
  data-testid={`spatial-container-node-${id}`}
186
201
  handles={{ target: true, source: true }}
187
202
  status={status}
188
203
  >
204
+ {/* Top 3D Highlight Line */}
205
+ <div className="absolute top-0 inset-x-0 h-0.5 bg-gradient-to-r from-transparent via-white/40 to-transparent pointer-events-none z-10" />
206
+
189
207
  {/* Header Panel */}
190
208
  <div
191
209
  className={cn(
@@ -203,13 +221,42 @@ export const SpatialContainerNode = memo(
203
221
  )}
204
222
  strokeWidth={2}
205
223
  />
206
- <div className="min-w-0 flex-1">
224
+ <div className="flex items-center gap-1.5 min-w-0 flex-1">
207
225
  <NodeTitle
208
- className="line-clamp-1 text-xs font-semibold tracking-wide uppercase"
226
+ className="truncate text-xs font-bold tracking-wider uppercase"
209
227
  title={displayTitle}
210
228
  >
211
229
  {displayTitle}
212
230
  </NodeTitle>
231
+ {displayDescription && (
232
+ <TooltipProvider>
233
+ <Tooltip delayDuration={100}>
234
+ <TooltipTrigger asChild>
235
+ <span className="cursor-help text-muted-foreground/60 hover:text-foreground shrink-0 transition-colors">
236
+ <Icon name="info" size="xs" />
237
+ </span>
238
+ </TooltipTrigger>
239
+ <TooltipContent
240
+ side="top"
241
+ align="start"
242
+ className="max-w-xs p-3 text-xs bg-popover text-popover-foreground border border-border shadow-lg space-y-2"
243
+ >
244
+ {displayDescription.split('\n\n').map((paragraph, idx) => (
245
+ <p
246
+ key={idx}
247
+ className={cn(
248
+ idx === 0 && "font-medium text-foreground",
249
+ idx === 1 && "italic text-primary/90 font-medium",
250
+ idx === 2 && "text-muted-foreground text-[11px]"
251
+ )}
252
+ >
253
+ {paragraph}
254
+ </p>
255
+ ))}
256
+ </TooltipContent>
257
+ </Tooltip>
258
+ </TooltipProvider>
259
+ )}
213
260
  </div>
214
261
  </div>
215
262
 
@@ -226,19 +273,35 @@ export const SpatialContainerNode = memo(
226
273
  )}
227
274
  </div>
228
275
 
229
- {/* Body Container Region for Child Nodes */}
230
- <div className="relative flex-1 p-3 flex flex-col justify-between pointer-events-none">
231
- {displayDescription && (
232
- <NodeDescription
233
- className="text-[11px] text-muted-foreground line-clamp-2"
234
- title={displayDescription}
235
- >
236
- {displayDescription}
237
- </NodeDescription>
276
+ {/* Body Container Region for Child HTML Card Items */}
277
+ <div className="relative flex-1 p-2.5 overflow-y-auto nowheel nodrag nopan custom-scrollbar pointer-events-auto flex flex-col gap-2">
278
+ {data.items && data.items.length > 0 ? (
279
+ data.items.map((item) => (
280
+ <div
281
+ key={item.id}
282
+ className="group relative flex items-center gap-2 rounded-md border border-border/70 bg-card/95 px-3 py-2 text-xs shadow-2xs transition-all hover:border-primary/50 hover:bg-accent/30"
283
+ >
284
+ <Icon
285
+ name={item.icon || "zap"}
286
+ size="xs"
287
+ className="shrink-0 text-primary"
288
+ strokeWidth={1.5}
289
+ />
290
+ <div className="min-w-0 flex-1">
291
+ <div className="font-medium text-foreground truncate" title={item.label}>
292
+ {item.label}
293
+ </div>
294
+ {item.description && (
295
+ <div className="text-[10px] text-muted-foreground truncate" title={item.description}>
296
+ {item.description}
297
+ </div>
298
+ )}
299
+ </div>
300
+ </div>
301
+ ))
302
+ ) : (
303
+ <div className="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(0,0,0,0.03)_1px,transparent_1px)] dark:bg-[radial-gradient(circle_at_center,rgba(255,255,255,0.04)_1px,transparent_1px)] bg-[size:16px_16px] pointer-events-none -z-10" />
238
304
  )}
239
-
240
- {/* Subtle Spatial Grid Watermark / Grid Background Accent */}
241
- <div className="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(0,0,0,0.03)_1px,transparent_1px)] dark:bg-[radial-gradient(circle_at_center,rgba(255,255,255,0.04)_1px,transparent_1px)] bg-[size:16px_16px] pointer-events-none -z-10" />
242
305
  </div>
243
306
 
244
307
  {/* Node Toolbar Actions */}
@@ -23,7 +23,7 @@ export interface SpecialistMessageData {
23
23
  icon?: React.ReactNode
24
24
  content: string
25
25
  toolCalls?: ToolCall[]
26
- status: "pending" | "active" | "completed" | "error"
26
+ status: "pending" | "active" | "completed" | "error" | "running" | "in_progress"
27
27
  avatarSrc?: string
28
28
  avatarName?: string
29
29
  input?: string | Record<string, unknown>
@@ -0,0 +1,46 @@
1
+ import type { Meta, StoryObj } from '@storybook/react'
2
+ import { SystemMessage } from './SystemMessage'
3
+
4
+ const meta: Meta<typeof SystemMessage> = {
5
+ title: 'Composites/SystemMessage',
6
+ component: SystemMessage,
7
+ parameters: {
8
+ layout: 'padded',
9
+ },
10
+ } satisfies Meta<typeof SystemMessage>
11
+
12
+ export default meta
13
+ type Story = StoryObj<typeof meta>
14
+
15
+ export const Default: Story = {
16
+ args: {
17
+ message: {
18
+ id: '1',
19
+ content: 'Background task completed successfully.',
20
+ },
21
+ },
22
+ }
23
+
24
+ export const WithLongContent: Story = {
25
+ args: {
26
+ message: {
27
+ id: '2',
28
+ content: 'The analysis has been completed and the results are now available for review. You can access the full report from the dashboard.',
29
+ },
30
+ },
31
+ }
32
+
33
+ export const WithCustomRenderer: Story = {
34
+ args: {
35
+ message: {
36
+ id: '3',
37
+ content: 'Audio message received',
38
+ },
39
+ renderContent: (content) => (
40
+ <div className="flex items-center gap-2">
41
+ <span className="text-xs">🎵</span>
42
+ <span>{content}</span>
43
+ </div>
44
+ ),
45
+ },
46
+ }