work-agent 0.1.0

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 (245) hide show
  1. package/README.md +234 -0
  2. package/app/(admin)/approvals/page.tsx +16 -0
  3. package/app/(admin)/audit/page.tsx +18 -0
  4. package/app/(admin)/layout.tsx +47 -0
  5. package/app/(admin)/scheduled-tasks/page.tsx +17 -0
  6. package/app/(admin)/settings/page.tsx +46 -0
  7. package/app/(admin)/skills/[name]/page.tsx +378 -0
  8. package/app/(admin)/skills/page.tsx +406 -0
  9. package/app/(admin)/statistics/page.tsx +416 -0
  10. package/app/(admin)/tickets/[id]/page.tsx +348 -0
  11. package/app/(admin)/tickets/new/page.tsx +309 -0
  12. package/app/(admin)/tickets/page.tsx +27 -0
  13. package/app/api/audit/route.ts +30 -0
  14. package/app/api/auth/feishu/callback/route.ts +72 -0
  15. package/app/api/auth/feishu/login/route.ts +17 -0
  16. package/app/api/auth/feishu/sso/route.ts +78 -0
  17. package/app/api/auth/login/route.ts +85 -0
  18. package/app/api/auth/oauth/route.ts +168 -0
  19. package/app/api/config/providers/route.ts +105 -0
  20. package/app/api/config/route.ts +115 -0
  21. package/app/api/config/status/route.ts +56 -0
  22. package/app/api/config/test/route.ts +212 -0
  23. package/app/api/documents/[id]/route.ts +88 -0
  24. package/app/api/documents/route.ts +53 -0
  25. package/app/api/health/route.ts +32 -0
  26. package/app/api/knowledge/[id]/route.ts +152 -0
  27. package/app/api/knowledge/from-session/route.ts +27 -0
  28. package/app/api/knowledge/route.ts +100 -0
  29. package/app/api/market/knowledge/[id]/route.ts +92 -0
  30. package/app/api/market/knowledge/route.ts +130 -0
  31. package/app/api/marketplace/skills/[id]/approve/route.ts +68 -0
  32. package/app/api/marketplace/skills/[id]/certify/route.ts +54 -0
  33. package/app/api/marketplace/skills/[id]/install/route.ts +180 -0
  34. package/app/api/marketplace/skills/[id]/promote-to-system/route.ts +219 -0
  35. package/app/api/marketplace/skills/[id]/rate/route.ts +90 -0
  36. package/app/api/marketplace/skills/[id]/ratings/route.ts +55 -0
  37. package/app/api/marketplace/skills/[id]/reject/route.ts +68 -0
  38. package/app/api/marketplace/skills/[id]/route.ts +177 -0
  39. package/app/api/marketplace/skills/route.ts +235 -0
  40. package/app/api/memory/route.ts +40 -0
  41. package/app/api/my/files/[id]/route.ts +52 -0
  42. package/app/api/my/files/route.ts +230 -0
  43. package/app/api/my/knowledge/route.ts +36 -0
  44. package/app/api/pi-chat/route.ts +443 -0
  45. package/app/api/recommend/route.ts +38 -0
  46. package/app/api/scheduled-tasks/[id]/execute/route.ts +132 -0
  47. package/app/api/scheduled-tasks/[id]/route.ts +165 -0
  48. package/app/api/scheduled-tasks/[id]/toggle/route.ts +53 -0
  49. package/app/api/scheduled-tasks/route.ts +101 -0
  50. package/app/api/sessions/[id]/messages/route.ts +212 -0
  51. package/app/api/sessions/route.ts +101 -0
  52. package/app/api/share/file/[id]/route.ts +37 -0
  53. package/app/api/skills/[name]/execute/route.ts +121 -0
  54. package/app/api/skills/[name]/route.ts +167 -0
  55. package/app/api/skills/create/route.ts +65 -0
  56. package/app/api/skills/generate/route.ts +405 -0
  57. package/app/api/skills/installed/route.ts +151 -0
  58. package/app/api/skills/route.ts +174 -0
  59. package/app/api/skills/translate/route.ts +40 -0
  60. package/app/api/skills/user/[name]/route.ts +159 -0
  61. package/app/api/skills/user/route.ts +90 -0
  62. package/app/api/statistics/route.ts +94 -0
  63. package/app/api/task-executions/[id]/route.ts +34 -0
  64. package/app/api/task-executions/route.ts +29 -0
  65. package/app/api/tickets/[id]/approve/route.ts +129 -0
  66. package/app/api/tickets/[id]/execute/route.ts +201 -0
  67. package/app/api/tickets/[id]/route.ts +127 -0
  68. package/app/api/tickets/route.ts +103 -0
  69. package/app/api/user/skills/route.ts +175 -0
  70. package/app/api/users/route.ts +80 -0
  71. package/app/chat/page.tsx +5 -0
  72. package/app/globals.css +84 -0
  73. package/app/h5/layout.tsx +5 -0
  74. package/app/h5/mobile-approvals-page.tsx +167 -0
  75. package/app/h5/mobile-chat-page.tsx +951 -0
  76. package/app/h5/mobile-profile-page.tsx +147 -0
  77. package/app/h5/mobile-tickets-page.tsx +121 -0
  78. package/app/h5/page.tsx +23 -0
  79. package/app/h5/ticket-action-buttons.tsx +80 -0
  80. package/app/layout.tsx +26 -0
  81. package/app/login/page.tsx +318 -0
  82. package/app/market/knowledge/[id]/page.tsx +77 -0
  83. package/app/market/knowledge/page.tsx +358 -0
  84. package/app/market/layout.tsx +29 -0
  85. package/app/market/page.tsx +18 -0
  86. package/app/market/skills/page.tsx +397 -0
  87. package/app/my/files/page.tsx +511 -0
  88. package/app/my/knowledge/[id]/page.tsx +271 -0
  89. package/app/my/knowledge/new/page.tsx +234 -0
  90. package/app/my/knowledge/page.tsx +248 -0
  91. package/app/my/layout.tsx +32 -0
  92. package/app/my/memory/page.tsx +164 -0
  93. package/app/my/page.tsx +18 -0
  94. package/app/my/scheduled-tasks/[id]/edit/page.tsx +290 -0
  95. package/app/my/scheduled-tasks/[id]/executions/page.tsx +275 -0
  96. package/app/my/scheduled-tasks/[id]/page.tsx +284 -0
  97. package/app/my/scheduled-tasks/new/page.tsx +230 -0
  98. package/app/my/scheduled-tasks/page.tsx +27 -0
  99. package/app/my/skills/[name]/page.tsx +320 -0
  100. package/app/my/skills/new/page.tsx +394 -0
  101. package/app/my/skills/page.tsx +303 -0
  102. package/app/page.tsx +2288 -0
  103. package/app/share/[sessionId]/page.tsx +226 -0
  104. package/app/share/file/[id]/page.tsx +140 -0
  105. package/bin/README.md +63 -0
  106. package/bin/generate-api-system +300 -0
  107. package/bin/postinstall.js +95 -0
  108. package/bin/work-agent.js +173 -0
  109. package/components/ai-elements/agent.tsx +142 -0
  110. package/components/ai-elements/artifact.tsx +149 -0
  111. package/components/ai-elements/attachments.tsx +427 -0
  112. package/components/ai-elements/audio-player.tsx +232 -0
  113. package/components/ai-elements/canvas.tsx +26 -0
  114. package/components/ai-elements/chain-of-thought.tsx +223 -0
  115. package/components/ai-elements/checkpoint.tsx +72 -0
  116. package/components/ai-elements/code-block.tsx +555 -0
  117. package/components/ai-elements/commit.tsx +449 -0
  118. package/components/ai-elements/confirmation.tsx +173 -0
  119. package/components/ai-elements/connection.tsx +28 -0
  120. package/components/ai-elements/context.tsx +410 -0
  121. package/components/ai-elements/controls.tsx +19 -0
  122. package/components/ai-elements/conversation.tsx +167 -0
  123. package/components/ai-elements/edge.tsx +144 -0
  124. package/components/ai-elements/environment-variables.tsx +325 -0
  125. package/components/ai-elements/file-tree.tsx +298 -0
  126. package/components/ai-elements/image.tsx +25 -0
  127. package/components/ai-elements/inline-citation.tsx +294 -0
  128. package/components/ai-elements/jsx-preview.tsx +250 -0
  129. package/components/ai-elements/message.tsx +367 -0
  130. package/components/ai-elements/mic-selector.tsx +372 -0
  131. package/components/ai-elements/model-selector.tsx +214 -0
  132. package/components/ai-elements/node.tsx +72 -0
  133. package/components/ai-elements/open-in-chat.tsx +367 -0
  134. package/components/ai-elements/package-info.tsx +235 -0
  135. package/components/ai-elements/panel.tsx +16 -0
  136. package/components/ai-elements/persona.tsx +280 -0
  137. package/components/ai-elements/plan.tsx +144 -0
  138. package/components/ai-elements/prompt-input.tsx +1341 -0
  139. package/components/ai-elements/queue.tsx +275 -0
  140. package/components/ai-elements/reasoning.tsx +355 -0
  141. package/components/ai-elements/sandbox.tsx +133 -0
  142. package/components/ai-elements/schema-display.tsx +473 -0
  143. package/components/ai-elements/shimmer.tsx +78 -0
  144. package/components/ai-elements/snippet.tsx +141 -0
  145. package/components/ai-elements/sources.tsx +78 -0
  146. package/components/ai-elements/speech-input.tsx +324 -0
  147. package/components/ai-elements/stack-trace.tsx +531 -0
  148. package/components/ai-elements/suggestion.tsx +58 -0
  149. package/components/ai-elements/task.tsx +88 -0
  150. package/components/ai-elements/terminal.tsx +277 -0
  151. package/components/ai-elements/test-results.tsx +497 -0
  152. package/components/ai-elements/tool.tsx +174 -0
  153. package/components/ai-elements/toolbar.tsx +17 -0
  154. package/components/ai-elements/transcription.tsx +126 -0
  155. package/components/ai-elements/voice-selector.tsx +525 -0
  156. package/components/ai-elements/web-preview.tsx +282 -0
  157. package/components/audit-log-list.tsx +114 -0
  158. package/components/chat/EmptyPreviewState.tsx +12 -0
  159. package/components/chat/KnowledgePickerDialog.tsx +464 -0
  160. package/components/chat/KnowledgePreview.tsx +70 -0
  161. package/components/chat/KnowledgePreviewPanel.tsx +86 -0
  162. package/components/chat/MentionInput.tsx +309 -0
  163. package/components/chat/OrganizeDialog.tsx +258 -0
  164. package/components/chat/RecommendationBanner.tsx +94 -0
  165. package/components/chat/SaveToKnowledgeDialog.tsx +193 -0
  166. package/components/chat/SkillSelector.tsx +305 -0
  167. package/components/chat/SkillSwitcher.tsx +163 -0
  168. package/components/client-layout.tsx +15 -0
  169. package/components/knowledge/KnowledgeMetadataPanel.tsx +293 -0
  170. package/components/layout-wrapper.tsx +18 -0
  171. package/components/mobile-layout.tsx +62 -0
  172. package/components/scheduled-task-list.tsx +356 -0
  173. package/components/setup-guide.tsx +484 -0
  174. package/components/sub-nav.tsx +54 -0
  175. package/components/ticket-detail-content.tsx +383 -0
  176. package/components/ticket-list.tsx +366 -0
  177. package/components/top-nav.tsx +132 -0
  178. package/components/ui/accordion.tsx +58 -0
  179. package/components/ui/alert.tsx +59 -0
  180. package/components/ui/avatar.tsx +50 -0
  181. package/components/ui/badge.tsx +36 -0
  182. package/components/ui/button-group.tsx +83 -0
  183. package/components/ui/button.tsx +57 -0
  184. package/components/ui/card.tsx +91 -0
  185. package/components/ui/carousel.tsx +262 -0
  186. package/components/ui/collapsible.tsx +11 -0
  187. package/components/ui/command.tsx +153 -0
  188. package/components/ui/dialog.tsx +122 -0
  189. package/components/ui/dropdown-menu.tsx +200 -0
  190. package/components/ui/hover-card.tsx +29 -0
  191. package/components/ui/input-group.tsx +170 -0
  192. package/components/ui/input.tsx +22 -0
  193. package/components/ui/label.tsx +26 -0
  194. package/components/ui/popover.tsx +31 -0
  195. package/components/ui/progress.tsx +28 -0
  196. package/components/ui/scroll-area.tsx +48 -0
  197. package/components/ui/select.tsx +174 -0
  198. package/components/ui/separator.tsx +31 -0
  199. package/components/ui/spinner.tsx +16 -0
  200. package/components/ui/switch.tsx +29 -0
  201. package/components/ui/table.tsx +120 -0
  202. package/components/ui/tabs.tsx +55 -0
  203. package/components/ui/textarea.tsx +22 -0
  204. package/components/ui/tooltip.tsx +30 -0
  205. package/components/welcome-guide.tsx +182 -0
  206. package/components.json +24 -0
  207. package/lib/command-parser.ts +331 -0
  208. package/lib/dangerous-commands.ts +672 -0
  209. package/lib/db.ts +2250 -0
  210. package/lib/feishu-auth.ts +135 -0
  211. package/lib/file-storage.ts +306 -0
  212. package/lib/file-tool.ts +583 -0
  213. package/lib/knowledge-tool.ts +152 -0
  214. package/lib/knowledge-types.ts +66 -0
  215. package/lib/market-client.ts +313 -0
  216. package/lib/market-db.ts +736 -0
  217. package/lib/market-types.ts +51 -0
  218. package/lib/memory-tool.ts +211 -0
  219. package/lib/memory.ts +197 -0
  220. package/lib/pi-config.ts +436 -0
  221. package/lib/pi-session.ts +799 -0
  222. package/lib/pinyin.ts +13 -0
  223. package/lib/recommendation.ts +227 -0
  224. package/lib/risk-estimator.ts +350 -0
  225. package/lib/scheduled-task-tool.ts +184 -0
  226. package/lib/scheduler-init.ts +43 -0
  227. package/lib/scheduler.ts +416 -0
  228. package/lib/secure-bash-tool.ts +413 -0
  229. package/lib/skill-engine.ts +396 -0
  230. package/lib/skill-generator.ts +269 -0
  231. package/lib/skill-loader.ts +234 -0
  232. package/lib/skill-tool.ts +188 -0
  233. package/lib/skill-types.ts +82 -0
  234. package/lib/skills-init.ts +58 -0
  235. package/lib/ticket-tool.ts +246 -0
  236. package/lib/user-skill-types.ts +30 -0
  237. package/lib/user-skills.ts +362 -0
  238. package/lib/utils.ts +6 -0
  239. package/lib/workflow.ts +154 -0
  240. package/lib/zip-tool.ts +191 -0
  241. package/next.config.js +8 -0
  242. package/package.json +106 -0
  243. package/public/.gitkeep +1 -0
  244. package/public/icon.svg +1 -0
  245. package/tsconfig.json +42 -0
@@ -0,0 +1,275 @@
1
+ "use client";
2
+
3
+ import type { ComponentProps } from "react";
4
+
5
+ import { Button } from "@/components/ui/button";
6
+ import {
7
+ Collapsible,
8
+ CollapsibleContent,
9
+ CollapsibleTrigger,
10
+ } from "@/components/ui/collapsible";
11
+ import { ScrollArea } from "@/components/ui/scroll-area";
12
+ import { cn } from "@/lib/utils";
13
+ import { ChevronDownIcon, PaperclipIcon } from "lucide-react";
14
+
15
+ export interface QueueMessagePart {
16
+ type: string;
17
+ text?: string;
18
+ url?: string;
19
+ filename?: string;
20
+ mediaType?: string;
21
+ }
22
+
23
+ export interface QueueMessage {
24
+ id: string;
25
+ parts: QueueMessagePart[];
26
+ }
27
+
28
+ export interface QueueTodo {
29
+ id: string;
30
+ title: string;
31
+ description?: string;
32
+ status?: "pending" | "completed";
33
+ }
34
+
35
+ export type QueueItemProps = ComponentProps<"li">;
36
+
37
+ export const QueueItem = ({ className, ...props }: QueueItemProps) => (
38
+ <li
39
+ className={cn(
40
+ "group flex flex-col gap-1 rounded-md px-3 py-1 text-sm transition-colors hover:bg-muted",
41
+ className
42
+ )}
43
+ {...props}
44
+ />
45
+ );
46
+
47
+ export type QueueItemIndicatorProps = ComponentProps<"span"> & {
48
+ completed?: boolean;
49
+ };
50
+
51
+ export const QueueItemIndicator = ({
52
+ completed = false,
53
+ className,
54
+ ...props
55
+ }: QueueItemIndicatorProps) => (
56
+ <span
57
+ className={cn(
58
+ "mt-0.5 inline-block size-2.5 rounded-full border",
59
+ completed
60
+ ? "border-muted-foreground/20 bg-muted-foreground/10"
61
+ : "border-muted-foreground/50",
62
+ className
63
+ )}
64
+ {...props}
65
+ />
66
+ );
67
+
68
+ export type QueueItemContentProps = ComponentProps<"span"> & {
69
+ completed?: boolean;
70
+ };
71
+
72
+ export const QueueItemContent = ({
73
+ completed = false,
74
+ className,
75
+ ...props
76
+ }: QueueItemContentProps) => (
77
+ <span
78
+ className={cn(
79
+ "line-clamp-1 grow break-words",
80
+ completed
81
+ ? "text-muted-foreground/50 line-through"
82
+ : "text-muted-foreground",
83
+ className
84
+ )}
85
+ {...props}
86
+ />
87
+ );
88
+
89
+ export type QueueItemDescriptionProps = ComponentProps<"div"> & {
90
+ completed?: boolean;
91
+ };
92
+
93
+ export const QueueItemDescription = ({
94
+ completed = false,
95
+ className,
96
+ ...props
97
+ }: QueueItemDescriptionProps) => (
98
+ <div
99
+ className={cn(
100
+ "ml-6 text-xs",
101
+ completed
102
+ ? "text-muted-foreground/40 line-through"
103
+ : "text-muted-foreground",
104
+ className
105
+ )}
106
+ {...props}
107
+ />
108
+ );
109
+
110
+ export type QueueItemActionsProps = ComponentProps<"div">;
111
+
112
+ export const QueueItemActions = ({
113
+ className,
114
+ ...props
115
+ }: QueueItemActionsProps) => (
116
+ <div className={cn("flex gap-1", className)} {...props} />
117
+ );
118
+
119
+ export type QueueItemActionProps = Omit<
120
+ ComponentProps<typeof Button>,
121
+ "variant" | "size"
122
+ >;
123
+
124
+ export const QueueItemAction = ({
125
+ className,
126
+ ...props
127
+ }: QueueItemActionProps) => (
128
+ <Button
129
+ className={cn(
130
+ "size-auto rounded p-1 text-muted-foreground opacity-0 transition-opacity hover:bg-muted-foreground/10 hover:text-foreground group-hover:opacity-100",
131
+ className
132
+ )}
133
+ size="icon"
134
+ type="button"
135
+ variant="ghost"
136
+ {...props}
137
+ />
138
+ );
139
+
140
+ export type QueueItemAttachmentProps = ComponentProps<"div">;
141
+
142
+ export const QueueItemAttachment = ({
143
+ className,
144
+ ...props
145
+ }: QueueItemAttachmentProps) => (
146
+ <div className={cn("mt-1 flex flex-wrap gap-2", className)} {...props} />
147
+ );
148
+
149
+ export type QueueItemImageProps = ComponentProps<"img">;
150
+
151
+ export const QueueItemImage = ({
152
+ className,
153
+ ...props
154
+ }: QueueItemImageProps) => (
155
+ <img
156
+ alt=""
157
+ className={cn("h-8 w-8 rounded border object-cover", className)}
158
+ height={32}
159
+ width={32}
160
+ {...props}
161
+ />
162
+ );
163
+
164
+ export type QueueItemFileProps = ComponentProps<"span">;
165
+
166
+ export const QueueItemFile = ({
167
+ children,
168
+ className,
169
+ ...props
170
+ }: QueueItemFileProps) => (
171
+ <span
172
+ className={cn(
173
+ "flex items-center gap-1 rounded border bg-muted px-2 py-1 text-xs",
174
+ className
175
+ )}
176
+ {...props}
177
+ >
178
+ <PaperclipIcon size={12} />
179
+ <span className="max-w-[100px] truncate">{children}</span>
180
+ </span>
181
+ );
182
+
183
+ export type QueueListProps = ComponentProps<typeof ScrollArea>;
184
+
185
+ export const QueueList = ({
186
+ children,
187
+ className,
188
+ ...props
189
+ }: QueueListProps) => (
190
+ <ScrollArea className={cn("mt-2 -mb-1", className)} {...props}>
191
+ <div className="max-h-40 pr-4">
192
+ <ul>{children}</ul>
193
+ </div>
194
+ </ScrollArea>
195
+ );
196
+
197
+ // QueueSection - collapsible section container
198
+ export type QueueSectionProps = ComponentProps<typeof Collapsible>;
199
+
200
+ export const QueueSection = ({
201
+ className,
202
+ defaultOpen = true,
203
+ ...props
204
+ }: QueueSectionProps) => (
205
+ <Collapsible className={cn(className)} defaultOpen={defaultOpen} {...props} />
206
+ );
207
+
208
+ // QueueSectionTrigger - section header/trigger
209
+ export type QueueSectionTriggerProps = ComponentProps<"button">;
210
+
211
+ export const QueueSectionTrigger = ({
212
+ children,
213
+ className,
214
+ ...props
215
+ }: QueueSectionTriggerProps) => (
216
+ <CollapsibleTrigger asChild>
217
+ <button
218
+ className={cn(
219
+ "group flex w-full items-center justify-between rounded-md bg-muted/40 px-3 py-2 text-left font-medium text-muted-foreground text-sm transition-colors hover:bg-muted",
220
+ className
221
+ )}
222
+ type="button"
223
+ {...props}
224
+ >
225
+ {children}
226
+ </button>
227
+ </CollapsibleTrigger>
228
+ );
229
+
230
+ // QueueSectionLabel - label content with icon and count
231
+ export type QueueSectionLabelProps = ComponentProps<"span"> & {
232
+ count?: number;
233
+ label: string;
234
+ icon?: React.ReactNode;
235
+ };
236
+
237
+ export const QueueSectionLabel = ({
238
+ count,
239
+ label,
240
+ icon,
241
+ className,
242
+ ...props
243
+ }: QueueSectionLabelProps) => (
244
+ <span className={cn("flex items-center gap-2", className)} {...props}>
245
+ <ChevronDownIcon className="size-4 transition-transform group-data-[state=closed]:-rotate-90" />
246
+ {icon}
247
+ <span>
248
+ {count} {label}
249
+ </span>
250
+ </span>
251
+ );
252
+
253
+ // QueueSectionContent - collapsible content area
254
+ export type QueueSectionContentProps = ComponentProps<
255
+ typeof CollapsibleContent
256
+ >;
257
+
258
+ export const QueueSectionContent = ({
259
+ className,
260
+ ...props
261
+ }: QueueSectionContentProps) => (
262
+ <CollapsibleContent className={cn(className)} {...props} />
263
+ );
264
+
265
+ export type QueueProps = ComponentProps<"div">;
266
+
267
+ export const Queue = ({ className, ...props }: QueueProps) => (
268
+ <div
269
+ className={cn(
270
+ "flex flex-col gap-2 rounded-xl border border-border bg-background px-3 pt-2 pb-2 shadow-xs",
271
+ className
272
+ )}
273
+ {...props}
274
+ />
275
+ );
@@ -0,0 +1,355 @@
1
+ "use client";
2
+
3
+ import type { ComponentProps, ReactNode } from "react";
4
+
5
+ import { useControllableState } from "@radix-ui/react-use-controllable-state";
6
+ import {
7
+ Collapsible,
8
+ CollapsibleContent,
9
+ CollapsibleTrigger,
10
+ } from "@/components/ui/collapsible";
11
+ import { Button } from "@/components/ui/button";
12
+ import {
13
+ Tooltip,
14
+ TooltipContent,
15
+ TooltipProvider,
16
+ TooltipTrigger,
17
+ } from "@/components/ui/tooltip";
18
+ import { cn } from "@/lib/utils";
19
+ import { cjk } from "@streamdown/cjk";
20
+ import { code } from "@streamdown/code";
21
+ import { math } from "@streamdown/math";
22
+ import { mermaid } from "@streamdown/mermaid";
23
+ import { BrainIcon, ChevronDownIcon, CopyIcon, DownloadIcon } from "lucide-react";
24
+ import {
25
+ createContext,
26
+ memo,
27
+ useCallback,
28
+ useContext,
29
+ useEffect,
30
+ useMemo,
31
+ useRef,
32
+ useState,
33
+ } from "react";
34
+ import { Streamdown } from "streamdown";
35
+
36
+ import { Shimmer } from "./shimmer";
37
+
38
+ interface ReasoningContextValue {
39
+ isStreaming: boolean;
40
+ isOpen: boolean;
41
+ setIsOpen: (open: boolean) => void;
42
+ duration: number | undefined;
43
+ }
44
+
45
+ const ReasoningContext = createContext<ReasoningContextValue | null>(null);
46
+
47
+ export const useReasoning = () => {
48
+ const context = useContext(ReasoningContext);
49
+ if (!context) {
50
+ throw new Error("Reasoning components must be used within Reasoning");
51
+ }
52
+ return context;
53
+ };
54
+
55
+ export type ReasoningProps = ComponentProps<typeof Collapsible> & {
56
+ isStreaming?: boolean;
57
+ open?: boolean;
58
+ defaultOpen?: boolean;
59
+ onOpenChange?: (open: boolean) => void;
60
+ duration?: number;
61
+ };
62
+
63
+ const AUTO_CLOSE_DELAY = 1000;
64
+ const MS_IN_S = 1000;
65
+
66
+ export const Reasoning = memo(
67
+ ({
68
+ className,
69
+ isStreaming = false,
70
+ open,
71
+ defaultOpen,
72
+ onOpenChange,
73
+ duration: durationProp,
74
+ children,
75
+ ...props
76
+ }: ReasoningProps) => {
77
+ const resolvedDefaultOpen = defaultOpen ?? isStreaming;
78
+ // Track if defaultOpen was explicitly set to false (to prevent auto-open)
79
+ const isExplicitlyClosed = defaultOpen === false;
80
+
81
+ const [isOpen, setIsOpen] = useControllableState<boolean>({
82
+ defaultProp: resolvedDefaultOpen,
83
+ onChange: onOpenChange,
84
+ prop: open,
85
+ });
86
+ const [duration, setDuration] = useControllableState<number | undefined>({
87
+ defaultProp: undefined,
88
+ prop: durationProp,
89
+ });
90
+
91
+ const hasEverStreamedRef = useRef(isStreaming);
92
+ const [hasAutoClosed, setHasAutoClosed] = useState(false);
93
+ const startTimeRef = useRef<number | null>(null);
94
+
95
+ // Track when streaming starts and compute duration
96
+ useEffect(() => {
97
+ if (isStreaming) {
98
+ hasEverStreamedRef.current = true;
99
+ if (startTimeRef.current === null) {
100
+ startTimeRef.current = Date.now();
101
+ }
102
+ } else if (startTimeRef.current !== null) {
103
+ setDuration(Math.ceil((Date.now() - startTimeRef.current) / MS_IN_S));
104
+ startTimeRef.current = null;
105
+ }
106
+ }, [isStreaming, setDuration]);
107
+
108
+ // Auto-open when streaming starts (unless explicitly closed)
109
+ useEffect(() => {
110
+ if (isStreaming && !isOpen && !isExplicitlyClosed) {
111
+ setIsOpen(true);
112
+ }
113
+ }, [isStreaming, isOpen, setIsOpen, isExplicitlyClosed]);
114
+
115
+ // Auto-close when streaming ends (once only, and only if it ever streamed)
116
+ useEffect(() => {
117
+ if (
118
+ hasEverStreamedRef.current &&
119
+ !isStreaming &&
120
+ isOpen &&
121
+ !hasAutoClosed
122
+ ) {
123
+ const timer = setTimeout(() => {
124
+ setIsOpen(false);
125
+ setHasAutoClosed(true);
126
+ }, AUTO_CLOSE_DELAY);
127
+
128
+ return () => clearTimeout(timer);
129
+ }
130
+ }, [isStreaming, isOpen, setIsOpen, hasAutoClosed]);
131
+
132
+ const handleOpenChange = useCallback(
133
+ (newOpen: boolean) => {
134
+ setIsOpen(newOpen);
135
+ },
136
+ [setIsOpen]
137
+ );
138
+
139
+ const contextValue = useMemo(
140
+ () => ({ duration, isOpen, isStreaming, setIsOpen }),
141
+ [duration, isOpen, isStreaming, setIsOpen]
142
+ );
143
+
144
+ return (
145
+ <ReasoningContext.Provider value={contextValue}>
146
+ <Collapsible
147
+ className={cn("not-prose mb-4", className)}
148
+ onOpenChange={handleOpenChange}
149
+ open={isOpen}
150
+ {...props}
151
+ >
152
+ {children}
153
+ </Collapsible>
154
+ </ReasoningContext.Provider>
155
+ );
156
+ }
157
+ );
158
+
159
+ export type ReasoningTriggerProps = ComponentProps<
160
+ typeof CollapsibleTrigger
161
+ > & {
162
+ getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode;
163
+ };
164
+
165
+ const defaultGetThinkingMessage = (isStreaming: boolean, duration?: number) => {
166
+ if (isStreaming || duration === 0) {
167
+ return <Shimmer duration={1}>Thinking...</Shimmer>;
168
+ }
169
+ if (duration === undefined) {
170
+ return <p>Thought for a few seconds</p>;
171
+ }
172
+ return <p>Thought for {duration} seconds</p>;
173
+ };
174
+
175
+ export const ReasoningTrigger = memo(
176
+ ({
177
+ className,
178
+ children,
179
+ getThinkingMessage = defaultGetThinkingMessage,
180
+ ...props
181
+ }: ReasoningTriggerProps) => {
182
+ const { isStreaming, isOpen, duration } = useReasoning();
183
+
184
+ return (
185
+ <CollapsibleTrigger
186
+ className={cn(
187
+ "flex w-full items-center gap-2 text-muted-foreground text-sm transition-colors hover:text-foreground",
188
+ className
189
+ )}
190
+ {...props}
191
+ >
192
+ {children ?? (
193
+ <>
194
+ <BrainIcon className="size-4" />
195
+ {getThinkingMessage(isStreaming, duration)}
196
+ <ChevronDownIcon
197
+ className={cn(
198
+ "size-4 transition-transform",
199
+ isOpen ? "rotate-180" : "rotate-0"
200
+ )}
201
+ />
202
+ </>
203
+ )}
204
+ </CollapsibleTrigger>
205
+ );
206
+ }
207
+ );
208
+
209
+ export type ReasoningContentProps = ComponentProps<
210
+ typeof CollapsibleContent
211
+ > & {
212
+ children: string;
213
+ };
214
+
215
+ // Custom code component with working copy and download buttons
216
+ // This works around the bug in streamdown v2.3.0 where code block copy buttons
217
+ // don't receive the code content as a prop
218
+ const CustomCodeBlock = memo(
219
+ ({
220
+ children,
221
+ className,
222
+ node,
223
+ ...props
224
+ }: ComponentProps<"code"> & {
225
+ node?: { properties?: { className?: string[] } };
226
+ "data-block"?: string;
227
+ }) => {
228
+ const [copied, setCopied] = useState(false);
229
+
230
+ // Check if this is a code block (not inline code)
231
+ const isCodeBlock = props["data-block"] !== undefined;
232
+
233
+ if (!isCodeBlock) {
234
+ // Inline code - render as-is
235
+ return (
236
+ <code
237
+ className={cn(
238
+ "rounded bg-muted px-1.5 py-0.5 font-mono text-sm",
239
+ className
240
+ )}
241
+ {...props}
242
+ >
243
+ {children}
244
+ </code>
245
+ );
246
+ }
247
+
248
+ // Extract language from className (e.g., "language-bash" -> "bash")
249
+ const langClass = className?.match(/language-(\w+)/)?.[1] || "";
250
+ const codeContent = typeof children === "string" ? children : String(children ?? "");
251
+
252
+ const handleCopy = useCallback(async () => {
253
+ try {
254
+ await navigator.clipboard.writeText(codeContent);
255
+ setCopied(true);
256
+ setTimeout(() => setCopied(false), 2000);
257
+ } catch (err) {
258
+ console.error("Failed to copy:", err);
259
+ }
260
+ }, [codeContent]);
261
+
262
+ const handleDownload = useCallback(() => {
263
+ const extension = langClass || "txt";
264
+ const blob = new Blob([codeContent], { type: "text/plain" });
265
+ const url = URL.createObjectURL(blob);
266
+ const a = document.createElement("a");
267
+ a.href = url;
268
+ a.download = `code.${extension}`;
269
+ document.body.appendChild(a);
270
+ a.click();
271
+ document.body.removeChild(a);
272
+ URL.revokeObjectURL(url);
273
+ }, [codeContent, langClass]);
274
+
275
+ return (
276
+ <div className="group relative my-4 w-fit max-w-full rounded-lg border border-border bg-muted/50">
277
+ {/* Header with language label and action buttons */}
278
+ <div className="flex items-center justify-between border-b border-border px-4 py-2">
279
+ <span className="text-xs font-mono lowercase text-muted-foreground">
280
+ {langClass || "code"}
281
+ </span>
282
+ <div className="flex items-center gap-1">
283
+ <TooltipProvider>
284
+ <Tooltip>
285
+ <TooltipTrigger asChild>
286
+ <Button
287
+ size="icon-sm"
288
+ type="button"
289
+ variant="ghost"
290
+ onClick={handleCopy}
291
+ className="h-7 w-7"
292
+ >
293
+ <CopyIcon size={14} />
294
+ <span className="sr-only">Copy code</span>
295
+ </Button>
296
+ </TooltipTrigger>
297
+ <TooltipContent>
298
+ <p>{copied ? "Copied!" : "Copy code"}</p>
299
+ </TooltipContent>
300
+ </Tooltip>
301
+ </TooltipProvider>
302
+ <TooltipProvider>
303
+ <Tooltip>
304
+ <TooltipTrigger asChild>
305
+ <Button
306
+ size="icon-sm"
307
+ type="button"
308
+ variant="ghost"
309
+ onClick={handleDownload}
310
+ className="h-7 w-7"
311
+ >
312
+ <DownloadIcon size={14} />
313
+ <span className="sr-only">Download code</span>
314
+ </Button>
315
+ </TooltipTrigger>
316
+ <TooltipContent>
317
+ <p>Download code</p>
318
+ </TooltipContent>
319
+ </Tooltip>
320
+ </TooltipProvider>
321
+ </div>
322
+ </div>
323
+ {/* Code content */}
324
+ <code className={cn("block overflow-x-auto p-4 text-sm bg-background rounded-b-lg", className)} {...props}>
325
+ {children}
326
+ </code>
327
+ </div>
328
+ );
329
+ }
330
+ );
331
+
332
+ CustomCodeBlock.displayName = "CustomCodeBlock";
333
+
334
+ const streamdownPlugins = { cjk, code, math, mermaid };
335
+
336
+ export const ReasoningContent = memo(
337
+ ({ className, children, ...props }: ReasoningContentProps) => (
338
+ <CollapsibleContent
339
+ className={cn(
340
+ "mt-4 text-sm",
341
+ "data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-muted-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
342
+ className
343
+ )}
344
+ {...props}
345
+ >
346
+ <Streamdown plugins={streamdownPlugins} components={{ code: CustomCodeBlock }} {...props}>
347
+ {children}
348
+ </Streamdown>
349
+ </CollapsibleContent>
350
+ )
351
+ );
352
+
353
+ Reasoning.displayName = "Reasoning";
354
+ ReasoningTrigger.displayName = "ReasoningTrigger";
355
+ ReasoningContent.displayName = "ReasoningContent";