@nocobase/portal-template-default 1.0.0 → 1.0.1

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 (145) hide show
  1. package/README.MD +70 -0
  2. package/components.json +3 -1
  3. package/package.json +19 -5
  4. package/registry/nocobase-acl/README.md +19 -0
  5. package/registry/nocobase-acl/components/acl-boundary.tsx +106 -0
  6. package/registry/nocobase-acl/components/index.ts +3 -0
  7. package/registry/nocobase-acl/components/role-options.ts +29 -0
  8. package/registry/nocobase-acl/components/role-switcher.tsx +130 -0
  9. package/registry/nocobase-acl/demo/boundary-api.tsx +115 -0
  10. package/registry/nocobase-acl/demo/components.tsx +210 -0
  11. package/registry/nocobase-acl/demo/index.tsx +474 -0
  12. package/registry/nocobase-acl/demo/lazy-route.tsx +15 -0
  13. package/registry/nocobase-acl/demo/prompt-generator.tsx +115 -0
  14. package/registry/nocobase-acl/demo/role-switcher-prompt-generator.tsx +179 -0
  15. package/registry/nocobase-acl/demo/scenario-section.tsx +37 -0
  16. package/registry/nocobase-acl/extension.tsx +71 -0
  17. package/registry/nocobase-acl/index.ts +1 -0
  18. package/registry/nocobase-acl/tests/acl-regression.mjs +313 -0
  19. package/registry/nocobase-ai/README.md +70 -0
  20. package/registry/nocobase-ai/adapters/react-hook-form.ts +20 -0
  21. package/registry/nocobase-ai/assets/nocobase-ai-chat.svg +50 -0
  22. package/registry/nocobase-ai/components/ai-root-provider.tsx +32 -0
  23. package/registry/nocobase-ai/components/chat/ai-employee-avatar.tsx +31 -0
  24. package/registry/nocobase-ai/components/chat/chat-attachment.tsx +92 -0
  25. package/registry/nocobase-ai/components/chat/chat-compact.tsx +90 -0
  26. package/registry/nocobase-ai/components/chat/chat-composer.tsx +429 -0
  27. package/registry/nocobase-ai/components/chat/chat-empty-state.tsx +51 -0
  28. package/registry/nocobase-ai/components/chat/chat-header.tsx +88 -0
  29. package/registry/nocobase-ai/components/chat/chat-history-dialog.tsx +77 -0
  30. package/registry/nocobase-ai/components/chat/chat-message.tsx +252 -0
  31. package/registry/nocobase-ai/components/chat/chat-messages.tsx +174 -0
  32. package/registry/nocobase-ai/components/chat/chat-window.tsx +118 -0
  33. package/registry/nocobase-ai/components/chat/conversation-list.tsx +345 -0
  34. package/registry/nocobase-ai/components/chat/markdown-message.tsx +165 -0
  35. package/registry/nocobase-ai/components/chat/model-select-options.tsx +30 -0
  36. package/registry/nocobase-ai/components/chat/reasoning-panel.tsx +29 -0
  37. package/registry/nocobase-ai/components/chat/sub-agent-conversation.tsx +142 -0
  38. package/registry/nocobase-ai/components/chat/tool-call-card.tsx +316 -0
  39. package/registry/nocobase-ai/components/chat/user-prompt-editor.tsx +99 -0
  40. package/registry/nocobase-ai/components/chat/work-context-chip.tsx +114 -0
  41. package/registry/nocobase-ai/components/index.ts +59 -0
  42. package/registry/nocobase-ai/components/page-elements/ai-form.tsx +44 -0
  43. package/registry/nocobase-ai/components/page-elements/page-element-provider.tsx +469 -0
  44. package/registry/nocobase-ai/components/surfaces/chat-dialog.tsx +31 -0
  45. package/registry/nocobase-ai/components/surfaces/chat-inline.tsx +18 -0
  46. package/registry/nocobase-ai/components/surfaces/chat-page.tsx +18 -0
  47. package/registry/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +54 -0
  48. package/registry/nocobase-ai/components/surfaces/chat-side-panel.tsx +74 -0
  49. package/registry/nocobase-ai/components/surfaces/chat-surface-actions.tsx +54 -0
  50. package/registry/nocobase-ai/components/tools/builtin-tool-renderers.tsx +31 -0
  51. package/registry/nocobase-ai/components/tools/business-report-dialog.tsx +367 -0
  52. package/registry/nocobase-ai/components/tools/business-report-renderer.tsx +122 -0
  53. package/registry/nocobase-ai/components/tools/business-report-utils.tsx +282 -0
  54. package/registry/nocobase-ai/components/tools/chart-renderer.tsx +110 -0
  55. package/registry/nocobase-ai/components/tools/echarts-preview.tsx +62 -0
  56. package/registry/nocobase-ai/components/tools/echarts-runtime-advanced.ts +18 -0
  57. package/registry/nocobase-ai/components/tools/echarts-runtime-common.ts +20 -0
  58. package/registry/nocobase-ai/components/tools/echarts-runtime-components.ts +34 -0
  59. package/registry/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +24 -0
  60. package/registry/nocobase-ai/components/tools/echarts-runtime.ts +138 -0
  61. package/registry/nocobase-ai/components/tools/sub-agent-renderer.tsx +67 -0
  62. package/registry/nocobase-ai/components/tools/suggestions-renderer.tsx +59 -0
  63. package/registry/nocobase-ai/components/tools/tool-renderer-provider.tsx +55 -0
  64. package/registry/nocobase-ai/components/tools/tool-renderer-utils.ts +18 -0
  65. package/registry/nocobase-ai/components/tools/workflow-renderer.tsx +108 -0
  66. package/registry/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +73 -0
  67. package/registry/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +149 -0
  68. package/registry/nocobase-ai/demo/configuration-gate.tsx +52 -0
  69. package/registry/nocobase-ai/demo/container-showcase.tsx +210 -0
  70. package/registry/nocobase-ai/demo/floating.tsx +134 -0
  71. package/registry/nocobase-ai/demo/index.tsx +346 -0
  72. package/registry/nocobase-ai/demo/interaction-showcase.tsx +151 -0
  73. package/registry/nocobase-ai/demo/page-context-prompt-generator.tsx +355 -0
  74. package/registry/nocobase-ai/demo/page-context.tsx +884 -0
  75. package/registry/nocobase-ai/demo/page-element-showcase.tsx +197 -0
  76. package/registry/nocobase-ai/demo/prompt-card.tsx +41 -0
  77. package/registry/nocobase-ai/demo/prompt-generator.tsx +452 -0
  78. package/registry/nocobase-ai/demo/shortcut.tsx +1207 -0
  79. package/registry/nocobase-ai/demo/tool-cards.tsx +530 -0
  80. package/registry/nocobase-ai/extension.tsx +163 -0
  81. package/registry/nocobase-ai/global-ai-chat.tsx +213 -0
  82. package/registry/nocobase-ai/index.ts +3 -0
  83. package/registry/nocobase-ai/providers/ai-provider.tsx +323 -0
  84. package/registry/nocobase-ai/providers/avatars.ts +817 -0
  85. package/registry/nocobase-ai/providers/chat-context.tsx +109 -0
  86. package/registry/nocobase-ai/providers/chat-controller.ts +84 -0
  87. package/registry/nocobase-ai/providers/chat-message-utils.ts +198 -0
  88. package/registry/nocobase-ai/providers/chat-provider.tsx +871 -0
  89. package/registry/nocobase-ai/providers/chat-reducer.ts +147 -0
  90. package/registry/nocobase-ai/providers/chat-task-utils.ts +105 -0
  91. package/registry/nocobase-ai/providers/chat-transport.ts +184 -0
  92. package/registry/nocobase-ai/providers/form-registry.tsx +271 -0
  93. package/registry/nocobase-ai/providers/frontend-tool-registry.tsx +231 -0
  94. package/registry/nocobase-ai/providers/index.ts +61 -0
  95. package/registry/nocobase-ai/providers/model.ts +32 -0
  96. package/registry/nocobase-ai/providers/page-context.tsx +136 -0
  97. package/registry/nocobase-ai/providers/stream-coalescer.ts +60 -0
  98. package/registry/nocobase-ai/providers/stream-event-utils.ts +82 -0
  99. package/registry/nocobase-ai/providers/stream-parser.ts +61 -0
  100. package/registry/nocobase-ai/providers/sub-agent-stream.ts +316 -0
  101. package/registry/nocobase-ai/providers/types.ts +183 -0
  102. package/registry/nocobase-ai/providers/ui-message-stream.ts +463 -0
  103. package/registry/nocobase-ai/providers/use-automatic-tool-approval.ts +94 -0
  104. package/registry/nocobase-ai/providers/use-chat-attachments.ts +206 -0
  105. package/registry/nocobase-ai/providers/use-chat-message-actions.ts +554 -0
  106. package/registry/nocobase-ai/providers/use-chat-runtime.ts +210 -0
  107. package/registry/nocobase-ai/providers/use-chat-work-context.ts +93 -0
  108. package/registry/nocobase-ai/providers/use-conversation-catalog.ts +108 -0
  109. package/registry/nocobase-ai/providers/use-conversation-history.ts +178 -0
  110. package/registry/nocobase-ai/services/index.ts +7 -0
  111. package/registry/nocobase-ai/services/nocobase-ai-service.ts +454 -0
  112. package/registry/nocobase-ai/services/types.ts +82 -0
  113. package/registry/nocobase-ai/tests/business-report-regression.mjs +70 -0
  114. package/registry/nocobase-ai/tests/form-filler-regression.mjs +129 -0
  115. package/registry/nocobase-ai/tests/frontend-tool-regression.mjs +128 -0
  116. package/registry/nocobase-ai/tests/page-context-regression.mjs +48 -0
  117. package/registry/nocobase-ai/tests/stream-regression.mjs +207 -0
  118. package/registry.config.json +143 -0
  119. package/scripts/registry.mjs +161 -0
  120. package/src/App.css +95 -0
  121. package/src/App.tsx +127 -50
  122. package/src/app/extension.ts +9 -0
  123. package/src/app/extensions.tsx +28 -0
  124. package/src/components/access-control/access-denied.tsx +30 -0
  125. package/src/components/access-control/acl-bootstrap.tsx +9 -0
  126. package/src/components/access-control/acl-gate.tsx +42 -0
  127. package/src/components/access-control/navigate-to-accessible-resource.tsx +17 -0
  128. package/src/components/access-control/resource-access-guard.tsx +21 -0
  129. package/src/components/access-control/use-acl-runtime.ts +28 -0
  130. package/src/components/app-shell/header.tsx +4 -1
  131. package/src/components/app-shell/sidebar.tsx +34 -4
  132. package/src/lib/nocobase/acl/action.ts +147 -0
  133. package/src/lib/nocobase/acl/data-source.ts +24 -0
  134. package/src/lib/nocobase/acl/index.ts +6 -0
  135. package/src/lib/nocobase/acl/menu.ts +41 -0
  136. package/src/lib/nocobase/acl/record-permissions.ts +111 -0
  137. package/src/lib/nocobase/acl/store.ts +182 -0
  138. package/src/lib/nocobase/acl/types.ts +96 -0
  139. package/src/lib/nocobase/client.ts +262 -0
  140. package/src/lib/nocobase/error.ts +32 -0
  141. package/src/providers/access-control.ts +34 -0
  142. package/src/providers/auth.ts +74 -60
  143. package/src/providers/constants.ts +2 -37
  144. package/src/providers/data.ts +242 -85
  145. package/vite.config.ts +17 -0
@@ -0,0 +1,345 @@
1
+ import { Button } from "@/components/ui/button";
2
+ import {
3
+ Dialog,
4
+ DialogContent,
5
+ DialogDescription,
6
+ DialogFooter,
7
+ DialogHeader,
8
+ DialogTitle,
9
+ } from "@/components/ui/dialog";
10
+ import {
11
+ DropdownMenu,
12
+ DropdownMenuContent,
13
+ DropdownMenuItem,
14
+ DropdownMenuTrigger,
15
+ } from "@/components/ui/dropdown-menu";
16
+ import { cn } from "@/lib/utils";
17
+ import { Input } from "@/components/ui/input";
18
+ import { Label } from "@/components/ui/label";
19
+ import { useAIChatBase } from "../../providers";
20
+ import {
21
+ MoreHorizontal,
22
+ PanelLeftClose,
23
+ Pencil,
24
+ Plus,
25
+ Search,
26
+ Trash2,
27
+ X,
28
+ LoaderCircle,
29
+ } from "lucide-react";
30
+ import { useEffect, useState, type FormEvent } from "react";
31
+
32
+ export function ConversationList({
33
+ onClose,
34
+ showCloseButton = true,
35
+ }: {
36
+ onClose?: () => void;
37
+ showCloseButton?: boolean;
38
+ } = {}) {
39
+ const {
40
+ conversations,
41
+ activeConversationId,
42
+ selectConversation,
43
+ renameConversation,
44
+ removeConversation,
45
+ startNewConversation,
46
+ setConversationListOpen,
47
+ conversationsLoading,
48
+ conversationSearch,
49
+ searchConversations,
50
+ historyError,
51
+ } = useAIChatBase();
52
+ const [searchValue, setSearchValue] = useState(conversationSearch);
53
+ const [renameTarget, setRenameTarget] = useState<{
54
+ id: string;
55
+ title: string;
56
+ }>();
57
+ const [renameTitle, setRenameTitle] = useState("");
58
+ const [renaming, setRenaming] = useState(false);
59
+ const [renameError, setRenameError] = useState<string>();
60
+ const [deleteTarget, setDeleteTarget] = useState<{
61
+ id: string;
62
+ title: string;
63
+ }>();
64
+ const [deleting, setDeleting] = useState(false);
65
+ const [deleteError, setDeleteError] = useState<string>();
66
+
67
+ useEffect(() => {
68
+ setRenameTitle(renameTarget?.title ?? "");
69
+ setRenameError(undefined);
70
+ }, [renameTarget]);
71
+
72
+ useEffect(() => {
73
+ setSearchValue(conversationSearch);
74
+ }, [conversationSearch]);
75
+
76
+ const submitRename = async (event: FormEvent) => {
77
+ event.preventDefault();
78
+ if (!renameTarget || !renameTitle.trim()) return;
79
+ setRenaming(true);
80
+ setRenameError(undefined);
81
+ try {
82
+ await renameConversation(renameTarget.id, renameTitle);
83
+ setRenameTarget(undefined);
84
+ } catch (error) {
85
+ setRenameError(
86
+ error instanceof Error ? error.message : "Unable to rename conversation"
87
+ );
88
+ } finally {
89
+ setRenaming(false);
90
+ }
91
+ };
92
+
93
+ const confirmDelete = async () => {
94
+ if (!deleteTarget) return;
95
+ setDeleting(true);
96
+ setDeleteError(undefined);
97
+ try {
98
+ await removeConversation(deleteTarget.id);
99
+ setDeleteTarget(undefined);
100
+ } catch (error) {
101
+ setDeleteError(
102
+ error instanceof Error ? error.message : "Unable to delete conversation"
103
+ );
104
+ } finally {
105
+ setDeleting(false);
106
+ }
107
+ };
108
+
109
+ return (
110
+ <div className="flex h-full min-w-0 flex-1 flex-col overflow-hidden overscroll-contain bg-card">
111
+ <div className="flex h-12 shrink-0 items-center justify-between border-b px-3">
112
+ <div className="flex min-w-0 items-center gap-1.5">
113
+ {showCloseButton ? (
114
+ <Button
115
+ variant="ghost"
116
+ size="icon-sm"
117
+ aria-label="Close conversation list"
118
+ onClick={() =>
119
+ onClose ? onClose() : setConversationListOpen(false)
120
+ }
121
+ >
122
+ <PanelLeftClose />
123
+ </Button>
124
+ ) : null}
125
+ <span className="truncate text-sm font-semibold">Conversations</span>
126
+ </div>
127
+ <Button
128
+ variant="ghost"
129
+ size="icon-sm"
130
+ aria-label="New conversation"
131
+ onClick={startNewConversation}
132
+ >
133
+ <Plus />
134
+ </Button>
135
+ </div>
136
+ <form
137
+ className="shrink-0 border-b p-2.5"
138
+ onSubmit={(event) => {
139
+ event.preventDefault();
140
+ void searchConversations(searchValue).catch(() => undefined);
141
+ }}
142
+ >
143
+ <div className="relative">
144
+ <Search className="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
145
+ <Input
146
+ value={searchValue}
147
+ className="h-8 pl-8 pr-8 text-sm"
148
+ placeholder="Search conversations"
149
+ aria-label="Search conversations"
150
+ onChange={(event) => setSearchValue(event.target.value)}
151
+ />
152
+ {conversationsLoading && conversationSearch ? (
153
+ <LoaderCircle className="absolute right-2.5 top-1/2 size-4 -translate-y-1/2 animate-spin text-muted-foreground" />
154
+ ) : searchValue ? (
155
+ <Button
156
+ type="button"
157
+ variant="ghost"
158
+ size="icon-xs"
159
+ className="absolute right-1.5 top-1/2 -translate-y-1/2"
160
+ aria-label="Clear conversation search"
161
+ onClick={() => {
162
+ setSearchValue("");
163
+ void searchConversations("").catch(() => undefined);
164
+ }}
165
+ >
166
+ <X />
167
+ </Button>
168
+ ) : null}
169
+ </div>
170
+ </form>
171
+ <div className="min-h-0 flex-1 overflow-y-auto overscroll-contain p-2">
172
+ {conversationsLoading ? (
173
+ <div className="flex items-center justify-center gap-2 px-3 py-8 text-sm text-muted-foreground">
174
+ <LoaderCircle className="size-4 animate-spin" /> Loading
175
+ conversations…
176
+ </div>
177
+ ) : conversations.length ? (
178
+ <div className="space-y-1">
179
+ {conversations.map((conversation) => {
180
+ const active = conversation.id === activeConversationId;
181
+ return (
182
+ <div
183
+ key={conversation.id}
184
+ className={cn(
185
+ "group/conversation flex items-start rounded-lg pr-1 transition-colors",
186
+ active ? "bg-accent" : "hover:bg-muted/70"
187
+ )}
188
+ >
189
+ <button
190
+ type="button"
191
+ className="flex min-w-0 flex-1 items-center gap-2 px-2.5 py-2.5 text-left"
192
+ onClick={() => selectConversation(conversation.id)}
193
+ >
194
+ {conversation.unread && !active ? (
195
+ <span
196
+ className="size-2 shrink-0 rounded-full bg-destructive"
197
+ aria-label="Unread conversation"
198
+ />
199
+ ) : null}
200
+ <span className="block min-w-0 flex-1 truncate text-sm font-medium">
201
+ {conversation.title}
202
+ </span>
203
+ </button>
204
+ <DropdownMenu>
205
+ <DropdownMenuTrigger
206
+ render={
207
+ <Button
208
+ variant="ghost"
209
+ size="icon-xs"
210
+ className="mt-1.5 opacity-0 group-hover/conversation:opacity-100 data-popup-open:opacity-100"
211
+ aria-label="Conversation actions"
212
+ />
213
+ }
214
+ >
215
+ <MoreHorizontal />
216
+ </DropdownMenuTrigger>
217
+ <DropdownMenuContent align="end" className="w-36">
218
+ <DropdownMenuItem
219
+ onClick={() =>
220
+ setRenameTarget({
221
+ id: conversation.id,
222
+ title: conversation.title,
223
+ })
224
+ }
225
+ >
226
+ <Pencil />
227
+ Rename
228
+ </DropdownMenuItem>
229
+ <DropdownMenuItem
230
+ variant="destructive"
231
+ onClick={() => {
232
+ setDeleteError(undefined);
233
+ setDeleteTarget({
234
+ id: conversation.id,
235
+ title: conversation.title,
236
+ });
237
+ }}
238
+ >
239
+ <Trash2 />
240
+ Delete
241
+ </DropdownMenuItem>
242
+ </DropdownMenuContent>
243
+ </DropdownMenu>
244
+ </div>
245
+ );
246
+ })}
247
+ </div>
248
+ ) : (
249
+ <div className="px-3 py-8 text-center text-sm text-muted-foreground">
250
+ {conversationSearch
251
+ ? "No matching conversations."
252
+ : "No conversations yet."}
253
+ </div>
254
+ )}
255
+ {historyError ? (
256
+ <div className="mx-2 mt-2 rounded-lg border border-destructive/25 bg-destructive/5 px-3 py-2 text-xs text-destructive">
257
+ {historyError.message}
258
+ </div>
259
+ ) : null}
260
+ </div>
261
+ <Dialog
262
+ open={!!renameTarget}
263
+ onOpenChange={(open) => {
264
+ if (!open && !renaming) setRenameTarget(undefined);
265
+ }}
266
+ >
267
+ <DialogContent>
268
+ <form onSubmit={submitRename}>
269
+ <DialogHeader>
270
+ <DialogTitle>Rename conversation</DialogTitle>
271
+ <DialogDescription>
272
+ Choose a title that makes this conversation easy to find.
273
+ </DialogDescription>
274
+ </DialogHeader>
275
+ <div className="mt-5 space-y-2">
276
+ <Label htmlFor="conversation-title">Title</Label>
277
+ <Input
278
+ id="conversation-title"
279
+ value={renameTitle}
280
+ onChange={(event) => setRenameTitle(event.target.value)}
281
+ autoFocus
282
+ maxLength={120}
283
+ />
284
+ {renameError ? (
285
+ <p className="text-xs text-destructive">{renameError}</p>
286
+ ) : null}
287
+ </div>
288
+ <DialogFooter className="mt-5">
289
+ <Button
290
+ type="button"
291
+ variant="outline"
292
+ disabled={renaming}
293
+ onClick={() => setRenameTarget(undefined)}
294
+ >
295
+ Cancel
296
+ </Button>
297
+ <Button type="submit" disabled={renaming || !renameTitle.trim()}>
298
+ {renaming ? <LoaderCircle className="animate-spin" /> : null}
299
+ Save
300
+ </Button>
301
+ </DialogFooter>
302
+ </form>
303
+ </DialogContent>
304
+ </Dialog>
305
+ <Dialog
306
+ open={!!deleteTarget}
307
+ onOpenChange={(open) => {
308
+ if (!open && !deleting) setDeleteTarget(undefined);
309
+ }}
310
+ >
311
+ <DialogContent>
312
+ <DialogHeader>
313
+ <DialogTitle>Delete conversation?</DialogTitle>
314
+ <DialogDescription>
315
+ “{deleteTarget?.title}” and its messages will be permanently
316
+ deleted.
317
+ </DialogDescription>
318
+ </DialogHeader>
319
+ {deleteError ? (
320
+ <p className="mt-3 text-sm text-destructive">{deleteError}</p>
321
+ ) : null}
322
+ <DialogFooter className="mt-5">
323
+ <Button
324
+ type="button"
325
+ variant="outline"
326
+ disabled={deleting}
327
+ onClick={() => setDeleteTarget(undefined)}
328
+ >
329
+ Cancel
330
+ </Button>
331
+ <Button
332
+ type="button"
333
+ variant="destructive"
334
+ disabled={deleting}
335
+ onClick={() => void confirmDelete()}
336
+ >
337
+ {deleting ? <LoaderCircle className="animate-spin" /> : null}
338
+ Delete
339
+ </Button>
340
+ </DialogFooter>
341
+ </DialogContent>
342
+ </Dialog>
343
+ </div>
344
+ );
345
+ }
@@ -0,0 +1,165 @@
1
+ import ReactMarkdown from "react-markdown";
2
+ import type { Components } from "react-markdown";
3
+ import remarkGfm from "remark-gfm";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ type MarkdownMessageProps = {
8
+ children: string;
9
+ variant?: "chat" | "document";
10
+ };
11
+
12
+ function createMarkdownComponents(variant: MarkdownMessageProps["variant"]): Components {
13
+ const document = variant === "document";
14
+
15
+ return {
16
+ h1: ({ className, ...props }) => (
17
+ <h1
18
+ {...props}
19
+ className={cn(
20
+ "mb-4 mt-7 scroll-m-20 font-bold tracking-tight first:mt-0",
21
+ document ? "text-3xl" : "text-xl",
22
+ className
23
+ )}
24
+ />
25
+ ),
26
+ h2: ({ className, ...props }) => (
27
+ <h2
28
+ {...props}
29
+ className={cn(
30
+ "mb-3 mt-7 scroll-m-20 border-b pb-2 font-semibold tracking-tight first:mt-0",
31
+ document ? "text-2xl" : "text-lg",
32
+ className
33
+ )}
34
+ />
35
+ ),
36
+ h3: ({ className, ...props }) => (
37
+ <h3
38
+ {...props}
39
+ className={cn(
40
+ "mb-2 mt-6 scroll-m-20 font-semibold tracking-tight first:mt-0",
41
+ document ? "text-xl" : "text-base",
42
+ className
43
+ )}
44
+ />
45
+ ),
46
+ h4: ({ className, ...props }) => (
47
+ <h4
48
+ {...props}
49
+ className={cn(
50
+ "mb-2 mt-5 scroll-m-20 font-semibold first:mt-0",
51
+ document ? "text-lg" : "text-sm",
52
+ className
53
+ )}
54
+ />
55
+ ),
56
+ p: ({ className, ...props }) => (
57
+ <p {...props} className={cn("my-3 leading-7", className)} />
58
+ ),
59
+ ul: ({ className, ...props }) => (
60
+ <ul
61
+ {...props}
62
+ className={cn("my-3 ml-6 list-disc space-y-1", className)}
63
+ />
64
+ ),
65
+ ol: ({ className, ...props }) => (
66
+ <ol
67
+ {...props}
68
+ className={cn("my-3 ml-6 list-decimal space-y-1", className)}
69
+ />
70
+ ),
71
+ li: ({ className, ...props }) => (
72
+ <li {...props} className={cn("pl-1", className)} />
73
+ ),
74
+ blockquote: ({ className, ...props }) => (
75
+ <blockquote
76
+ {...props}
77
+ className={cn(
78
+ "my-4 border-l-4 border-border pl-4 italic text-muted-foreground",
79
+ className
80
+ )}
81
+ />
82
+ ),
83
+ hr: ({ className, ...props }) => (
84
+ <hr {...props} className={cn("my-6 border-border", className)} />
85
+ ),
86
+ table: ({ className, ...props }) => (
87
+ <div className="my-4 w-full overflow-x-auto rounded-md border">
88
+ <table
89
+ {...props}
90
+ className={cn("w-full border-collapse text-sm", className)}
91
+ />
92
+ </div>
93
+ ),
94
+ th: ({ className, ...props }) => (
95
+ <th
96
+ {...props}
97
+ className={cn(
98
+ "border-b border-r bg-muted/60 px-3 py-2 text-left font-semibold last:border-r-0",
99
+ className
100
+ )}
101
+ />
102
+ ),
103
+ td: ({ className, ...props }) => (
104
+ <td
105
+ {...props}
106
+ className={cn(
107
+ "border-b border-r px-3 py-2 align-top last:border-r-0",
108
+ className
109
+ )}
110
+ />
111
+ ),
112
+ tr: ({ className, ...props }) => (
113
+ <tr
114
+ {...props}
115
+ className={cn("last:[&>td]:border-b-0", className)}
116
+ />
117
+ ),
118
+ strong: ({ className, ...props }) => (
119
+ <strong {...props} className={cn("font-semibold", className)} />
120
+ ),
121
+ a: ({ className, ...props }) => (
122
+ <a
123
+ {...props}
124
+ className={cn("underline underline-offset-4", className)}
125
+ target="_blank"
126
+ rel="noreferrer"
127
+ />
128
+ ),
129
+ pre: ({ className, ...props }) => (
130
+ <pre
131
+ {...props}
132
+ className={cn(
133
+ "my-4 overflow-x-auto rounded-lg bg-muted p-4 text-sm",
134
+ className
135
+ )}
136
+ />
137
+ ),
138
+ code: ({ className, ...props }) => (
139
+ <code
140
+ {...props}
141
+ className={cn(
142
+ "rounded bg-muted px-1.5 py-0.5 font-mono text-[0.875em]",
143
+ className
144
+ )}
145
+ />
146
+ ),
147
+ };
148
+ }
149
+
150
+ const chatComponents = createMarkdownComponents("chat");
151
+ const documentComponents = createMarkdownComponents("document");
152
+
153
+ export function MarkdownMessage({
154
+ children,
155
+ variant = "chat",
156
+ }: MarkdownMessageProps) {
157
+ return (
158
+ <ReactMarkdown
159
+ remarkPlugins={[remarkGfm]}
160
+ components={variant === "document" ? documentComponents : chatComponents}
161
+ >
162
+ {children}
163
+ </ReactMarkdown>
164
+ );
165
+ }
@@ -0,0 +1,30 @@
1
+ import {
2
+ SelectGroup,
3
+ SelectItem,
4
+ SelectLabel,
5
+ SelectSeparator,
6
+ } from "@/components/ui/select";
7
+ import { getAIModelKey, groupAIModels, type AIModel } from "../../providers";
8
+ import { Fragment } from "react";
9
+
10
+ export function AIModelSelectOptions({ models }: { models: AIModel[] }) {
11
+ const groups = groupAIModels(models);
12
+
13
+ return groups.map((group, index) => (
14
+ <Fragment key={group.key}>
15
+ {index > 0 ? <SelectSeparator /> : null}
16
+ <SelectGroup className="py-1">
17
+ <SelectLabel className="px-2 py-1.5 font-medium">
18
+ {group.label}
19
+ </SelectLabel>
20
+ {group.models.map((model) => (
21
+ <SelectItem key={getAIModelKey(model)} value={getAIModelKey(model)}>
22
+ <span className="block max-w-64 truncate" title={model.label}>
23
+ {model.label}
24
+ </span>
25
+ </SelectItem>
26
+ ))}
27
+ </SelectGroup>
28
+ </Fragment>
29
+ ));
30
+ }
@@ -0,0 +1,29 @@
1
+ import {
2
+ Collapsible,
3
+ CollapsibleContent,
4
+ CollapsibleTrigger,
5
+ } from "@/components/ui/collapsible";
6
+ import { Brain, ChevronDown } from "lucide-react";
7
+
8
+ export function ReasoningPanel({
9
+ children,
10
+ streaming,
11
+ }: {
12
+ children: string;
13
+ streaming?: boolean;
14
+ }) {
15
+ if (!children) return null;
16
+
17
+ return (
18
+ <Collapsible className="overflow-hidden rounded-lg border border-border/70 bg-muted/30">
19
+ <CollapsibleTrigger className="group/reasoning flex w-full items-center gap-2 px-3 py-2 text-xs font-medium text-muted-foreground outline-none hover:bg-muted/40 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset">
20
+ <Brain className="size-3.5" />
21
+ <span>{streaming ? "Thinking…" : "Reasoning"}</span>
22
+ <ChevronDown className="ml-auto size-3.5 transition-transform group-data-panel-open/reasoning:rotate-180" />
23
+ </CollapsibleTrigger>
24
+ <CollapsibleContent className="border-t px-3 py-2.5 text-xs leading-5 text-muted-foreground">
25
+ {children}
26
+ </CollapsibleContent>
27
+ </Collapsible>
28
+ );
29
+ }