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,126 @@
1
+ "use client";
2
+
3
+ import type { Experimental_TranscriptionResult as TranscriptionResult } from "ai";
4
+ import type { ComponentProps, ReactNode } from "react";
5
+
6
+ import { useControllableState } from "@radix-ui/react-use-controllable-state";
7
+ import { cn } from "@/lib/utils";
8
+ import { createContext, useCallback, useContext, useMemo } from "react";
9
+
10
+ type TranscriptionSegment = TranscriptionResult["segments"][number];
11
+
12
+ interface TranscriptionContextValue {
13
+ segments: TranscriptionSegment[];
14
+ currentTime: number;
15
+ onTimeUpdate: (time: number) => void;
16
+ onSeek?: (time: number) => void;
17
+ }
18
+
19
+ const TranscriptionContext = createContext<TranscriptionContextValue | null>(
20
+ null
21
+ );
22
+
23
+ const useTranscription = () => {
24
+ const context = useContext(TranscriptionContext);
25
+ if (!context) {
26
+ throw new Error(
27
+ "Transcription components must be used within Transcription"
28
+ );
29
+ }
30
+ return context;
31
+ };
32
+
33
+ export type TranscriptionProps = Omit<ComponentProps<"div">, "children"> & {
34
+ segments: TranscriptionSegment[];
35
+ currentTime?: number;
36
+ onSeek?: (time: number) => void;
37
+ children: (segment: TranscriptionSegment, index: number) => ReactNode;
38
+ };
39
+
40
+ export const Transcription = ({
41
+ segments,
42
+ currentTime: externalCurrentTime,
43
+ onSeek,
44
+ className,
45
+ children,
46
+ ...props
47
+ }: TranscriptionProps) => {
48
+ const [currentTime, setCurrentTime] = useControllableState({
49
+ defaultProp: 0,
50
+ onChange: onSeek,
51
+ prop: externalCurrentTime,
52
+ });
53
+
54
+ const contextValue = useMemo(
55
+ () => ({ currentTime, onSeek, onTimeUpdate: setCurrentTime, segments }),
56
+ [currentTime, onSeek, setCurrentTime, segments]
57
+ );
58
+
59
+ return (
60
+ <TranscriptionContext.Provider value={contextValue}>
61
+ <div
62
+ className={cn(
63
+ "flex flex-wrap gap-1 text-sm leading-relaxed",
64
+ className
65
+ )}
66
+ data-slot="transcription"
67
+ {...props}
68
+ >
69
+ {segments
70
+ .filter((segment) => segment.text.trim())
71
+ .map((segment, index) => children(segment, index))}
72
+ </div>
73
+ </TranscriptionContext.Provider>
74
+ );
75
+ };
76
+
77
+ export type TranscriptionSegmentProps = ComponentProps<"button"> & {
78
+ segment: TranscriptionSegment;
79
+ index: number;
80
+ };
81
+
82
+ export const TranscriptionSegment = ({
83
+ segment,
84
+ index,
85
+ className,
86
+ onClick,
87
+ ...props
88
+ }: TranscriptionSegmentProps) => {
89
+ const { currentTime, onSeek } = useTranscription();
90
+
91
+ const isActive =
92
+ currentTime >= segment.startSecond && currentTime < segment.endSecond;
93
+ const isPast = currentTime >= segment.endSecond;
94
+
95
+ const handleClick = useCallback(
96
+ (event: React.MouseEvent<HTMLButtonElement>) => {
97
+ if (onSeek) {
98
+ onSeek(segment.startSecond);
99
+ }
100
+ onClick?.(event);
101
+ },
102
+ [onSeek, segment.startSecond, onClick]
103
+ );
104
+
105
+ return (
106
+ <button
107
+ className={cn(
108
+ "inline text-left",
109
+ isActive && "text-primary",
110
+ isPast && "text-muted-foreground",
111
+ !(isActive || isPast) && "text-muted-foreground/60",
112
+ onSeek && "cursor-pointer hover:text-foreground",
113
+ !onSeek && "cursor-default",
114
+ className
115
+ )}
116
+ data-active={isActive}
117
+ data-index={index}
118
+ data-slot="transcription-segment"
119
+ onClick={handleClick}
120
+ type="button"
121
+ {...props}
122
+ >
123
+ {segment.text}
124
+ </button>
125
+ );
126
+ };
@@ -0,0 +1,525 @@
1
+ "use client";
2
+
3
+ import type { ComponentProps, ReactNode } from "react";
4
+
5
+ import { useControllableState } from "@radix-ui/react-use-controllable-state";
6
+ import { Button } from "@/components/ui/button";
7
+ import {
8
+ Command,
9
+ CommandDialog,
10
+ CommandEmpty,
11
+ CommandGroup,
12
+ CommandInput,
13
+ CommandItem,
14
+ CommandList,
15
+ CommandSeparator,
16
+ CommandShortcut,
17
+ } from "@/components/ui/command";
18
+ import {
19
+ Dialog,
20
+ DialogContent,
21
+ DialogTitle,
22
+ DialogTrigger,
23
+ } from "@/components/ui/dialog";
24
+ import { Spinner } from "@/components/ui/spinner";
25
+ import { cn } from "@/lib/utils";
26
+ import {
27
+ CircleSmallIcon,
28
+ MarsIcon,
29
+ MarsStrokeIcon,
30
+ NonBinaryIcon,
31
+ PauseIcon,
32
+ PlayIcon,
33
+ TransgenderIcon,
34
+ VenusAndMarsIcon,
35
+ VenusIcon,
36
+ } from "lucide-react";
37
+ import { createContext, useCallback, useContext, useMemo } from "react";
38
+
39
+ interface VoiceSelectorContextValue {
40
+ value: string | undefined;
41
+ setValue: (value: string | undefined) => void;
42
+ open: boolean;
43
+ setOpen: (open: boolean) => void;
44
+ }
45
+
46
+ const VoiceSelectorContext = createContext<VoiceSelectorContextValue | null>(
47
+ null
48
+ );
49
+
50
+ export const useVoiceSelector = () => {
51
+ const context = useContext(VoiceSelectorContext);
52
+ if (!context) {
53
+ throw new Error(
54
+ "VoiceSelector components must be used within VoiceSelector"
55
+ );
56
+ }
57
+ return context;
58
+ };
59
+
60
+ export type VoiceSelectorProps = ComponentProps<typeof Dialog> & {
61
+ value?: string;
62
+ defaultValue?: string;
63
+ onValueChange?: (value: string | undefined) => void;
64
+ };
65
+
66
+ export const VoiceSelector = ({
67
+ value: valueProp,
68
+ defaultValue,
69
+ onValueChange,
70
+ open: openProp,
71
+ defaultOpen = false,
72
+ onOpenChange,
73
+ children,
74
+ ...props
75
+ }: VoiceSelectorProps) => {
76
+ const [value, setValue] = useControllableState({
77
+ defaultProp: defaultValue,
78
+ onChange: onValueChange,
79
+ prop: valueProp,
80
+ });
81
+
82
+ const [open, setOpen] = useControllableState({
83
+ defaultProp: defaultOpen,
84
+ onChange: onOpenChange,
85
+ prop: openProp,
86
+ });
87
+
88
+ const voiceSelectorContext = useMemo(
89
+ () => ({ open, setOpen, setValue, value }),
90
+ [value, setValue, open, setOpen]
91
+ );
92
+
93
+ return (
94
+ <VoiceSelectorContext.Provider value={voiceSelectorContext}>
95
+ <Dialog onOpenChange={setOpen} open={open} {...props}>
96
+ {children}
97
+ </Dialog>
98
+ </VoiceSelectorContext.Provider>
99
+ );
100
+ };
101
+
102
+ export type VoiceSelectorTriggerProps = ComponentProps<typeof DialogTrigger>;
103
+
104
+ export const VoiceSelectorTrigger = (props: VoiceSelectorTriggerProps) => (
105
+ <DialogTrigger {...props} />
106
+ );
107
+
108
+ export type VoiceSelectorContentProps = ComponentProps<typeof DialogContent> & {
109
+ title?: ReactNode;
110
+ };
111
+
112
+ export const VoiceSelectorContent = ({
113
+ className,
114
+ children,
115
+ title = "Voice Selector",
116
+ ...props
117
+ }: VoiceSelectorContentProps) => (
118
+ <DialogContent
119
+ aria-describedby={undefined}
120
+ className={cn("p-0", className)}
121
+ {...props}
122
+ >
123
+ <DialogTitle className="sr-only">{title}</DialogTitle>
124
+ <Command className="**:data-[slot=command-input-wrapper]:h-auto">
125
+ {children}
126
+ </Command>
127
+ </DialogContent>
128
+ );
129
+
130
+ export type VoiceSelectorDialogProps = ComponentProps<typeof CommandDialog>;
131
+
132
+ export const VoiceSelectorDialog = (props: VoiceSelectorDialogProps) => (
133
+ <CommandDialog {...props} />
134
+ );
135
+
136
+ export type VoiceSelectorInputProps = ComponentProps<typeof CommandInput>;
137
+
138
+ export const VoiceSelectorInput = ({
139
+ className,
140
+ ...props
141
+ }: VoiceSelectorInputProps) => (
142
+ <CommandInput className={cn("h-auto py-3.5", className)} {...props} />
143
+ );
144
+
145
+ export type VoiceSelectorListProps = ComponentProps<typeof CommandList>;
146
+
147
+ export const VoiceSelectorList = (props: VoiceSelectorListProps) => (
148
+ <CommandList {...props} />
149
+ );
150
+
151
+ export type VoiceSelectorEmptyProps = ComponentProps<typeof CommandEmpty>;
152
+
153
+ export const VoiceSelectorEmpty = (props: VoiceSelectorEmptyProps) => (
154
+ <CommandEmpty {...props} />
155
+ );
156
+
157
+ export type VoiceSelectorGroupProps = ComponentProps<typeof CommandGroup>;
158
+
159
+ export const VoiceSelectorGroup = (props: VoiceSelectorGroupProps) => (
160
+ <CommandGroup {...props} />
161
+ );
162
+
163
+ export type VoiceSelectorItemProps = ComponentProps<typeof CommandItem>;
164
+
165
+ export const VoiceSelectorItem = ({
166
+ className,
167
+ ...props
168
+ }: VoiceSelectorItemProps) => (
169
+ <CommandItem className={cn("px-4 py-2", className)} {...props} />
170
+ );
171
+
172
+ export type VoiceSelectorShortcutProps = ComponentProps<typeof CommandShortcut>;
173
+
174
+ export const VoiceSelectorShortcut = (props: VoiceSelectorShortcutProps) => (
175
+ <CommandShortcut {...props} />
176
+ );
177
+
178
+ export type VoiceSelectorSeparatorProps = ComponentProps<
179
+ typeof CommandSeparator
180
+ >;
181
+
182
+ export const VoiceSelectorSeparator = (props: VoiceSelectorSeparatorProps) => (
183
+ <CommandSeparator {...props} />
184
+ );
185
+
186
+ export type VoiceSelectorGenderProps = ComponentProps<"span"> & {
187
+ value?:
188
+ | "male"
189
+ | "female"
190
+ | "transgender"
191
+ | "androgyne"
192
+ | "non-binary"
193
+ | "intersex";
194
+ };
195
+
196
+ export const VoiceSelectorGender = ({
197
+ className,
198
+ value,
199
+ children,
200
+ ...props
201
+ }: VoiceSelectorGenderProps) => {
202
+ let icon: ReactNode | null = null;
203
+
204
+ switch (value) {
205
+ case "male": {
206
+ icon = <MarsIcon className="size-4" />;
207
+ break;
208
+ }
209
+ case "female": {
210
+ icon = <VenusIcon className="size-4" />;
211
+ break;
212
+ }
213
+ case "transgender": {
214
+ icon = <TransgenderIcon className="size-4" />;
215
+ break;
216
+ }
217
+ case "androgyne": {
218
+ icon = <MarsStrokeIcon className="size-4" />;
219
+ break;
220
+ }
221
+ case "non-binary": {
222
+ icon = <NonBinaryIcon className="size-4" />;
223
+ break;
224
+ }
225
+ case "intersex": {
226
+ icon = <VenusAndMarsIcon className="size-4" />;
227
+ break;
228
+ }
229
+ default: {
230
+ icon = <CircleSmallIcon className="size-4" />;
231
+ }
232
+ }
233
+
234
+ return (
235
+ <span className={cn("text-muted-foreground text-xs", className)} {...props}>
236
+ {children ?? icon}
237
+ </span>
238
+ );
239
+ };
240
+
241
+ export type VoiceSelectorAccentProps = ComponentProps<"span"> & {
242
+ value?:
243
+ | "american"
244
+ | "british"
245
+ | "australian"
246
+ | "canadian"
247
+ | "irish"
248
+ | "scottish"
249
+ | "indian"
250
+ | "south-african"
251
+ | "new-zealand"
252
+ | "spanish"
253
+ | "french"
254
+ | "german"
255
+ | "italian"
256
+ | "portuguese"
257
+ | "brazilian"
258
+ | "mexican"
259
+ | "argentinian"
260
+ | "japanese"
261
+ | "chinese"
262
+ | "korean"
263
+ | "russian"
264
+ | "arabic"
265
+ | "dutch"
266
+ | "swedish"
267
+ | "norwegian"
268
+ | "danish"
269
+ | "finnish"
270
+ | "polish"
271
+ | "turkish"
272
+ | "greek"
273
+ | string;
274
+ };
275
+
276
+ export const VoiceSelectorAccent = ({
277
+ className,
278
+ value,
279
+ children,
280
+ ...props
281
+ }: VoiceSelectorAccentProps) => {
282
+ let emoji: string | null = null;
283
+
284
+ switch (value) {
285
+ case "american": {
286
+ emoji = "🇺🇸";
287
+ break;
288
+ }
289
+ case "british": {
290
+ emoji = "🇬🇧";
291
+ break;
292
+ }
293
+ case "australian": {
294
+ emoji = "🇦🇺";
295
+ break;
296
+ }
297
+ case "canadian": {
298
+ emoji = "🇨🇦";
299
+ break;
300
+ }
301
+ case "irish": {
302
+ emoji = "🇮🇪";
303
+ break;
304
+ }
305
+ case "scottish": {
306
+ emoji = "🏴󠁧󠁢󠁳󠁣󠁴󠁿";
307
+ break;
308
+ }
309
+ case "indian": {
310
+ emoji = "🇮🇳";
311
+ break;
312
+ }
313
+ case "south-african": {
314
+ emoji = "🇿🇦";
315
+ break;
316
+ }
317
+ case "new-zealand": {
318
+ emoji = "🇳🇿";
319
+ break;
320
+ }
321
+ case "spanish": {
322
+ emoji = "🇪🇸";
323
+ break;
324
+ }
325
+ case "french": {
326
+ emoji = "🇫🇷";
327
+ break;
328
+ }
329
+ case "german": {
330
+ emoji = "🇩🇪";
331
+ break;
332
+ }
333
+ case "italian": {
334
+ emoji = "🇮🇹";
335
+ break;
336
+ }
337
+ case "portuguese": {
338
+ emoji = "🇵🇹";
339
+ break;
340
+ }
341
+ case "brazilian": {
342
+ emoji = "🇧🇷";
343
+ break;
344
+ }
345
+ case "mexican": {
346
+ emoji = "🇲🇽";
347
+ break;
348
+ }
349
+ case "argentinian": {
350
+ emoji = "🇦🇷";
351
+ break;
352
+ }
353
+ case "japanese": {
354
+ emoji = "🇯🇵";
355
+ break;
356
+ }
357
+ case "chinese": {
358
+ emoji = "🇨🇳";
359
+ break;
360
+ }
361
+ case "korean": {
362
+ emoji = "🇰🇷";
363
+ break;
364
+ }
365
+ case "russian": {
366
+ emoji = "🇷🇺";
367
+ break;
368
+ }
369
+ case "arabic": {
370
+ emoji = "🇸🇦";
371
+ break;
372
+ }
373
+ case "dutch": {
374
+ emoji = "🇳🇱";
375
+ break;
376
+ }
377
+ case "swedish": {
378
+ emoji = "🇸🇪";
379
+ break;
380
+ }
381
+ case "norwegian": {
382
+ emoji = "🇳🇴";
383
+ break;
384
+ }
385
+ case "danish": {
386
+ emoji = "🇩🇰";
387
+ break;
388
+ }
389
+ case "finnish": {
390
+ emoji = "🇫🇮";
391
+ break;
392
+ }
393
+ case "polish": {
394
+ emoji = "🇵🇱";
395
+ break;
396
+ }
397
+ case "turkish": {
398
+ emoji = "🇹🇷";
399
+ break;
400
+ }
401
+ case "greek": {
402
+ emoji = "🇬🇷";
403
+ break;
404
+ }
405
+ default: {
406
+ emoji = null;
407
+ }
408
+ }
409
+
410
+ return (
411
+ <span className={cn("text-muted-foreground text-xs", className)} {...props}>
412
+ {children ?? emoji}
413
+ </span>
414
+ );
415
+ };
416
+
417
+ export type VoiceSelectorAgeProps = ComponentProps<"span">;
418
+
419
+ export const VoiceSelectorAge = ({
420
+ className,
421
+ ...props
422
+ }: VoiceSelectorAgeProps) => (
423
+ <span
424
+ className={cn("text-muted-foreground text-xs tabular-nums", className)}
425
+ {...props}
426
+ />
427
+ );
428
+
429
+ export type VoiceSelectorNameProps = ComponentProps<"span">;
430
+
431
+ export const VoiceSelectorName = ({
432
+ className,
433
+ ...props
434
+ }: VoiceSelectorNameProps) => (
435
+ <span
436
+ className={cn("flex-1 truncate text-left font-medium", className)}
437
+ {...props}
438
+ />
439
+ );
440
+
441
+ export type VoiceSelectorDescriptionProps = ComponentProps<"span">;
442
+
443
+ export const VoiceSelectorDescription = ({
444
+ className,
445
+ ...props
446
+ }: VoiceSelectorDescriptionProps) => (
447
+ <span className={cn("text-muted-foreground text-xs", className)} {...props} />
448
+ );
449
+
450
+ export type VoiceSelectorAttributesProps = ComponentProps<"div">;
451
+
452
+ export const VoiceSelectorAttributes = ({
453
+ className,
454
+ children,
455
+ ...props
456
+ }: VoiceSelectorAttributesProps) => (
457
+ <div className={cn("flex items-center text-xs", className)} {...props}>
458
+ {children}
459
+ </div>
460
+ );
461
+
462
+ export type VoiceSelectorBulletProps = ComponentProps<"span">;
463
+
464
+ export const VoiceSelectorBullet = ({
465
+ className,
466
+ ...props
467
+ }: VoiceSelectorBulletProps) => (
468
+ <span
469
+ aria-hidden="true"
470
+ className={cn("select-none text-border", className)}
471
+ {...props}
472
+ >
473
+ &bull;
474
+ </span>
475
+ );
476
+
477
+ export type VoiceSelectorPreviewProps = Omit<
478
+ ComponentProps<"button">,
479
+ "children"
480
+ > & {
481
+ playing?: boolean;
482
+ loading?: boolean;
483
+ onPlay?: () => void;
484
+ };
485
+
486
+ export const VoiceSelectorPreview = ({
487
+ className,
488
+ playing,
489
+ loading,
490
+ onPlay,
491
+ onClick,
492
+ ...props
493
+ }: VoiceSelectorPreviewProps) => {
494
+ const handleClick = useCallback(
495
+ (event: React.MouseEvent<HTMLButtonElement>) => {
496
+ event.stopPropagation();
497
+ onClick?.(event);
498
+ onPlay?.();
499
+ },
500
+ [onClick, onPlay]
501
+ );
502
+
503
+ let icon = <PlayIcon className="size-3" />;
504
+
505
+ if (loading) {
506
+ icon = <Spinner className="size-3" />;
507
+ } else if (playing) {
508
+ icon = <PauseIcon className="size-3" />;
509
+ }
510
+
511
+ return (
512
+ <Button
513
+ aria-label={playing ? "Pause preview" : "Play preview"}
514
+ className={cn("size-6", className)}
515
+ disabled={loading}
516
+ onClick={handleClick}
517
+ size="icon-sm"
518
+ type="button"
519
+ variant="outline"
520
+ {...props}
521
+ >
522
+ {icon}
523
+ </Button>
524
+ );
525
+ };