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,142 @@
1
+ "use client";
2
+
3
+ import type { Tool } from "ai";
4
+ import type { ComponentProps } from "react";
5
+
6
+ import {
7
+ Accordion,
8
+ AccordionContent,
9
+ AccordionItem,
10
+ AccordionTrigger,
11
+ } from "@/components/ui/accordion";
12
+ import { Badge } from "@/components/ui/badge";
13
+ import { cn } from "@/lib/utils";
14
+ import { BotIcon } from "lucide-react";
15
+ import { memo } from "react";
16
+
17
+ import { CodeBlock } from "./code-block";
18
+
19
+ export type AgentProps = ComponentProps<"div">;
20
+
21
+ export const Agent = memo(({ className, ...props }: AgentProps) => (
22
+ <div
23
+ className={cn("not-prose w-full rounded-md border", className)}
24
+ {...props}
25
+ />
26
+ ));
27
+
28
+ export type AgentHeaderProps = ComponentProps<"div"> & {
29
+ name: string;
30
+ model?: string;
31
+ };
32
+
33
+ export const AgentHeader = memo(
34
+ ({ className, name, model, ...props }: AgentHeaderProps) => (
35
+ <div
36
+ className={cn(
37
+ "flex w-full items-center justify-between gap-4 p-3",
38
+ className
39
+ )}
40
+ {...props}
41
+ >
42
+ <div className="flex items-center gap-2">
43
+ <BotIcon className="size-4 text-muted-foreground" />
44
+ <span className="font-medium text-sm">{name}</span>
45
+ {model && (
46
+ <Badge className="font-mono text-xs" variant="secondary">
47
+ {model}
48
+ </Badge>
49
+ )}
50
+ </div>
51
+ </div>
52
+ )
53
+ );
54
+
55
+ export type AgentContentProps = ComponentProps<"div">;
56
+
57
+ export const AgentContent = memo(
58
+ ({ className, ...props }: AgentContentProps) => (
59
+ <div className={cn("space-y-4 p-4 pt-0", className)} {...props} />
60
+ )
61
+ );
62
+
63
+ export type AgentInstructionsProps = ComponentProps<"div"> & {
64
+ children: string;
65
+ };
66
+
67
+ export const AgentInstructions = memo(
68
+ ({ className, children, ...props }: AgentInstructionsProps) => (
69
+ <div className={cn("space-y-2", className)} {...props}>
70
+ <span className="font-medium text-muted-foreground text-sm">
71
+ Instructions
72
+ </span>
73
+ <div className="rounded-md bg-muted/50 p-3 text-muted-foreground text-sm">
74
+ <p>{children}</p>
75
+ </div>
76
+ </div>
77
+ )
78
+ );
79
+
80
+ export type AgentToolsProps = ComponentProps<typeof Accordion>;
81
+
82
+ export const AgentTools = memo(({ className, ...props }: AgentToolsProps) => (
83
+ <div className={cn("space-y-2", className)}>
84
+ <span className="font-medium text-muted-foreground text-sm">Tools</span>
85
+ <Accordion className="rounded-md border" {...props} />
86
+ </div>
87
+ ));
88
+
89
+ export type AgentToolProps = ComponentProps<typeof AccordionItem> & {
90
+ tool: Tool;
91
+ };
92
+
93
+ export const AgentTool = memo(
94
+ ({ className, tool, value, ...props }: AgentToolProps) => {
95
+ const schema =
96
+ "jsonSchema" in tool && tool.jsonSchema
97
+ ? tool.jsonSchema
98
+ : tool.inputSchema;
99
+
100
+ return (
101
+ <AccordionItem
102
+ className={cn("border-b last:border-b-0", className)}
103
+ value={value}
104
+ {...props}
105
+ >
106
+ <AccordionTrigger className="px-3 py-2 text-sm hover:no-underline">
107
+ {tool.description ?? "No description"}
108
+ </AccordionTrigger>
109
+ <AccordionContent className="px-3 pb-3">
110
+ <div className="rounded-md bg-muted/50">
111
+ <CodeBlock code={JSON.stringify(schema, null, 2)} language="json" />
112
+ </div>
113
+ </AccordionContent>
114
+ </AccordionItem>
115
+ );
116
+ }
117
+ );
118
+
119
+ export type AgentOutputProps = ComponentProps<"div"> & {
120
+ schema: string;
121
+ };
122
+
123
+ export const AgentOutput = memo(
124
+ ({ className, schema, ...props }: AgentOutputProps) => (
125
+ <div className={cn("space-y-2", className)} {...props}>
126
+ <span className="font-medium text-muted-foreground text-sm">
127
+ Output Schema
128
+ </span>
129
+ <div className="rounded-md bg-muted/50">
130
+ <CodeBlock code={schema} language="typescript" />
131
+ </div>
132
+ </div>
133
+ )
134
+ );
135
+
136
+ Agent.displayName = "Agent";
137
+ AgentHeader.displayName = "AgentHeader";
138
+ AgentContent.displayName = "AgentContent";
139
+ AgentInstructions.displayName = "AgentInstructions";
140
+ AgentTools.displayName = "AgentTools";
141
+ AgentTool.displayName = "AgentTool";
142
+ AgentOutput.displayName = "AgentOutput";
@@ -0,0 +1,149 @@
1
+ "use client";
2
+
3
+ import type { LucideIcon } from "lucide-react";
4
+ import type { ComponentProps, HTMLAttributes } from "react";
5
+
6
+ import { Button } from "@/components/ui/button";
7
+ import {
8
+ Tooltip,
9
+ TooltipContent,
10
+ TooltipProvider,
11
+ TooltipTrigger,
12
+ } from "@/components/ui/tooltip";
13
+ import { cn } from "@/lib/utils";
14
+ import { XIcon } from "lucide-react";
15
+
16
+ export type ArtifactProps = HTMLAttributes<HTMLDivElement>;
17
+
18
+ export const Artifact = ({ className, ...props }: ArtifactProps) => (
19
+ <div
20
+ className={cn(
21
+ "flex flex-col overflow-hidden rounded-lg border bg-background shadow-sm",
22
+ className
23
+ )}
24
+ {...props}
25
+ />
26
+ );
27
+
28
+ export type ArtifactHeaderProps = HTMLAttributes<HTMLDivElement>;
29
+
30
+ export const ArtifactHeader = ({
31
+ className,
32
+ ...props
33
+ }: ArtifactHeaderProps) => (
34
+ <div
35
+ className={cn(
36
+ "flex items-center justify-between border-b bg-muted/50 px-4 py-3",
37
+ className
38
+ )}
39
+ {...props}
40
+ />
41
+ );
42
+
43
+ export type ArtifactCloseProps = ComponentProps<typeof Button>;
44
+
45
+ export const ArtifactClose = ({
46
+ className,
47
+ children,
48
+ size = "sm",
49
+ variant = "ghost",
50
+ ...props
51
+ }: ArtifactCloseProps) => (
52
+ <Button
53
+ className={cn(
54
+ "size-8 p-0 text-muted-foreground hover:text-foreground",
55
+ className
56
+ )}
57
+ size={size}
58
+ type="button"
59
+ variant={variant}
60
+ {...props}
61
+ >
62
+ {children ?? <XIcon className="size-4" />}
63
+ <span className="sr-only">Close</span>
64
+ </Button>
65
+ );
66
+
67
+ export type ArtifactTitleProps = HTMLAttributes<HTMLParagraphElement>;
68
+
69
+ export const ArtifactTitle = ({ className, ...props }: ArtifactTitleProps) => (
70
+ <p
71
+ className={cn("font-medium text-foreground text-sm", className)}
72
+ {...props}
73
+ />
74
+ );
75
+
76
+ export type ArtifactDescriptionProps = HTMLAttributes<HTMLParagraphElement>;
77
+
78
+ export const ArtifactDescription = ({
79
+ className,
80
+ ...props
81
+ }: ArtifactDescriptionProps) => (
82
+ <p className={cn("text-muted-foreground text-sm", className)} {...props} />
83
+ );
84
+
85
+ export type ArtifactActionsProps = HTMLAttributes<HTMLDivElement>;
86
+
87
+ export const ArtifactActions = ({
88
+ className,
89
+ ...props
90
+ }: ArtifactActionsProps) => (
91
+ <div className={cn("flex items-center gap-1", className)} {...props} />
92
+ );
93
+
94
+ export type ArtifactActionProps = ComponentProps<typeof Button> & {
95
+ tooltip?: string;
96
+ label?: string;
97
+ icon?: LucideIcon;
98
+ };
99
+
100
+ export const ArtifactAction = ({
101
+ tooltip,
102
+ label,
103
+ icon: Icon,
104
+ children,
105
+ className,
106
+ size = "sm",
107
+ variant = "ghost",
108
+ ...props
109
+ }: ArtifactActionProps) => {
110
+ const button = (
111
+ <Button
112
+ className={cn(
113
+ "size-8 p-0 text-muted-foreground hover:text-foreground",
114
+ className
115
+ )}
116
+ size={size}
117
+ type="button"
118
+ variant={variant}
119
+ {...props}
120
+ >
121
+ {Icon ? <Icon className="size-4" /> : children}
122
+ <span className="sr-only">{label || tooltip}</span>
123
+ </Button>
124
+ );
125
+
126
+ if (tooltip) {
127
+ return (
128
+ <TooltipProvider>
129
+ <Tooltip>
130
+ <TooltipTrigger asChild>{button}</TooltipTrigger>
131
+ <TooltipContent>
132
+ <p>{tooltip}</p>
133
+ </TooltipContent>
134
+ </Tooltip>
135
+ </TooltipProvider>
136
+ );
137
+ }
138
+
139
+ return button;
140
+ };
141
+
142
+ export type ArtifactContentProps = HTMLAttributes<HTMLDivElement>;
143
+
144
+ export const ArtifactContent = ({
145
+ className,
146
+ ...props
147
+ }: ArtifactContentProps) => (
148
+ <div className={cn("flex-1 overflow-auto p-4", className)} {...props} />
149
+ );
@@ -0,0 +1,427 @@
1
+ "use client";
2
+
3
+ import type { FileUIPart, SourceDocumentUIPart } from "ai";
4
+ import type { ComponentProps, HTMLAttributes, ReactNode } from "react";
5
+
6
+ import { Button } from "@/components/ui/button";
7
+ import {
8
+ HoverCard,
9
+ HoverCardContent,
10
+ HoverCardTrigger,
11
+ } from "@/components/ui/hover-card";
12
+ import { cn } from "@/lib/utils";
13
+ import {
14
+ FileTextIcon,
15
+ GlobeIcon,
16
+ ImageIcon,
17
+ Music2Icon,
18
+ PaperclipIcon,
19
+ VideoIcon,
20
+ XIcon,
21
+ } from "lucide-react";
22
+ import { createContext, useCallback, useContext, useMemo } from "react";
23
+
24
+ // ============================================================================
25
+ // Types
26
+ // ============================================================================
27
+
28
+ export type AttachmentData =
29
+ | (FileUIPart & { id: string })
30
+ | (SourceDocumentUIPart & { id: string });
31
+
32
+ export type AttachmentMediaCategory =
33
+ | "image"
34
+ | "video"
35
+ | "audio"
36
+ | "document"
37
+ | "source"
38
+ | "unknown";
39
+
40
+ export type AttachmentVariant = "grid" | "inline" | "list";
41
+
42
+ const mediaCategoryIcons: Record<AttachmentMediaCategory, typeof ImageIcon> = {
43
+ audio: Music2Icon,
44
+ document: FileTextIcon,
45
+ image: ImageIcon,
46
+ source: GlobeIcon,
47
+ unknown: PaperclipIcon,
48
+ video: VideoIcon,
49
+ };
50
+
51
+ // ============================================================================
52
+ // Utility Functions
53
+ // ============================================================================
54
+
55
+ export const getMediaCategory = (
56
+ data: AttachmentData
57
+ ): AttachmentMediaCategory => {
58
+ if (data.type === "source-document") {
59
+ return "source";
60
+ }
61
+
62
+ const mediaType = data.mediaType ?? "";
63
+
64
+ if (mediaType.startsWith("image/")) {
65
+ return "image";
66
+ }
67
+ if (mediaType.startsWith("video/")) {
68
+ return "video";
69
+ }
70
+ if (mediaType.startsWith("audio/")) {
71
+ return "audio";
72
+ }
73
+ if (mediaType.startsWith("application/") || mediaType.startsWith("text/")) {
74
+ return "document";
75
+ }
76
+
77
+ return "unknown";
78
+ };
79
+
80
+ export const getAttachmentLabel = (data: AttachmentData): string => {
81
+ if (data.type === "source-document") {
82
+ return data.title || data.filename || "Source";
83
+ }
84
+
85
+ const category = getMediaCategory(data);
86
+ return data.filename || (category === "image" ? "Image" : "Attachment");
87
+ };
88
+
89
+ const renderAttachmentImage = (
90
+ url: string,
91
+ filename: string | undefined,
92
+ isGrid: boolean
93
+ ) =>
94
+ isGrid ? (
95
+ <img
96
+ alt={filename || "Image"}
97
+ className="size-full object-cover"
98
+ height={96}
99
+ src={url}
100
+ width={96}
101
+ />
102
+ ) : (
103
+ <img
104
+ alt={filename || "Image"}
105
+ className="size-full rounded object-cover"
106
+ height={20}
107
+ src={url}
108
+ width={20}
109
+ />
110
+ );
111
+
112
+ // ============================================================================
113
+ // Contexts
114
+ // ============================================================================
115
+
116
+ interface AttachmentsContextValue {
117
+ variant: AttachmentVariant;
118
+ }
119
+
120
+ const AttachmentsContext = createContext<AttachmentsContextValue | null>(null);
121
+
122
+ interface AttachmentContextValue {
123
+ data: AttachmentData;
124
+ mediaCategory: AttachmentMediaCategory;
125
+ onRemove?: () => void;
126
+ variant: AttachmentVariant;
127
+ }
128
+
129
+ const AttachmentContext = createContext<AttachmentContextValue | null>(null);
130
+
131
+ // ============================================================================
132
+ // Hooks
133
+ // ============================================================================
134
+
135
+ export const useAttachmentsContext = () =>
136
+ useContext(AttachmentsContext) ?? { variant: "grid" as const };
137
+
138
+ export const useAttachmentContext = () => {
139
+ const ctx = useContext(AttachmentContext);
140
+ if (!ctx) {
141
+ throw new Error("Attachment components must be used within <Attachment>");
142
+ }
143
+ return ctx;
144
+ };
145
+
146
+ // ============================================================================
147
+ // Attachments - Container
148
+ // ============================================================================
149
+
150
+ export type AttachmentsProps = HTMLAttributes<HTMLDivElement> & {
151
+ variant?: AttachmentVariant;
152
+ };
153
+
154
+ export const Attachments = ({
155
+ variant = "grid",
156
+ className,
157
+ children,
158
+ ...props
159
+ }: AttachmentsProps) => {
160
+ const contextValue = useMemo(() => ({ variant }), [variant]);
161
+
162
+ return (
163
+ <AttachmentsContext.Provider value={contextValue}>
164
+ <div
165
+ className={cn(
166
+ "flex items-start",
167
+ variant === "list" ? "flex-col gap-2" : "flex-wrap gap-2",
168
+ variant === "grid" && "ml-auto w-fit",
169
+ className
170
+ )}
171
+ {...props}
172
+ >
173
+ {children}
174
+ </div>
175
+ </AttachmentsContext.Provider>
176
+ );
177
+ };
178
+
179
+ // ============================================================================
180
+ // Attachment - Item
181
+ // ============================================================================
182
+
183
+ export type AttachmentProps = HTMLAttributes<HTMLDivElement> & {
184
+ data: AttachmentData;
185
+ onRemove?: () => void;
186
+ };
187
+
188
+ export const Attachment = ({
189
+ data,
190
+ onRemove,
191
+ className,
192
+ children,
193
+ ...props
194
+ }: AttachmentProps) => {
195
+ const { variant } = useAttachmentsContext();
196
+ const mediaCategory = getMediaCategory(data);
197
+
198
+ const contextValue = useMemo<AttachmentContextValue>(
199
+ () => ({ data, mediaCategory, onRemove, variant }),
200
+ [data, mediaCategory, onRemove, variant]
201
+ );
202
+
203
+ return (
204
+ <AttachmentContext.Provider value={contextValue}>
205
+ <div
206
+ className={cn(
207
+ "group relative",
208
+ variant === "grid" && "size-24 overflow-hidden rounded-lg",
209
+ variant === "inline" && [
210
+ "flex h-8 cursor-pointer select-none items-center gap-1.5",
211
+ "rounded-md border border-border px-1.5",
212
+ "font-medium text-sm transition-all",
213
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
214
+ ],
215
+ variant === "list" && [
216
+ "flex w-full items-center gap-3 rounded-lg border p-3",
217
+ "hover:bg-accent/50",
218
+ ],
219
+ className
220
+ )}
221
+ {...props}
222
+ >
223
+ {children}
224
+ </div>
225
+ </AttachmentContext.Provider>
226
+ );
227
+ };
228
+
229
+ // ============================================================================
230
+ // AttachmentPreview - Media preview
231
+ // ============================================================================
232
+
233
+ export type AttachmentPreviewProps = HTMLAttributes<HTMLDivElement> & {
234
+ fallbackIcon?: ReactNode;
235
+ };
236
+
237
+ export const AttachmentPreview = ({
238
+ fallbackIcon,
239
+ className,
240
+ ...props
241
+ }: AttachmentPreviewProps) => {
242
+ const { data, mediaCategory, variant } = useAttachmentContext();
243
+
244
+ const iconSize = variant === "inline" ? "size-3" : "size-4";
245
+
246
+ const renderIcon = (Icon: typeof ImageIcon) => (
247
+ <Icon className={cn(iconSize, "text-muted-foreground")} />
248
+ );
249
+
250
+ const renderContent = () => {
251
+ if (mediaCategory === "image" && data.type === "file" && data.url) {
252
+ return renderAttachmentImage(data.url, data.filename, variant === "grid");
253
+ }
254
+
255
+ if (mediaCategory === "video" && data.type === "file" && data.url) {
256
+ return <video className="size-full object-cover" muted src={data.url} />;
257
+ }
258
+
259
+ const Icon = mediaCategoryIcons[mediaCategory];
260
+ return fallbackIcon ?? renderIcon(Icon);
261
+ };
262
+
263
+ return (
264
+ <div
265
+ className={cn(
266
+ "flex shrink-0 items-center justify-center overflow-hidden",
267
+ variant === "grid" && "size-full bg-muted",
268
+ variant === "inline" && "size-5 rounded bg-background",
269
+ variant === "list" && "size-12 rounded bg-muted",
270
+ className
271
+ )}
272
+ {...props}
273
+ >
274
+ {renderContent()}
275
+ </div>
276
+ );
277
+ };
278
+
279
+ // ============================================================================
280
+ // AttachmentInfo - Name and type display
281
+ // ============================================================================
282
+
283
+ export type AttachmentInfoProps = HTMLAttributes<HTMLDivElement> & {
284
+ showMediaType?: boolean;
285
+ };
286
+
287
+ export const AttachmentInfo = ({
288
+ showMediaType = false,
289
+ className,
290
+ ...props
291
+ }: AttachmentInfoProps) => {
292
+ const { data, variant } = useAttachmentContext();
293
+ const label = getAttachmentLabel(data);
294
+
295
+ if (variant === "grid") {
296
+ return null;
297
+ }
298
+
299
+ return (
300
+ <div className={cn("min-w-0 flex-1", className)} {...props}>
301
+ <span className="block truncate">{label}</span>
302
+ {showMediaType && data.mediaType && (
303
+ <span className="block truncate text-muted-foreground text-xs">
304
+ {data.mediaType}
305
+ </span>
306
+ )}
307
+ </div>
308
+ );
309
+ };
310
+
311
+ // ============================================================================
312
+ // AttachmentRemove - Remove button
313
+ // ============================================================================
314
+
315
+ export type AttachmentRemoveProps = ComponentProps<typeof Button> & {
316
+ label?: string;
317
+ };
318
+
319
+ export const AttachmentRemove = ({
320
+ label = "Remove",
321
+ className,
322
+ children,
323
+ ...props
324
+ }: AttachmentRemoveProps) => {
325
+ const { onRemove, variant } = useAttachmentContext();
326
+
327
+ const handleClick = useCallback(
328
+ (e: React.MouseEvent) => {
329
+ e.stopPropagation();
330
+ onRemove?.();
331
+ },
332
+ [onRemove]
333
+ );
334
+
335
+ if (!onRemove) {
336
+ return null;
337
+ }
338
+
339
+ return (
340
+ <Button
341
+ aria-label={label}
342
+ className={cn(
343
+ variant === "grid" && [
344
+ "absolute top-2 right-2 size-6 rounded-full p-0",
345
+ "bg-background/80 backdrop-blur-sm",
346
+ "opacity-0 transition-opacity group-hover:opacity-100",
347
+ "hover:bg-background",
348
+ "[&>svg]:size-3",
349
+ ],
350
+ variant === "inline" && [
351
+ "size-5 rounded p-0",
352
+ "opacity-0 transition-opacity group-hover:opacity-100",
353
+ "[&>svg]:size-2.5",
354
+ ],
355
+ variant === "list" && ["size-8 shrink-0 rounded p-0", "[&>svg]:size-4"],
356
+ className
357
+ )}
358
+ onClick={handleClick}
359
+ type="button"
360
+ variant="ghost"
361
+ {...props}
362
+ >
363
+ {children ?? <XIcon />}
364
+ <span className="sr-only">{label}</span>
365
+ </Button>
366
+ );
367
+ };
368
+
369
+ // ============================================================================
370
+ // AttachmentHoverCard - Hover preview
371
+ // ============================================================================
372
+
373
+ export type AttachmentHoverCardProps = ComponentProps<typeof HoverCard>;
374
+
375
+ export const AttachmentHoverCard = ({
376
+ openDelay = 0,
377
+ closeDelay = 0,
378
+ ...props
379
+ }: AttachmentHoverCardProps) => (
380
+ <HoverCard closeDelay={closeDelay} openDelay={openDelay} {...props} />
381
+ );
382
+
383
+ export type AttachmentHoverCardTriggerProps = ComponentProps<
384
+ typeof HoverCardTrigger
385
+ >;
386
+
387
+ export const AttachmentHoverCardTrigger = (
388
+ props: AttachmentHoverCardTriggerProps
389
+ ) => <HoverCardTrigger {...props} />;
390
+
391
+ export type AttachmentHoverCardContentProps = ComponentProps<
392
+ typeof HoverCardContent
393
+ >;
394
+
395
+ export const AttachmentHoverCardContent = ({
396
+ align = "start",
397
+ className,
398
+ ...props
399
+ }: AttachmentHoverCardContentProps) => (
400
+ <HoverCardContent
401
+ align={align}
402
+ className={cn("w-auto p-2", className)}
403
+ {...props}
404
+ />
405
+ );
406
+
407
+ // ============================================================================
408
+ // AttachmentEmpty - Empty state
409
+ // ============================================================================
410
+
411
+ export type AttachmentEmptyProps = HTMLAttributes<HTMLDivElement>;
412
+
413
+ export const AttachmentEmpty = ({
414
+ className,
415
+ children,
416
+ ...props
417
+ }: AttachmentEmptyProps) => (
418
+ <div
419
+ className={cn(
420
+ "flex items-center justify-center p-4 text-muted-foreground text-sm",
421
+ className
422
+ )}
423
+ {...props}
424
+ >
425
+ {children ?? "No attachments"}
426
+ </div>
427
+ );