@nocobase/portal-template-default 1.0.1 → 1.0.3

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 (186) hide show
  1. package/README.MD +22 -3
  2. package/package.json +4 -2
  3. package/registry/nocobase-acl/components/role-options.ts +7 -0
  4. package/registry/nocobase-acl/components/role-switcher.tsx +9 -4
  5. package/registry/nocobase-acl/demo/boundary-api.tsx +87 -16
  6. package/registry/nocobase-acl/demo/components.tsx +32 -11
  7. package/registry/nocobase-acl/demo/index.tsx +45 -14
  8. package/registry/nocobase-acl/demo/lazy-route.tsx +2 -7
  9. package/registry/nocobase-acl/demo/prompt-generator.tsx +52 -33
  10. package/registry/nocobase-acl/demo/role-switcher-prompt-generator.tsx +28 -9
  11. package/registry/nocobase-acl/extension.tsx +1 -1
  12. package/registry/nocobase-acl/tests/acl-regression.mjs +7 -6
  13. package/registry/nocobase-ai/components/chat/chat-messages.tsx +3 -4
  14. package/registry/nocobase-ai/components/chat/conversation-list.tsx +2 -4
  15. package/registry/nocobase-ai/components/tools/business-report-dialog.tsx +16 -19
  16. package/registry/nocobase-ai/components/tools/chart-renderer.tsx +3 -14
  17. package/registry/nocobase-ai/components/tools/echarts-preview.tsx +2 -5
  18. package/registry/nocobase-ai/demo/configuration-gate.tsx +9 -12
  19. package/registry/nocobase-ai/extension.tsx +2 -7
  20. package/registry/nocobase-i18n/README.md +53 -0
  21. package/registry/nocobase-i18n/components/index.ts +1 -0
  22. package/registry/nocobase-i18n/components/language-switcher.tsx +138 -0
  23. package/registry/nocobase-i18n/demo/index.tsx +209 -0
  24. package/registry/nocobase-i18n/demo/prompt-generator.tsx +155 -0
  25. package/registry/nocobase-i18n/extension.tsx +46 -0
  26. package/registry/nocobase-i18n/i18n-provider.ts +9 -0
  27. package/registry/nocobase-i18n/index.ts +6 -0
  28. package/registry/nocobase-i18n/locale-store.ts +134 -0
  29. package/registry/nocobase-i18n/locales/en-US.ts +89 -0
  30. package/registry/nocobase-i18n/locales/zh-CN.ts +85 -0
  31. package/registry/nocobase-i18n/provider.tsx +32 -0
  32. package/registry/nocobase-i18n/runtime.ts +118 -0
  33. package/registry/nocobase-i18n/server-resources.ts +184 -0
  34. package/registry/nocobase-i18n/tests/i18n-regression.mjs +42 -0
  35. package/registry.config.json +34 -14
  36. package/scripts/registry.mjs +5 -0
  37. package/src/App.tsx +27 -86
  38. package/src/app/extension.ts +3 -1
  39. package/src/app/extensions.tsx +21 -0
  40. package/src/components/access-control/acl-gate.tsx +4 -10
  41. package/src/components/app-shell/breadcrumb.tsx +39 -3
  42. package/src/components/app-shell/document-title-handler.tsx +11 -20
  43. package/src/components/app-shell/header.tsx +18 -3
  44. package/src/components/app-shell/loading-overlay.tsx +2 -4
  45. package/src/components/app-shell/loading-state.tsx +15 -0
  46. package/src/components/app-shell/sidebar.tsx +75 -47
  47. package/src/components/data-table/data-table-pagination.tsx +27 -7
  48. package/src/components/data-table/data-table-styles.ts +57 -0
  49. package/src/components/data-table/data-table.tsx +14 -59
  50. package/src/components/resources/resource-label.ts +101 -0
  51. package/src/components/resources/views/create-view.tsx +17 -14
  52. package/src/components/resources/views/edit-view.tsx +17 -15
  53. package/src/components/resources/views/list-view.tsx +20 -10
  54. package/src/components/resources/views/show-view.tsx +20 -18
  55. package/src/extensions/nocobase-acl/README.md +19 -0
  56. package/src/extensions/nocobase-acl/components/acl-boundary.tsx +106 -0
  57. package/src/extensions/nocobase-acl/components/index.ts +3 -0
  58. package/src/extensions/nocobase-acl/components/role-options.ts +36 -0
  59. package/src/extensions/nocobase-acl/components/role-switcher.tsx +135 -0
  60. package/src/extensions/nocobase-acl/demo/boundary-api.tsx +186 -0
  61. package/src/extensions/nocobase-acl/demo/components.tsx +231 -0
  62. package/src/extensions/nocobase-acl/demo/index.tsx +505 -0
  63. package/src/extensions/nocobase-acl/demo/lazy-route.tsx +10 -0
  64. package/src/extensions/nocobase-acl/demo/prompt-generator.tsx +134 -0
  65. package/src/extensions/nocobase-acl/demo/role-switcher-prompt-generator.tsx +198 -0
  66. package/src/extensions/nocobase-acl/demo/scenario-section.tsx +37 -0
  67. package/src/extensions/nocobase-acl/extension.tsx +71 -0
  68. package/src/extensions/nocobase-acl/index.ts +1 -0
  69. package/src/extensions/nocobase-ai/README.md +70 -0
  70. package/src/extensions/nocobase-ai/adapters/react-hook-form.ts +20 -0
  71. package/src/extensions/nocobase-ai/assets/nocobase-ai-chat.svg +50 -0
  72. package/src/extensions/nocobase-ai/components/ai-root-provider.tsx +32 -0
  73. package/src/extensions/nocobase-ai/components/chat/ai-employee-avatar.tsx +31 -0
  74. package/src/extensions/nocobase-ai/components/chat/chat-attachment.tsx +92 -0
  75. package/src/extensions/nocobase-ai/components/chat/chat-compact.tsx +90 -0
  76. package/src/extensions/nocobase-ai/components/chat/chat-composer.tsx +429 -0
  77. package/src/extensions/nocobase-ai/components/chat/chat-empty-state.tsx +51 -0
  78. package/src/extensions/nocobase-ai/components/chat/chat-header.tsx +88 -0
  79. package/src/extensions/nocobase-ai/components/chat/chat-history-dialog.tsx +77 -0
  80. package/src/extensions/nocobase-ai/components/chat/chat-message.tsx +252 -0
  81. package/src/extensions/nocobase-ai/components/chat/chat-messages.tsx +173 -0
  82. package/src/extensions/nocobase-ai/components/chat/chat-window.tsx +118 -0
  83. package/src/extensions/nocobase-ai/components/chat/conversation-list.tsx +343 -0
  84. package/src/extensions/nocobase-ai/components/chat/markdown-message.tsx +165 -0
  85. package/src/extensions/nocobase-ai/components/chat/model-select-options.tsx +30 -0
  86. package/src/extensions/nocobase-ai/components/chat/reasoning-panel.tsx +29 -0
  87. package/src/extensions/nocobase-ai/components/chat/sub-agent-conversation.tsx +142 -0
  88. package/src/extensions/nocobase-ai/components/chat/tool-call-card.tsx +316 -0
  89. package/src/extensions/nocobase-ai/components/chat/user-prompt-editor.tsx +99 -0
  90. package/src/extensions/nocobase-ai/components/chat/work-context-chip.tsx +114 -0
  91. package/src/extensions/nocobase-ai/components/index.ts +59 -0
  92. package/src/extensions/nocobase-ai/components/page-elements/ai-form.tsx +44 -0
  93. package/src/extensions/nocobase-ai/components/page-elements/page-element-provider.tsx +469 -0
  94. package/src/extensions/nocobase-ai/components/surfaces/chat-dialog.tsx +31 -0
  95. package/src/extensions/nocobase-ai/components/surfaces/chat-inline.tsx +18 -0
  96. package/src/extensions/nocobase-ai/components/surfaces/chat-page.tsx +18 -0
  97. package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +54 -0
  98. package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel.tsx +74 -0
  99. package/src/extensions/nocobase-ai/components/surfaces/chat-surface-actions.tsx +54 -0
  100. package/src/extensions/nocobase-ai/components/tools/builtin-tool-renderers.tsx +31 -0
  101. package/src/extensions/nocobase-ai/components/tools/business-report-dialog.tsx +364 -0
  102. package/src/extensions/nocobase-ai/components/tools/business-report-renderer.tsx +122 -0
  103. package/src/extensions/nocobase-ai/components/tools/business-report-utils.tsx +282 -0
  104. package/src/extensions/nocobase-ai/components/tools/chart-renderer.tsx +99 -0
  105. package/src/extensions/nocobase-ai/components/tools/echarts-preview.tsx +59 -0
  106. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-advanced.ts +18 -0
  107. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-common.ts +20 -0
  108. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-components.ts +34 -0
  109. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +24 -0
  110. package/src/extensions/nocobase-ai/components/tools/echarts-runtime.ts +138 -0
  111. package/src/extensions/nocobase-ai/components/tools/sub-agent-renderer.tsx +67 -0
  112. package/src/extensions/nocobase-ai/components/tools/suggestions-renderer.tsx +59 -0
  113. package/src/extensions/nocobase-ai/components/tools/tool-renderer-provider.tsx +55 -0
  114. package/src/extensions/nocobase-ai/components/tools/tool-renderer-utils.ts +18 -0
  115. package/src/extensions/nocobase-ai/components/tools/workflow-renderer.tsx +108 -0
  116. package/src/extensions/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +73 -0
  117. package/src/extensions/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +149 -0
  118. package/src/extensions/nocobase-ai/demo/configuration-gate.tsx +49 -0
  119. package/src/extensions/nocobase-ai/demo/container-showcase.tsx +210 -0
  120. package/src/extensions/nocobase-ai/demo/floating.tsx +134 -0
  121. package/src/extensions/nocobase-ai/demo/index.tsx +346 -0
  122. package/src/extensions/nocobase-ai/demo/interaction-showcase.tsx +151 -0
  123. package/src/extensions/nocobase-ai/demo/page-context-prompt-generator.tsx +355 -0
  124. package/src/extensions/nocobase-ai/demo/page-context.tsx +884 -0
  125. package/src/extensions/nocobase-ai/demo/page-element-showcase.tsx +197 -0
  126. package/src/extensions/nocobase-ai/demo/prompt-card.tsx +41 -0
  127. package/src/extensions/nocobase-ai/demo/prompt-generator.tsx +452 -0
  128. package/src/extensions/nocobase-ai/demo/shortcut.tsx +1207 -0
  129. package/src/extensions/nocobase-ai/demo/tool-cards.tsx +530 -0
  130. package/src/extensions/nocobase-ai/extension.tsx +158 -0
  131. package/src/extensions/nocobase-ai/global-ai-chat.tsx +213 -0
  132. package/src/extensions/nocobase-ai/index.ts +3 -0
  133. package/src/extensions/nocobase-ai/providers/ai-provider.tsx +323 -0
  134. package/src/extensions/nocobase-ai/providers/avatars.ts +817 -0
  135. package/src/extensions/nocobase-ai/providers/chat-context.tsx +109 -0
  136. package/src/extensions/nocobase-ai/providers/chat-controller.ts +84 -0
  137. package/src/extensions/nocobase-ai/providers/chat-message-utils.ts +198 -0
  138. package/src/extensions/nocobase-ai/providers/chat-provider.tsx +871 -0
  139. package/src/extensions/nocobase-ai/providers/chat-reducer.ts +147 -0
  140. package/src/extensions/nocobase-ai/providers/chat-task-utils.ts +105 -0
  141. package/src/extensions/nocobase-ai/providers/chat-transport.ts +184 -0
  142. package/src/extensions/nocobase-ai/providers/form-registry.tsx +271 -0
  143. package/src/extensions/nocobase-ai/providers/frontend-tool-registry.tsx +231 -0
  144. package/src/extensions/nocobase-ai/providers/index.ts +61 -0
  145. package/src/extensions/nocobase-ai/providers/model.ts +32 -0
  146. package/src/extensions/nocobase-ai/providers/page-context.tsx +136 -0
  147. package/src/extensions/nocobase-ai/providers/stream-coalescer.ts +60 -0
  148. package/src/extensions/nocobase-ai/providers/stream-event-utils.ts +82 -0
  149. package/src/extensions/nocobase-ai/providers/stream-parser.ts +61 -0
  150. package/src/extensions/nocobase-ai/providers/sub-agent-stream.ts +316 -0
  151. package/src/extensions/nocobase-ai/providers/types.ts +183 -0
  152. package/src/extensions/nocobase-ai/providers/ui-message-stream.ts +463 -0
  153. package/src/extensions/nocobase-ai/providers/use-automatic-tool-approval.ts +94 -0
  154. package/src/extensions/nocobase-ai/providers/use-chat-attachments.ts +206 -0
  155. package/src/extensions/nocobase-ai/providers/use-chat-message-actions.ts +554 -0
  156. package/src/extensions/nocobase-ai/providers/use-chat-runtime.ts +210 -0
  157. package/src/extensions/nocobase-ai/providers/use-chat-work-context.ts +93 -0
  158. package/src/extensions/nocobase-ai/providers/use-conversation-catalog.ts +108 -0
  159. package/src/extensions/nocobase-ai/providers/use-conversation-history.ts +178 -0
  160. package/src/extensions/nocobase-ai/services/index.ts +7 -0
  161. package/src/extensions/nocobase-ai/services/nocobase-ai-service.ts +454 -0
  162. package/src/extensions/nocobase-ai/services/types.ts +82 -0
  163. package/src/index.tsx +2 -3
  164. package/src/lib/i18n.ts +161 -0
  165. package/src/lib/nocobase/client.ts +19 -2
  166. package/src/locales/en-US.ts +82 -0
  167. package/src/locales/index.ts +13 -0
  168. package/src/locales/zh-CN.ts +79 -0
  169. package/src/pages/users/create.tsx +98 -0
  170. package/src/pages/users/edit.tsx +92 -0
  171. package/src/pages/users/form-context.ts +57 -0
  172. package/src/pages/users/form-fields.tsx +211 -0
  173. package/src/pages/users/list.tsx +205 -0
  174. package/src/pages/users/show.tsx +187 -0
  175. package/src/pages/users/types.ts +20 -0
  176. package/src/providers/constants.ts +1 -0
  177. package/src/pages/blog-posts/create.tsx +0 -177
  178. package/src/pages/blog-posts/edit.tsx +0 -197
  179. package/src/pages/blog-posts/list.tsx +0 -131
  180. package/src/pages/blog-posts/show.tsx +0 -81
  181. package/src/pages/categories/create.tsx +0 -73
  182. package/src/pages/categories/edit.tsx +0 -73
  183. package/src/pages/categories/index.ts +0 -4
  184. package/src/pages/categories/list.tsx +0 -86
  185. package/src/pages/categories/show.tsx +0 -37
  186. /package/src/pages/{blog-posts → users}/index.ts +0 -0
@@ -0,0 +1,77 @@
1
+ import { Button } from "@/components/ui/button";
2
+ import {
3
+ Dialog,
4
+ DialogContent,
5
+ DialogDescription,
6
+ DialogTitle,
7
+ } from "@/components/ui/dialog";
8
+ import { History, Menu } from "lucide-react";
9
+ import { useEffect, useState, type ReactNode } from "react";
10
+ import type { AIToolCallDecision } from "../../providers";
11
+ import { ChatMessages } from "./chat-messages";
12
+ import { ConversationList } from "./conversation-list";
13
+
14
+ export function AIChatHistoryDialog({
15
+ open: controlledOpen,
16
+ onOpenChange,
17
+ trigger,
18
+ onToolCallDecision,
19
+ }: {
20
+ open?: boolean;
21
+ onOpenChange?: (open: boolean) => void;
22
+ trigger?: ReactNode;
23
+ onToolCallDecision?: (decision: AIToolCallDecision) => void | Promise<void>;
24
+ }) {
25
+ const [internalOpen, setInternalOpen] = useState(false);
26
+ const open = controlledOpen ?? internalOpen;
27
+ const setOpen = onOpenChange ?? setInternalOpen;
28
+ const [conversationListVisible, setConversationListVisible] = useState(true);
29
+
30
+ useEffect(() => {
31
+ if (open) setConversationListVisible(true);
32
+ }, [open]);
33
+
34
+ return (
35
+ <>
36
+ {trigger === null ? null : (
37
+ <span onClick={() => setOpen(true)}>
38
+ {trigger ?? (
39
+ <Button variant="ghost" size="sm">
40
+ <History /> Message history
41
+ </Button>
42
+ )}
43
+ </span>
44
+ )}
45
+ <Dialog open={open} onOpenChange={setOpen}>
46
+ <DialogContent className="flex h-[82svh] w-[min(1040px,calc(100vw-2rem))] max-w-[1040px] flex-col gap-0 overflow-hidden p-0 sm:max-w-[1040px]">
47
+ <div className="flex h-12 shrink-0 items-center gap-2 border-b px-3 pr-12">
48
+ {!conversationListVisible ? (
49
+ <Button
50
+ variant="ghost"
51
+ size="icon-sm"
52
+ aria-label="Show conversations"
53
+ onClick={() => setConversationListVisible(true)}
54
+ >
55
+ <Menu />
56
+ </Button>
57
+ ) : null}
58
+ <DialogTitle>Message history</DialogTitle>
59
+ </div>
60
+ <DialogDescription className="sr-only">
61
+ Browse conversations and inspect the full message history.
62
+ </DialogDescription>
63
+ <div className="flex min-h-0 flex-1 overflow-hidden">
64
+ {conversationListVisible ? (
65
+ <aside className="absolute top-12 bottom-0 left-0 z-20 w-[280px] border-r bg-card sm:relative sm:inset-auto sm:z-auto sm:block sm:w-[300px] sm:shrink-0">
66
+ <ConversationList
67
+ onClose={() => setConversationListVisible(false)}
68
+ />
69
+ </aside>
70
+ ) : null}
71
+ <ChatMessages onToolCallDecision={onToolCallDecision} />
72
+ </div>
73
+ </DialogContent>
74
+ </Dialog>
75
+ </>
76
+ );
77
+ }
@@ -0,0 +1,252 @@
1
+ import { Button } from "@/components/ui/button";
2
+ import {
3
+ type AIChatMessage as AIChatMessageType,
4
+ type AIToolCallDecision,
5
+ } from "../../providers";
6
+ import { Check, Copy, Pencil, RefreshCcw } from "lucide-react";
7
+ import { memo, useState } from "react";
8
+ import { MarkdownMessage } from "./markdown-message";
9
+ import { ReasoningPanel } from "./reasoning-panel";
10
+ import {
11
+ getToolCallName,
12
+ isToolCallPart,
13
+ ToolCallCard,
14
+ } from "./tool-call-card";
15
+ import { ChatAttachment } from "./chat-attachment";
16
+ import { useAIToolRenderer } from "../tools/tool-renderer-provider";
17
+ import { SubAgentConversation } from "./sub-agent-conversation";
18
+ import { WorkContextChip } from "./work-context-chip";
19
+
20
+ type ChatMessageProps = {
21
+ message: AIChatMessageType;
22
+ onToolCallDecision?: (decision: AIToolCallDecision) => void | Promise<void>;
23
+ showActions?: boolean;
24
+ status?: "submitted" | "streaming" | "ready" | "error";
25
+ retryMessage?: (message: AIChatMessageType) => Promise<void>;
26
+ decideToolCall?: (decision: AIToolCallDecision) => Promise<void>;
27
+ startEditingMessage?: (message: AIChatMessageType) => Promise<void>;
28
+ focusComposer?: () => void;
29
+ };
30
+
31
+ function ChatMessageComponent({
32
+ message,
33
+ onToolCallDecision,
34
+ showActions = true,
35
+ status = "ready",
36
+ retryMessage,
37
+ decideToolCall,
38
+ startEditingMessage,
39
+ focusComposer,
40
+ }: ChatMessageProps) {
41
+ const interactionPending = status === "streaming" || status === "submitted";
42
+ const [copied, setCopied] = useState(false);
43
+ const isUser = message.role === "user";
44
+ const text = message.parts
45
+ .filter((part) => part.type === "text")
46
+ .map((part) => part.text)
47
+ .join("\n");
48
+ const toolCalls = message.parts.filter(isToolCallPart);
49
+ const singleToolCall = toolCalls.length === 1 ? toolCalls[0] : undefined;
50
+ const singleToolRenderer = useAIToolRenderer(
51
+ singleToolCall ? getToolCallName(singleToolCall) : ""
52
+ );
53
+ const useInlineToolActions =
54
+ !text && Boolean(singleToolCall) && !singleToolRenderer;
55
+ const assistantParts = message.parts.filter(
56
+ (part) =>
57
+ part.type === "text" ||
58
+ part.type === "reasoning" ||
59
+ part.type === "data-subAgent" ||
60
+ isToolCallPart(part)
61
+ );
62
+ const showGenerating = !assistantParts.length && interactionPending;
63
+ const attachments = message.metadata?.attachments ?? [];
64
+ const workContext = message.metadata?.workContext ?? [];
65
+ if (isUser) {
66
+ return (
67
+ <article className="group/message flex flex-col items-end px-4 py-2 sm:px-5">
68
+ <div className="flex max-w-[80%] flex-col items-end">
69
+ {workContext.length ? (
70
+ <div className="mb-1.5 flex max-w-full flex-wrap justify-end gap-1.5">
71
+ {workContext.map((item, index) => (
72
+ <WorkContextChip
73
+ key={`${item.type}:${item.id ?? item.title ?? index}`}
74
+ item={item}
75
+ className="bg-background"
76
+ />
77
+ ))}
78
+ </div>
79
+ ) : null}
80
+ {attachments.length ? (
81
+ <div className="mb-1.5 flex max-w-full flex-wrap justify-end gap-1.5">
82
+ {attachments.map((attachment) => (
83
+ <ChatAttachment key={attachment.uid} attachment={attachment} />
84
+ ))}
85
+ </div>
86
+ ) : null}
87
+ {text ? (
88
+ <div className="rounded-2xl rounded-br-md bg-secondary px-4 py-2.5 text-sm leading-6 text-secondary-foreground">
89
+ <div className="whitespace-pre-wrap break-words">{text}</div>
90
+ </div>
91
+ ) : null}
92
+ </div>
93
+ {showActions ? (
94
+ <div className="pointer-events-none mt-1 flex h-6 items-center gap-1 opacity-0 transition-opacity group-hover/message:pointer-events-auto group-hover/message:opacity-100 focus-within:pointer-events-auto focus-within:opacity-100">
95
+ <Button
96
+ variant="ghost"
97
+ size="icon-xs"
98
+ aria-label="Edit message"
99
+ disabled={interactionPending || !startEditingMessage}
100
+ onClick={() => void startEditingMessage?.(message)}
101
+ >
102
+ <Pencil />
103
+ </Button>
104
+ {text ? (
105
+ <Button
106
+ variant="ghost"
107
+ size="icon-xs"
108
+ aria-label="Copy message"
109
+ onClick={async () => {
110
+ await navigator.clipboard.writeText(text);
111
+ setCopied(true);
112
+ window.setTimeout(() => setCopied(false), 1200);
113
+ }}
114
+ >
115
+ {copied ? <Check /> : <Copy />}
116
+ </Button>
117
+ ) : null}
118
+ </div>
119
+ ) : null}
120
+ </article>
121
+ );
122
+ }
123
+
124
+ const messageActions = (
125
+ <>
126
+ {text ? (
127
+ <Button
128
+ variant="ghost"
129
+ size="icon-xs"
130
+ aria-label="Copy response"
131
+ onClick={async () => {
132
+ await navigator.clipboard.writeText(text);
133
+ setCopied(true);
134
+ window.setTimeout(() => setCopied(false), 1200);
135
+ }}
136
+ >
137
+ {copied ? <Check /> : <Copy />}
138
+ </Button>
139
+ ) : null}
140
+ <Button
141
+ variant="ghost"
142
+ size="icon-xs"
143
+ aria-label="Retry response"
144
+ disabled={interactionPending || !retryMessage}
145
+ onClick={() => void retryMessage?.(message)}
146
+ >
147
+ <RefreshCcw />
148
+ </Button>
149
+ </>
150
+ );
151
+
152
+ return (
153
+ <article className="group/message px-4 py-2 sm:px-5">
154
+ <div className="min-w-0 space-y-4">
155
+ {assistantParts.map((part, index) => {
156
+ if (part.type === "reasoning") {
157
+ return (
158
+ <ReasoningPanel
159
+ key={`reasoning-${index}`}
160
+ streaming={part.state === "streaming"}
161
+ >
162
+ {part.text}
163
+ </ReasoningPanel>
164
+ );
165
+ }
166
+ if (part.type === "text") {
167
+ return (
168
+ <div
169
+ key={`text-${index}`}
170
+ className="ai-markdown text-sm leading-6 text-foreground"
171
+ >
172
+ <MarkdownMessage>{part.text}</MarkdownMessage>
173
+ </div>
174
+ );
175
+ }
176
+ if (part.type === "data-subAgent") {
177
+ return (
178
+ <SubAgentConversation
179
+ key={part.id ?? part.data.sessionId}
180
+ conversation={part.data}
181
+ onToolCallDecision={onToolCallDecision}
182
+ status={status}
183
+ decideToolCall={decideToolCall}
184
+ focusComposer={focusComposer}
185
+ />
186
+ );
187
+ }
188
+ if (!isToolCallPart(part)) return null;
189
+ return (
190
+ <ToolCallCard
191
+ key={part.toolCallId}
192
+ part={part}
193
+ approval={message.metadata?.toolApprovals?.[part.toolCallId]}
194
+ disabled={interactionPending}
195
+ onRevise={focusComposer}
196
+ inlineActions={
197
+ showActions && useInlineToolActions && part === singleToolCall
198
+ ? messageActions
199
+ : undefined
200
+ }
201
+ onDecision={async (decision, input) => {
202
+ const toolDecision = {
203
+ messageId: message.id,
204
+ toolCallId: part.toolCallId,
205
+ toolName: getToolCallName(part),
206
+ decision,
207
+ input,
208
+ } satisfies AIToolCallDecision;
209
+ await decideToolCall?.(toolDecision);
210
+ try {
211
+ await onToolCallDecision?.(toolDecision);
212
+ } catch (error) {
213
+ console.error("Tool-call decision callback failed", error);
214
+ }
215
+ }}
216
+ />
217
+ );
218
+ })}
219
+ {showGenerating ? (
220
+ <div className="min-h-6 text-sm leading-6 text-foreground">
221
+ <span
222
+ className="inline-flex gap-1 py-2"
223
+ aria-label="Generating response"
224
+ >
225
+ <span className="size-1.5 animate-pulse rounded-full bg-muted-foreground/70" />
226
+ <span className="size-1.5 animate-pulse rounded-full bg-muted-foreground/70 [animation-delay:120ms]" />
227
+ <span className="size-1.5 animate-pulse rounded-full bg-muted-foreground/70 [animation-delay:240ms]" />
228
+ </span>
229
+ </div>
230
+ ) : null}
231
+ </div>
232
+ {showActions && (text || (toolCalls.length && !useInlineToolActions)) ? (
233
+ <div className="pointer-events-none mt-1 flex h-6 items-center gap-1 opacity-0 transition-opacity group-hover/message:pointer-events-auto group-hover/message:opacity-100 focus-within:pointer-events-auto focus-within:opacity-100">
234
+ {messageActions}
235
+ </div>
236
+ ) : null}
237
+ </article>
238
+ );
239
+ }
240
+
241
+ export const ChatMessage = memo(
242
+ ChatMessageComponent,
243
+ (previous, next) =>
244
+ previous.message === next.message &&
245
+ previous.status === next.status &&
246
+ previous.showActions === next.showActions &&
247
+ previous.onToolCallDecision === next.onToolCallDecision &&
248
+ previous.retryMessage === next.retryMessage &&
249
+ previous.decideToolCall === next.decideToolCall &&
250
+ previous.startEditingMessage === next.startEditingMessage &&
251
+ previous.focusComposer === next.focusComposer
252
+ );
@@ -0,0 +1,173 @@
1
+ import { Button } from "@/components/ui/button";
2
+ import { LoadingState } from "@/components/app-shell/loading-state";
3
+ import {
4
+ useAIChatBase,
5
+ useAIChatMessages,
6
+ useAIChatStatus,
7
+ type AIChatMessage,
8
+ type AIToolCallDecision,
9
+ } from "../../providers";
10
+ import { cn } from "@/lib/utils";
11
+ import { ArrowDown } from "lucide-react";
12
+ import { useEffect, useRef, useState, type ReactNode } from "react";
13
+ import { ChatEmptyState } from "./chat-empty-state";
14
+ import { ChatMessage } from "./chat-message";
15
+
16
+ export function ChatMessages({
17
+ onToolCallDecision,
18
+ }: {
19
+ onToolCallDecision?: (decision: AIToolCallDecision) => void | Promise<void>;
20
+ }) {
21
+ const { messages } = useAIChatMessages();
22
+ const { status, error } = useAIChatStatus();
23
+ const {
24
+ messagesLoading,
25
+ historyError,
26
+ interactionError,
27
+ retryMessage,
28
+ decideToolCall,
29
+ startEditingMessage,
30
+ focusComposer,
31
+ } = useAIChatBase();
32
+
33
+ return (
34
+ <AIChatMessageList
35
+ messages={messages}
36
+ status={status}
37
+ loading={messagesLoading}
38
+ error={error}
39
+ historyError={historyError}
40
+ interactionError={interactionError}
41
+ onToolCallDecision={onToolCallDecision}
42
+ retryMessage={retryMessage}
43
+ decideToolCall={decideToolCall}
44
+ startEditingMessage={startEditingMessage}
45
+ focusComposer={focusComposer}
46
+ />
47
+ );
48
+ }
49
+
50
+ export type AIChatMessageListProps = {
51
+ messages: AIChatMessage[];
52
+ status?: "submitted" | "streaming" | "ready" | "error";
53
+ loading?: boolean;
54
+ error?: Error | null;
55
+ historyError?: Error | null;
56
+ interactionError?: Error | null;
57
+ emptyState?: ReactNode;
58
+ className?: string;
59
+ onToolCallDecision?: (decision: AIToolCallDecision) => void | Promise<void>;
60
+ showMessageActions?: boolean;
61
+ retryMessage?: (message: AIChatMessage) => Promise<void>;
62
+ decideToolCall?: (decision: AIToolCallDecision) => Promise<void>;
63
+ startEditingMessage?: (message: AIChatMessage) => Promise<void>;
64
+ focusComposer?: () => void;
65
+ };
66
+
67
+ export function AIChatMessageList({
68
+ messages,
69
+ status = "ready",
70
+ loading = false,
71
+ error,
72
+ historyError,
73
+ interactionError,
74
+ emptyState,
75
+ className,
76
+ onToolCallDecision,
77
+ showMessageActions = true,
78
+ retryMessage,
79
+ decideToolCall,
80
+ startEditingMessage,
81
+ focusComposer,
82
+ }: AIChatMessageListProps) {
83
+ const viewportRef = useRef<HTMLDivElement>(null);
84
+ const [atBottom, setAtBottom] = useState(true);
85
+
86
+ const scrollToBottom = (behavior: ScrollBehavior = "smooth") => {
87
+ const viewport = viewportRef.current;
88
+ if (!viewport) return;
89
+ viewport.scrollTo({ top: viewport.scrollHeight, behavior });
90
+ };
91
+
92
+ useEffect(() => {
93
+ if (atBottom) scrollToBottom(status === "streaming" ? "auto" : "smooth");
94
+ }, [atBottom, messages, status]);
95
+
96
+ return (
97
+ <div className={cn("relative min-h-0 flex-1 bg-background", className)}>
98
+ <div
99
+ ref={viewportRef}
100
+ role="log"
101
+ aria-live="polite"
102
+ className="absolute inset-0 overflow-y-auto overscroll-contain"
103
+ onScroll={(event) => {
104
+ const element = event.currentTarget;
105
+ setAtBottom(
106
+ element.scrollHeight - element.scrollTop - element.clientHeight < 48
107
+ );
108
+ }}
109
+ >
110
+ {loading ? (
111
+ <LoadingState className="h-full" />
112
+ ) : (
113
+ <>
114
+ {messages.length ? (
115
+ <div className="mx-auto w-full max-w-3xl py-2">
116
+ {messages.map((message) => (
117
+ <ChatMessage
118
+ key={message.id}
119
+ message={message}
120
+ onToolCallDecision={onToolCallDecision}
121
+ showActions={showMessageActions}
122
+ status={status}
123
+ retryMessage={retryMessage}
124
+ decideToolCall={decideToolCall}
125
+ startEditingMessage={startEditingMessage}
126
+ focusComposer={focusComposer}
127
+ />
128
+ ))}
129
+ </div>
130
+ ) : (
131
+ emptyState ?? <ChatEmptyState />
132
+ )}
133
+ {error ? (
134
+ <div
135
+ role="alert"
136
+ className="mx-5 my-3 rounded-lg border border-destructive/25 bg-destructive/5 px-3 py-2 text-sm text-destructive"
137
+ >
138
+ {error.message}
139
+ </div>
140
+ ) : null}
141
+ {historyError ? (
142
+ <div
143
+ role="alert"
144
+ className="mx-5 my-3 rounded-lg border border-destructive/25 bg-destructive/5 px-3 py-2 text-sm text-destructive"
145
+ >
146
+ {historyError.message}
147
+ </div>
148
+ ) : null}
149
+ {interactionError ? (
150
+ <div
151
+ role="alert"
152
+ className="mx-5 my-3 rounded-lg border border-destructive/25 bg-destructive/5 px-3 py-2 text-sm text-destructive"
153
+ >
154
+ {interactionError.message}
155
+ </div>
156
+ ) : null}
157
+ </>
158
+ )}
159
+ </div>
160
+ {!atBottom ? (
161
+ <Button
162
+ size="icon-sm"
163
+ variant="outline"
164
+ className="absolute bottom-3 left-1/2 z-10 -translate-x-1/2 rounded-full bg-background shadow-sm"
165
+ aria-label="Scroll to bottom"
166
+ onClick={() => scrollToBottom()}
167
+ >
168
+ <ArrowDown />
169
+ </Button>
170
+ ) : null}
171
+ </div>
172
+ );
173
+ }
@@ -0,0 +1,118 @@
1
+ import { cn } from "@/lib/utils";
2
+ import { useAIChatBase, type AIToolCallDecision } from "../../providers";
3
+ import { useRef, useState, type DragEvent, type ReactNode } from "react";
4
+ import { ChatComposer, type AIChatComposerAction } from "./chat-composer";
5
+ import { ChatHeader } from "./chat-header";
6
+ import { ChatMessages } from "./chat-messages";
7
+ import { ConversationList } from "./conversation-list";
8
+
9
+ export function AIChatWindow({
10
+ className,
11
+ headerActions,
12
+ composerActions,
13
+ showConversationToggle = true,
14
+ showNewConversation = true,
15
+ showEmployeeSelector = true,
16
+ showModelSelector = true,
17
+ showUserPrompt = true,
18
+ enableAttachments = false,
19
+ attachmentActionIndex = 0,
20
+ placeholder,
21
+ disclaimer,
22
+ onToolCallDecision,
23
+ }: AIChatWindowProps) {
24
+ const { conversationListOpen, setConversationListOpen, uploadFiles } =
25
+ useAIChatBase();
26
+ const [draggingFiles, setDraggingFiles] = useState(false);
27
+ const dragDepthRef = useRef(0);
28
+
29
+ const hasFiles = (event: DragEvent) =>
30
+ Array.from(event.dataTransfer.types).includes("Files");
31
+
32
+ return (
33
+ <section
34
+ className={cn(
35
+ "ai-chat-window relative flex h-full min-h-0 w-full overflow-hidden bg-background text-foreground",
36
+ className
37
+ )}
38
+ onDragEnter={(event) => {
39
+ if (!enableAttachments || !hasFiles(event)) return;
40
+ event.preventDefault();
41
+ dragDepthRef.current += 1;
42
+ setDraggingFiles(true);
43
+ }}
44
+ onDragOver={(event) => {
45
+ if (!enableAttachments || !hasFiles(event)) return;
46
+ event.preventDefault();
47
+ event.dataTransfer.dropEffect = "copy";
48
+ }}
49
+ onDragLeave={(event) => {
50
+ if (!enableAttachments || !hasFiles(event)) return;
51
+ event.preventDefault();
52
+ dragDepthRef.current = Math.max(0, dragDepthRef.current - 1);
53
+ if (dragDepthRef.current === 0) setDraggingFiles(false);
54
+ }}
55
+ onDrop={(event) => {
56
+ if (!enableAttachments || !hasFiles(event)) return;
57
+ event.preventDefault();
58
+ dragDepthRef.current = 0;
59
+ setDraggingFiles(false);
60
+ const files = Array.from(event.dataTransfer.files);
61
+ if (files.length) void uploadFiles(files);
62
+ }}
63
+ >
64
+ {draggingFiles ? (
65
+ <div className="pointer-events-none absolute inset-2 z-50 flex items-center justify-center rounded-xl border-2 border-dashed border-foreground/25 bg-background/90 text-sm font-medium shadow-sm backdrop-blur-sm">
66
+ Drop files to upload
67
+ </div>
68
+ ) : null}
69
+ {conversationListOpen && showConversationToggle ? (
70
+ <button
71
+ type="button"
72
+ className="ai-chat-conversation-backdrop absolute inset-0 z-20 bg-black/25 backdrop-blur-[1px]"
73
+ aria-label="Close conversation list"
74
+ onClick={() => setConversationListOpen(false)}
75
+ />
76
+ ) : null}
77
+ {conversationListOpen && showConversationToggle ? (
78
+ <aside className="ai-chat-conversation-panel z-30 flex h-full w-[300px] shrink-0 border-r bg-card">
79
+ <ConversationList />
80
+ </aside>
81
+ ) : null}
82
+ <div className="flex min-w-0 flex-1 flex-col">
83
+ <ChatHeader
84
+ actions={headerActions}
85
+ showConversationToggle={showConversationToggle}
86
+ showNewConversation={showNewConversation}
87
+ showUserPrompt={showUserPrompt}
88
+ />
89
+ <ChatMessages onToolCallDecision={onToolCallDecision} />
90
+ <ChatComposer
91
+ actions={composerActions}
92
+ showEmployeeSelector={showEmployeeSelector}
93
+ showModelSelector={showModelSelector}
94
+ enableAttachments={enableAttachments}
95
+ attachmentActionIndex={attachmentActionIndex}
96
+ placeholder={placeholder}
97
+ disclaimer={disclaimer}
98
+ />
99
+ </div>
100
+ </section>
101
+ );
102
+ }
103
+
104
+ export type AIChatWindowProps = {
105
+ className?: string;
106
+ headerActions?: ReactNode;
107
+ composerActions?: AIChatComposerAction[];
108
+ showConversationToggle?: boolean;
109
+ showNewConversation?: boolean;
110
+ showEmployeeSelector?: boolean;
111
+ showModelSelector?: boolean;
112
+ showUserPrompt?: boolean;
113
+ enableAttachments?: boolean;
114
+ attachmentActionIndex?: number;
115
+ placeholder?: string;
116
+ disclaimer?: ReactNode | false;
117
+ onToolCallDecision?: (decision: AIToolCallDecision) => void | Promise<void>;
118
+ };