@genui/a3-create 0.1.36

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 (91) hide show
  1. package/README.md +123 -0
  2. package/dist/index.js +684 -0
  3. package/package.json +52 -0
  4. package/template/.cursor/rules/example-app.mdc +9 -0
  5. package/template/CLAUDE.md +121 -0
  6. package/template/README.md +20 -0
  7. package/template/_gitignore +36 -0
  8. package/template/app/ThemeProvider.tsx +17 -0
  9. package/template/app/agents/age.ts +25 -0
  10. package/template/app/agents/greeting.ts +30 -0
  11. package/template/app/agents/index.ts +57 -0
  12. package/template/app/agents/onboarding/index.ts +15 -0
  13. package/template/app/agents/onboarding/prompt.ts +59 -0
  14. package/template/app/agents/registry.ts +17 -0
  15. package/template/app/agents/state.ts +10 -0
  16. package/template/app/api/agui/route.ts +56 -0
  17. package/template/app/api/chat/route.ts +35 -0
  18. package/template/app/api/stream/route.ts +57 -0
  19. package/template/app/apple-icon-dark.png +0 -0
  20. package/template/app/apple-icon.png +0 -0
  21. package/template/app/components/atoms/AgentNode.tsx +56 -0
  22. package/template/app/components/atoms/AppLogo.tsx +44 -0
  23. package/template/app/components/atoms/ChatContainer.tsx +13 -0
  24. package/template/app/components/atoms/ChatHeader.tsx +49 -0
  25. package/template/app/components/atoms/MarkdownRenderer.tsx +134 -0
  26. package/template/app/components/atoms/MessageBubble.tsx +21 -0
  27. package/template/app/components/atoms/TransitionEdge.tsx +49 -0
  28. package/template/app/components/atoms/index.ts +7 -0
  29. package/template/app/components/molecules/ChatInput.tsx +94 -0
  30. package/template/app/components/molecules/ChatMessage.tsx +45 -0
  31. package/template/app/components/molecules/index.ts +2 -0
  32. package/template/app/components/organisms/AgentGraph.tsx +75 -0
  33. package/template/app/components/organisms/AguiChat.tsx +133 -0
  34. package/template/app/components/organisms/Chat.tsx +88 -0
  35. package/template/app/components/organisms/ChatMessageList.tsx +35 -0
  36. package/template/app/components/organisms/ExamplePageLayout.tsx +118 -0
  37. package/template/app/components/organisms/OnboardingChat.tsx +24 -0
  38. package/template/app/components/organisms/Sidebar.tsx +147 -0
  39. package/template/app/components/organisms/SidebarLayout.tsx +58 -0
  40. package/template/app/components/organisms/StateViewer.tsx +126 -0
  41. package/template/app/components/organisms/StreamChat.tsx +173 -0
  42. package/template/app/components/organisms/index.ts +10 -0
  43. package/template/app/constants/chat.ts +52 -0
  44. package/template/app/constants/paths.ts +1 -0
  45. package/template/app/constants/ui.ts +61 -0
  46. package/template/app/examples/agui/page.tsx +26 -0
  47. package/template/app/examples/chat/page.tsx +26 -0
  48. package/template/app/examples/page.tsx +106 -0
  49. package/template/app/examples/stream/page.tsx +26 -0
  50. package/template/app/favicon-dark.ico +0 -0
  51. package/template/app/favicon.ico +0 -0
  52. package/template/app/icon.svg +13 -0
  53. package/template/app/layout.tsx +36 -0
  54. package/template/app/lib/actions/restartSession.ts +10 -0
  55. package/template/app/lib/getAgentGraphData.ts +43 -0
  56. package/template/app/lib/getGraphLayout.ts +99 -0
  57. package/template/app/lib/hooks/useRestart.ts +33 -0
  58. package/template/app/lib/parseTransitionTargets.ts +140 -0
  59. package/template/app/lib/providers/anthropic.ts +12 -0
  60. package/template/app/lib/providers/bedrock.ts +12 -0
  61. package/template/app/lib/providers/openai.ts +10 -0
  62. package/template/app/onboarding/page.tsx +21 -0
  63. package/template/app/page.tsx +16 -0
  64. package/template/app/styled.d.ts +6 -0
  65. package/template/app/theme.ts +22 -0
  66. package/template/docs/A3-README.md +121 -0
  67. package/template/docs/API-REFERENCE.md +85 -0
  68. package/template/docs/ARCHITECTURE.md +84 -0
  69. package/template/docs/CORE-CONCEPTS.md +347 -0
  70. package/template/docs/CUSTOM_LOGGING.md +36 -0
  71. package/template/docs/CUSTOM_PROVIDERS.md +642 -0
  72. package/template/docs/CUSTOM_STORES.md +228 -0
  73. package/template/docs/PROVIDER-ANTHROPIC.md +45 -0
  74. package/template/docs/PROVIDER-BEDROCK.md +45 -0
  75. package/template/docs/PROVIDER-OPENAI.md +47 -0
  76. package/template/docs/PROVIDERS.md +124 -0
  77. package/template/docs/QUICK-START-EXAMPLES.md +197 -0
  78. package/template/docs/RESILIENCE.md +226 -0
  79. package/template/docs/TRANSITIONS.md +245 -0
  80. package/template/docs/WIDGETS.md +331 -0
  81. package/template/docs/contributing/LOGGING.md +104 -0
  82. package/template/docs/designs/a3-gtm-strategy.md +280 -0
  83. package/template/docs/designs/a3-platform-vision.md +276 -0
  84. package/template/next-env.d.ts +6 -0
  85. package/template/next.config.mjs +15 -0
  86. package/template/package.json +41 -0
  87. package/template/public/android-chrome-192x192.png +0 -0
  88. package/template/public/android-chrome-512x512.png +0 -0
  89. package/template/public/site.webmanifest +11 -0
  90. package/template/scripts/dev.mjs +29 -0
  91. package/template/tsconfig.json +47 -0
@@ -0,0 +1,56 @@
1
+ 'use client'
2
+
3
+ import { memo } from 'react'
4
+ import { Handle, Position, type NodeProps } from '@xyflow/react'
5
+ import { Box, Typography } from '@mui/material'
6
+ import { GRAPH_ACTIVE_BADGE } from '@constants/ui'
7
+
8
+ function AgentNodeComponent({ data }: NodeProps) {
9
+ const { label, isActive } = data as { label: string; isActive: boolean; description: string }
10
+
11
+ return (
12
+ <>
13
+ <Handle type="target" id="target-top" position={Position.Top} style={{ opacity: 0 }} />
14
+ <Handle type="target" id="target-right" position={Position.Right} style={{ opacity: 0 }} />
15
+ <Box
16
+ sx={{
17
+ px: 2,
18
+ py: 1,
19
+ borderRadius: '10px',
20
+ bgcolor: isActive ? '#2563eb' : '#e2e8f0',
21
+ border: '1.5px solid',
22
+ borderColor: isActive ? '#1d4ed8' : '#cbd5e1',
23
+ boxShadow: isActive ? '0 0 8px rgba(37, 99, 235, 0.5)' : 'none',
24
+ animation: isActive ? 'agentPulse 2s ease-in-out infinite' : 'none',
25
+ '@keyframes agentPulse': {
26
+ '0%, 100%': { boxShadow: '0 0 4px rgba(37, 99, 235, 0.3)' },
27
+ '50%': { boxShadow: '0 0 12px rgba(37, 99, 235, 0.6)' },
28
+ },
29
+ minWidth: 100,
30
+ textAlign: 'center',
31
+ }}
32
+ >
33
+ <Typography
34
+ sx={{
35
+ fontSize: 12,
36
+ fontWeight: isActive ? 700 : 500,
37
+ color: isActive ? '#ffffff' : '#1e293b',
38
+ lineHeight: 1.2,
39
+ }}
40
+ >
41
+ {label}
42
+ </Typography>
43
+ </Box>
44
+ {isActive && (
45
+ <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 0.5, mt: 0.5 }}>
46
+ <Box sx={{ width: 6, height: 6, borderRadius: '50%', bgcolor: '#22c55e' }} />
47
+ <Typography sx={{ fontSize: 9, fontWeight: 600, color: '#2563eb' }}>{GRAPH_ACTIVE_BADGE}</Typography>
48
+ </Box>
49
+ )}
50
+ <Handle type="source" id="source-bottom" position={Position.Bottom} style={{ opacity: 0 }} />
51
+ <Handle type="source" id="source-right" position={Position.Right} style={{ opacity: 0 }} />
52
+ </>
53
+ )
54
+ }
55
+
56
+ export const AgentNode = memo(AgentNodeComponent)
@@ -0,0 +1,44 @@
1
+ const MARK_PATH =
2
+ 'M 560.50 870.11 C544.60,872.39 493.52,873.54 477.50,871.98 C429.64,867.32 381.89,853.57 340.50,832.53 C316.53,820.34 296.91,807.54 274.00,789.15 C261.51,779.12 236.58,754.18 225.88,741.00 C180.72,685.39 153.06,618.33 146.08,547.58 C144.70,533.59 144.69,497.56 146.06,483.42 C151.82,424.12 172.58,365.85 205.33,317.00 C257.37,239.38 349.03,180.93 442.00,166.07 C464.04,162.55 478.67,161.69 505.88,162.29 C555.48,163.38 594.16,171.21 639.00,189.25 C659.38,197.45 694.34,215.75 702.79,222.64 L 705.07 224.50 L 699.18 234.00 C695.94,239.23 690.48,248.11 687.05,253.75 C683.61,259.39 680.44,264.00 679.99,264.00 C679.54,264.00 674.52,261.04 668.84,257.42 C655.11,248.69 625.53,234.22 610.00,228.66 C583.75,219.25 553.35,212.43 527.00,210.05 C512.11,208.70 478.67,208.71 464.00,210.07 C448.87,211.47 421.38,216.98 407.00,221.49 C359.48,236.39 317.14,262.02 281.44,297.50 C258.31,320.48 241.26,343.77 226.14,373.00 C202.42,418.87 191.69,463.62 191.74,516.50 C191.78,555.23 197.10,585.98 210.10,622.48 C215.21,636.83 229.38,665.53 238.40,679.79 C258.58,711.72 284.51,739.87 314.60,762.50 C358.47,795.49 416.85,818.66 475.00,826.15 C490.90,828.19 536.15,827.93 552.00,825.69 C589.08,820.47 619.66,811.35 651.80,795.94 C758.47,744.79 825.34,642.74 828.69,526.00 C830.16,474.91 818.90,425.90 794.39,376.67 C790.88,369.61 788.00,363.68 788.00,363.49 C788.00,362.88 830.91,349.00 832.81,349.00 C834.20,349.00 836.30,352.42 841.26,362.75 C849.43,379.78 850.04,381.23 856.16,398.49 C887.34,486.35 882.15,583.99 841.89,667.00 C819.83,712.49 793.79,746.96 757.00,779.42 C700.79,829.00 634.78,859.47 560.50,870.11 ZM 335.80 648.00 L 331.50 660.50 L 297.25 660.76 C278.41,660.91 263.00,660.77 263.00,660.45 C263.00,659.86 278.07,617.38 291.17,581.00 C295.04,570.28 308.24,533.38 320.52,499.00 C332.80,464.62 347.87,422.55 354.01,405.50 C360.15,388.45 366.62,370.45 368.39,365.50 L 371.61 356.50 L 409.10 356.24 L 446.59 355.98 L 457.22 385.74 C463.07,402.11 475.32,436.42 484.45,462.00 C493.58,487.58 502.36,512.10 503.97,516.50 C505.58,520.90 515.27,547.90 525.51,576.50 C535.75,605.10 546.52,635.12 549.43,643.22 C552.35,651.31 554.49,658.17 554.20,658.47 C553.91,658.76 537.97,659.00 518.78,659.00 L 483.90 659.00 L 482.58 655.75 C481.85,653.96 477.26,640.69 472.38,626.26 L 463.50 600.02 L 352.45 600.00 L 346.28 617.75 C342.88,627.51 338.17,641.12 335.80,648.00 ZM 687.50 661.35 C660.93,665.46 637.94,663.57 615.06,655.41 C601.27,650.49 591.03,644.01 580.37,633.46 C572.36,625.53 570.61,623.14 566.13,613.96 C560.20,601.81 557.14,589.54 556.76,576.32 L 556.50 567.50 L 616.57 566.97 L 617.62 574.74 C620.91,598.97 640.94,614.52 666.50,612.68 C689.18,611.04 704.64,597.72 707.93,576.97 C709.92,564.44 706.02,552.13 697.07,542.67 C687.39,532.43 676.26,529.04 652.23,529.02 L 636.96 529.00 L 637.23 504.75 L 637.50 480.50 L 654.00 479.93 C674.62,479.21 680.48,477.31 690.26,468.16 C697.55,461.34 700.26,455.37 700.77,445.00 C701.35,433.25 698.88,426.59 691.06,418.89 C682.94,410.89 676.70,408.61 663.00,408.61 C649.61,408.61 643.48,410.81 635.00,418.66 C628.14,425.01 625.48,428.98 622.42,437.42 L 620.22 443.50 L 591.93 443.77 C576.36,443.91 563.30,443.69 562.90,443.27 C561.92,442.23 564.65,426.97 567.16,419.51 C578.07,387.00 606.35,365.08 645.92,358.47 C659.23,356.24 682.31,356.90 695.19,359.88 C714.30,364.30 729.13,372.16 741.60,384.49 C755.80,398.54 761.90,412.81 762.75,434.04 C763.17,444.62 762.92,447.89 761.13,455.04 C756.76,472.50 747.16,486.30 733.31,495.04 C729.72,497.32 727.16,499.52 727.64,499.94 C728.11,500.37 731.08,501.76 734.24,503.03 C747.92,508.57 761.40,522.70 767.11,537.47 C771.91,549.87 773.33,559.36 772.69,574.50 C772.04,589.80 770.30,597.27 764.55,609.50 C751.70,636.85 723.57,655.78 687.50,661.35 ZM 370.99 545.75 C371.00,546.73 379.19,547.00 408.61,547.00 C438.28,547.00 446.12,546.74 445.75,545.75 C445.49,545.06 437.02,519.64 426.93,489.25 C416.83,458.86 408.31,434.00 407.99,434.00 C407.33,434.00 370.98,543.79 370.99,545.75 Z'
3
+ const DOT_PATH =
4
+ 'M 806.90 322.97 C785.45,328.12 765.24,322.48 749.97,307.09 C737.88,294.91 733.00,282.92 733.00,265.40 C733.00,254.57 734.84,247.01 739.72,237.71 C746.39,225.02 759.41,213.62 772.68,208.85 C780.93,205.88 799.11,205.17 807.92,207.47 C830.44,213.34 847.16,231.12 851.67,253.99 C855.45,273.14 849.31,292.70 835.02,307.04 C826.17,315.92 818.73,320.14 806.90,322.97 Z'
5
+
6
+ export type AppLogoProps = {
7
+ /** Fill color for the A3 + ring (default: dark gray) */
8
+ markColor?: string
9
+ /** Fill color for the accent dot (default: orange) */
10
+ dotColor?: string
11
+ width?: number | string
12
+ height?: number | string
13
+ className?: string
14
+ 'aria-label'?: string
15
+ }
16
+
17
+ const DEFAULT_MARK = 'rgb(4,4,4)'
18
+ const DEFAULT_DOT = 'rgb(248,175,81)'
19
+
20
+ export function AppLogo({
21
+ markColor = DEFAULT_MARK,
22
+ dotColor = DEFAULT_DOT,
23
+ width = 32,
24
+ height = 32,
25
+ className,
26
+ 'aria-label': ariaLabel = 'A3',
27
+ }: AppLogoProps) {
28
+ return (
29
+ <svg
30
+ xmlns="http://www.w3.org/2000/svg"
31
+ viewBox="0 0 1024 1024"
32
+ width={width}
33
+ height={height}
34
+ className={className}
35
+ aria-label={ariaLabel}
36
+ role="img"
37
+ >
38
+ <g>
39
+ <path d={MARK_PATH} fill={markColor} />
40
+ <path d={DOT_PATH} fill={dotColor} />
41
+ </g>
42
+ </svg>
43
+ )
44
+ }
@@ -0,0 +1,13 @@
1
+ 'use client'
2
+
3
+ import styled from 'styled-components'
4
+ import { Paper } from '@mui/material'
5
+
6
+ export const ChatContainer = styled(Paper)`
7
+ display: flex;
8
+ flex-direction: column;
9
+ height: 100%;
10
+ min-height: 0;
11
+ border-radius: 12px;
12
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
13
+ `
@@ -0,0 +1,49 @@
1
+ 'use client'
2
+
3
+ import type { ReactNode } from 'react'
4
+ import styled from 'styled-components'
5
+ import { IconButton, Tooltip, CircularProgress } from '@mui/material'
6
+ import RestartAltIcon from '@mui/icons-material/RestartAlt'
7
+ import type { Theme } from '@mui/material/styles'
8
+ import { CHAT_RESTART } from '@constants/ui'
9
+
10
+ const HeaderContainer = styled.div`
11
+ border-bottom: 1px solid ${({ theme }) => (theme as Theme).palette.divider};
12
+ background-color: ${({ theme }) => (theme as Theme).palette.background.paper};
13
+ padding: ${({ theme }) => (theme as Theme).spacing(2, 3)};
14
+ flex-shrink: 0;
15
+ display: flex;
16
+ justify-content: flex-end;
17
+ align-items: center;
18
+ gap: ${({ theme }) => (theme as Theme).spacing(1)};
19
+ min-height: 40px;
20
+ height: 40px;
21
+ `
22
+
23
+ interface ChatHeaderProps {
24
+ children?: ReactNode
25
+ onRestart?: () => void
26
+ isRestarting?: boolean
27
+ }
28
+
29
+ export function ChatHeader({ children, onRestart, isRestarting }: ChatHeaderProps) {
30
+ return (
31
+ <HeaderContainer>
32
+ {children}
33
+ {onRestart && (
34
+ <Tooltip title={CHAT_RESTART}>
35
+ <span>
36
+ <IconButton
37
+ aria-label={CHAT_RESTART}
38
+ onClick={onRestart}
39
+ disabled={isRestarting}
40
+ size="small"
41
+ >
42
+ {isRestarting ? <CircularProgress size={16} /> : <RestartAltIcon fontSize="small" />}
43
+ </IconButton>
44
+ </span>
45
+ </Tooltip>
46
+ )}
47
+ </HeaderContainer>
48
+ )
49
+ }
@@ -0,0 +1,134 @@
1
+ 'use client'
2
+
3
+ import { styled, useTheme } from '@mui/material/styles'
4
+ import Typography from '@mui/material/Typography'
5
+ import Link from '@mui/material/Link'
6
+ import ReactMarkdown, { type Components } from 'react-markdown'
7
+ import remarkGfm from 'remark-gfm'
8
+ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
9
+ import { oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism'
10
+ import { REPO_URL } from '@constants/paths'
11
+
12
+ const MarkdownRoot = styled('div')(({ theme }) => ({
13
+ '& > *:first-of-type': { marginTop: 0 },
14
+ '& > *:last-child': { marginBottom: 0 },
15
+ '& ul, & ol': {
16
+ paddingLeft: theme.spacing(3),
17
+ marginTop: theme.spacing(0.5),
18
+ marginBottom: theme.spacing(0.5),
19
+ },
20
+ '& li': {
21
+ marginBottom: theme.spacing(0.25),
22
+ },
23
+ '& blockquote': {
24
+ margin: theme.spacing(1, 0),
25
+ padding: theme.spacing(0.5, 2),
26
+ borderLeft: `4px solid ${theme.palette.primary.main}`,
27
+ backgroundColor: theme.palette.action.hover,
28
+ },
29
+ }))
30
+
31
+ function useMarkdownComponents(): Components {
32
+ const theme = useTheme()
33
+
34
+ return {
35
+ p: ({ children }) => (
36
+ <Typography variant="body2" sx={{ my: 0.5 }}>
37
+ {children}
38
+ </Typography>
39
+ ),
40
+ h1: ({ children }) => (
41
+ <Typography variant="h5" sx={{ mt: 2, mb: 1, fontWeight: 600 }}>
42
+ {children}
43
+ </Typography>
44
+ ),
45
+ h2: ({ children }) => (
46
+ <Typography variant="h6" sx={{ mt: 1.5, mb: 0.75, fontWeight: 600 }}>
47
+ {children}
48
+ </Typography>
49
+ ),
50
+ h3: ({ children }) => (
51
+ <Typography variant="subtitle1" sx={{ mt: 1.5, mb: 0.5, fontWeight: 600 }}>
52
+ {children}
53
+ </Typography>
54
+ ),
55
+ h4: ({ children }) => (
56
+ <Typography variant="subtitle2" sx={{ mt: 1, mb: 0.5, fontWeight: 600 }}>
57
+ {children}
58
+ </Typography>
59
+ ),
60
+ h5: ({ children }) => (
61
+ <Typography variant="body1" sx={{ mt: 1, mb: 0.5, fontWeight: 600 }}>
62
+ {children}
63
+ </Typography>
64
+ ),
65
+ h6: ({ children }) => (
66
+ <Typography variant="body2" sx={{ mt: 1, mb: 0.5, fontWeight: 600 }}>
67
+ {children}
68
+ </Typography>
69
+ ),
70
+ a: ({ href, children }) => {
71
+ const resolvedHref = href?.match(/^\.?\/?docs\/.*\.md$/) ? `${REPO_URL}/${href.replace(/^\.\//, '')}` : href
72
+
73
+ return (
74
+ <Link href={resolvedHref} target="_blank" rel="noopener noreferrer">
75
+ {children}
76
+ </Link>
77
+ )
78
+ },
79
+ li: ({ children }) => (
80
+ <Typography variant="body2" component="li" sx={{ my: 0.25 }}>
81
+ {children}
82
+ </Typography>
83
+ ),
84
+ code: ({ className, children, ...props }) => {
85
+ const match = /language-(\w+)/.exec(className || '')
86
+ const codeString = String(children as string).replace(/\n$/, '')
87
+ const isBlock = !!match || codeString.includes('\n')
88
+
89
+ if (isBlock) {
90
+ return (
91
+ <SyntaxHighlighter
92
+ style={oneLight}
93
+ language={match?.[1] || 'text'}
94
+ PreTag="div"
95
+ customStyle={{
96
+ margin: theme.spacing(1, 0),
97
+ borderRadius: theme.shape.borderRadius,
98
+ fontSize: '0.8125rem',
99
+ }}
100
+ >
101
+ {codeString}
102
+ </SyntaxHighlighter>
103
+ )
104
+ }
105
+
106
+ return (
107
+ <code
108
+ style={{
109
+ backgroundColor: theme.palette.action.hover,
110
+ padding: '2px 6px',
111
+ borderRadius: theme.shape.borderRadius,
112
+ fontSize: '0.8125rem',
113
+ fontFamily: 'monospace',
114
+ }}
115
+ {...props}
116
+ >
117
+ {children}
118
+ </code>
119
+ )
120
+ },
121
+ }
122
+ }
123
+
124
+ export function MarkdownRenderer({ content }: { content: string }) {
125
+ const components = useMarkdownComponents()
126
+
127
+ return (
128
+ <MarkdownRoot>
129
+ <ReactMarkdown remarkPlugins={[remarkGfm]} components={components}>
130
+ {content}
131
+ </ReactMarkdown>
132
+ </MarkdownRoot>
133
+ )
134
+ }
@@ -0,0 +1,21 @@
1
+ import styled from 'styled-components'
2
+ import { Paper } from '@mui/material'
3
+ import type { Theme } from '@mui/material/styles'
4
+
5
+ export const MessageBubble = styled(Paper)<{ $isUser: boolean }>`
6
+ max-width: 80%;
7
+ padding: ${({ theme }) => (theme as Theme).spacing(1.5, 2)};
8
+ border-radius: ${({ theme }) => (theme as Theme).spacing(2.5)};
9
+ ${({ $isUser, theme }) =>
10
+ $isUser
11
+ ? `
12
+ background-color: ${(theme as Theme).palette.primary.main};
13
+ color: ${(theme as Theme).palette.primary.contrastText};
14
+ border-bottom-right-radius: ${(theme as Theme).spacing(0.5)};
15
+ `
16
+ : `
17
+ background-color: ${(theme as Theme).palette.grey[200]};
18
+ color: ${(theme as Theme).palette.text.primary};
19
+ border-bottom-left-radius: ${(theme as Theme).spacing(0.5)};
20
+ `}
21
+ `
@@ -0,0 +1,49 @@
1
+ 'use client'
2
+
3
+ import { memo } from 'react'
4
+ import { BaseEdge, getBezierPath, type EdgeProps } from '@xyflow/react'
5
+
6
+ function DeterministicEdgeComponent(props: EdgeProps) {
7
+ const [edgePath] = getBezierPath(props)
8
+ return (
9
+ <BaseEdge
10
+ path={edgePath}
11
+ markerEnd={props.markerEnd}
12
+ style={{ stroke: '#64748b', strokeWidth: 1.5 }}
13
+ />
14
+ )
15
+ }
16
+
17
+ function DynamicEdgeComponent(props: EdgeProps) {
18
+ const [edgePath] = getBezierPath(props)
19
+ return (
20
+ <BaseEdge
21
+ path={edgePath}
22
+ markerEnd={props.markerEnd}
23
+ style={{ stroke: '#94a3b8', strokeWidth: 1.5, strokeDasharray: '5,3' }}
24
+ />
25
+ )
26
+ }
27
+
28
+ function SelfLoopEdgeComponent({ targetX, targetY, markerEnd, data }: EdgeProps) {
29
+ const isDynamic = (data as Record<string, unknown>)?.isDynamic
30
+ const stroke = isDynamic ? '#94a3b8' : '#64748b'
31
+
32
+ // Small arc above the node's target handle (top center)
33
+ const d = `M ${targetX - 12} ${targetY} C ${targetX - 12} ${targetY - 25}, ${targetX + 12} ${targetY - 25}, ${targetX + 12} ${targetY}`
34
+
35
+ return (
36
+ <path
37
+ d={d}
38
+ fill="none"
39
+ stroke={stroke}
40
+ strokeWidth={1.5}
41
+ strokeDasharray={isDynamic ? '5,3' : undefined}
42
+ markerEnd={markerEnd}
43
+ />
44
+ )
45
+ }
46
+
47
+ export const DeterministicEdge = memo(DeterministicEdgeComponent)
48
+ export const DynamicEdge = memo(DynamicEdgeComponent)
49
+ export const SelfLoopEdge = memo(SelfLoopEdgeComponent)
@@ -0,0 +1,7 @@
1
+ export { AgentNode } from './AgentNode'
2
+ export { AppLogo } from './AppLogo'
3
+ export { MarkdownRenderer } from './MarkdownRenderer'
4
+ export { MessageBubble } from './MessageBubble'
5
+ export { ChatContainer } from './ChatContainer'
6
+ export { ChatHeader } from './ChatHeader'
7
+ export { DeterministicEdge, DynamicEdge, SelfLoopEdge } from './TransitionEdge'
@@ -0,0 +1,94 @@
1
+ 'use client'
2
+
3
+ import { useState, useCallback, useEffect, useRef } from 'react'
4
+ import styled from 'styled-components'
5
+ import { TextField, Button, Box } from '@mui/material'
6
+ import type { Theme } from '@mui/material/styles'
7
+ import SendIcon from '@mui/icons-material/Send'
8
+ import { CHAT_PLACEHOLDER, CHAT_SEND } from '@constants/ui'
9
+
10
+ type Props = {
11
+ onSubmit: (text: string) => void | Promise<void>
12
+ disabled?: boolean
13
+ placeholder?: string
14
+ }
15
+
16
+ const InputContainer = styled(Box)`
17
+ border-top: 1px solid ${({ theme }) => (theme as Theme).palette.divider};
18
+ background-color: ${({ theme }) => (theme as Theme).palette.background.paper};
19
+ padding: ${({ theme }) => (theme as Theme).spacing(2)};
20
+ flex-shrink: 0;
21
+ `
22
+
23
+ const InputForm = styled.form`
24
+ display: flex;
25
+ gap: ${({ theme }) => (theme as Theme).spacing(1.5)};
26
+ `
27
+
28
+ export function ChatInput({ onSubmit, disabled, placeholder = CHAT_PLACEHOLDER }: Props) {
29
+ const [value, setValue] = useState('')
30
+ const inputRef = useRef<HTMLTextAreaElement>(null)
31
+ const prevDisabled = useRef(disabled)
32
+
33
+ useEffect(() => {
34
+ // When transitioning from disabled (true) back to enabled (false), restore focus
35
+ if (prevDisabled.current && !disabled) {
36
+ // Small timeout to ensure the clear-disabled DOM update finishes
37
+ const timer = setTimeout(() => inputRef.current?.focus(), 10)
38
+ return () => clearTimeout(timer)
39
+ }
40
+ prevDisabled.current = disabled
41
+ }, [disabled])
42
+
43
+ const handleSubmit = useCallback(
44
+ (e: React.FormEvent) => {
45
+ e.preventDefault()
46
+ const trimmed = value.trim()
47
+ if (!trimmed || disabled) return
48
+ void onSubmit(trimmed)
49
+ setValue('')
50
+ },
51
+ [value, disabled, onSubmit],
52
+ )
53
+
54
+ const handleKeyDown = useCallback(
55
+ (e: React.KeyboardEvent) => {
56
+ if (e.key === 'Enter' && !e.shiftKey) {
57
+ e.preventDefault()
58
+ handleSubmit(e)
59
+ }
60
+ },
61
+ [handleSubmit],
62
+ )
63
+
64
+ return (
65
+ <InputContainer>
66
+ <InputForm onSubmit={handleSubmit}>
67
+ <TextField
68
+ inputRef={inputRef}
69
+ fullWidth
70
+ multiline
71
+ maxRows={6}
72
+ value={value}
73
+ onChange={(e) => setValue(e.target.value)}
74
+ onKeyDown={handleKeyDown}
75
+ placeholder={placeholder}
76
+ disabled={disabled}
77
+ variant="outlined"
78
+ size="small"
79
+ autoComplete="off"
80
+ data-testid="chat-input"
81
+ />
82
+ <Button
83
+ type="submit"
84
+ variant="contained"
85
+ disabled={disabled || !value.trim()}
86
+ startIcon={<SendIcon />}
87
+ data-testid="chat-send"
88
+ >
89
+ {CHAT_SEND}
90
+ </Button>
91
+ </InputForm>
92
+ </InputContainer>
93
+ )
94
+ }
@@ -0,0 +1,45 @@
1
+ import styled, { keyframes } from 'styled-components'
2
+ import { Typography } from '@mui/material'
3
+ import { MarkdownRenderer, MessageBubble } from '@atoms'
4
+ import { MessageSender } from '@genui/a3'
5
+ import type { Message } from '@genui/a3'
6
+
7
+ const MessageRow = styled.div<{ $isUser: boolean }>`
8
+ display: flex;
9
+ justify-content: ${({ $isUser }) => ($isUser ? 'flex-end' : 'flex-start')};
10
+ `
11
+
12
+ const blink = keyframes`
13
+ 0%, 100% { opacity: 1; }
14
+ 50% { opacity: 0; }
15
+ `
16
+
17
+ const StreamingCursor = styled.span`
18
+ display: inline-block;
19
+ width: 6px;
20
+ height: 14px;
21
+ margin-left: 2px;
22
+ background-color: currentColor;
23
+ vertical-align: text-bottom;
24
+ animation: ${blink} 0.8s step-end infinite;
25
+ `
26
+
27
+ export function ChatMessage({ message }: { message: Message }) {
28
+ const isUser = message?.metadata?.source === MessageSender.USER
29
+ return (
30
+ <MessageRow $isUser={isUser} data-testid="chat-message">
31
+ <MessageBubble $isUser={isUser} elevation={0}>
32
+ {isUser ? (
33
+ <Typography variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>
34
+ {message.text.trim()}
35
+ </Typography>
36
+ ) : (
37
+ <>
38
+ <MarkdownRenderer content={message.text.trim()} />
39
+ {message.isStreaming && <StreamingCursor />}
40
+ </>
41
+ )}
42
+ </MessageBubble>
43
+ </MessageRow>
44
+ )
45
+ }
@@ -0,0 +1,2 @@
1
+ export { ChatMessage } from './ChatMessage'
2
+ export { ChatInput } from './ChatInput'
@@ -0,0 +1,75 @@
1
+ 'use client'
2
+
3
+ import { useMemo } from 'react'
4
+ import { ReactFlow, ReactFlowProvider } from '@xyflow/react'
5
+ import { Paper, Typography, Box, Stack } from '@mui/material'
6
+ import { AgentNode } from '@atoms/AgentNode'
7
+ import { DeterministicEdge, DynamicEdge, SelfLoopEdge } from '@atoms/TransitionEdge'
8
+ import { getGraphLayout } from '@lib/getGraphLayout'
9
+ import type { AgentInfo } from '@lib/getAgentGraphData'
10
+ import { GRAPH_HEADING, GRAPH_LEGEND_DETERMINISTIC, GRAPH_LEGEND_LLM } from '@constants/ui'
11
+ import '@xyflow/react/dist/style.css'
12
+
13
+ interface AgentGraphProps {
14
+ agents: AgentInfo[]
15
+ activeAgentId: string | null
16
+ }
17
+
18
+ const nodeTypes = { agent: AgentNode }
19
+ const edgeTypes = {
20
+ deterministic: DeterministicEdge,
21
+ dynamic: DynamicEdge,
22
+ selfLoop: SelfLoopEdge,
23
+ }
24
+
25
+ export function AgentGraph({ agents, activeAgentId }: AgentGraphProps) {
26
+ const { nodes, edges } = useMemo(() => getGraphLayout(agents, activeAgentId), [agents, activeAgentId])
27
+
28
+ if (agents.length === 0) return null
29
+
30
+ return (
31
+ <Paper elevation={0} sx={{ p: 2, border: 1, borderColor: 'divider', borderRadius: 3 }}>
32
+ <Stack direction="row" justifyContent="space-between" alignItems="center" mb={1}>
33
+ <Typography variant="subtitle2" fontWeight="bold">
34
+ {GRAPH_HEADING}
35
+ </Typography>
36
+ <Stack direction="row" gap={2} alignItems="center">
37
+ <Stack direction="row" gap={0.5} alignItems="center">
38
+ <Box sx={{ width: 20, height: 0, borderTop: '2px solid #64748b' }} />
39
+ <Typography variant="caption" color="text.secondary">
40
+ {GRAPH_LEGEND_DETERMINISTIC}
41
+ </Typography>
42
+ </Stack>
43
+ <Stack direction="row" gap={0.5} alignItems="center">
44
+ <Box sx={{ width: 20, height: 0, borderTop: '2px dashed #94a3b8' }} />
45
+ <Typography variant="caption" color="text.secondary">
46
+ {GRAPH_LEGEND_LLM}
47
+ </Typography>
48
+ </Stack>
49
+ </Stack>
50
+ </Stack>
51
+ <Box sx={{ height: 350 }}>
52
+ <ReactFlowProvider>
53
+ <ReactFlow
54
+ nodes={nodes}
55
+ edges={edges}
56
+ nodeTypes={nodeTypes}
57
+ edgeTypes={edgeTypes}
58
+ fitView
59
+ fitViewOptions={{ padding: 0.4 }}
60
+ nodesDraggable={false}
61
+ nodesConnectable={false}
62
+ elementsSelectable={false}
63
+ zoomOnScroll={false}
64
+ zoomOnPinch={false}
65
+ zoomOnDoubleClick={false}
66
+ panOnDrag={false}
67
+ panOnScroll={false}
68
+ preventScrolling={false}
69
+ proOptions={{ hideAttribution: true }}
70
+ />
71
+ </ReactFlowProvider>
72
+ </Box>
73
+ </Paper>
74
+ )
75
+ }