@aomi-labs/widget-lib 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,16 @@
1
1
  "use client";
2
2
 
3
- import { type CSSProperties, type ReactNode } from "react";
4
3
  import {
5
- AomiRuntimeProvider,
6
- cn,
7
- useAomiRuntime,
8
- type UserConfig,
9
- } from "@aomi-labs/react";
4
+ type CSSProperties,
5
+ type ReactNode,
6
+ type FC,
7
+ createContext,
8
+ useContext,
9
+ } from "react";
10
+ import { AomiRuntimeProvider, cn, useAomiRuntime } from "@aomi-labs/react";
10
11
  import { Thread } from "@/components/assistant-ui/thread";
11
12
  import { ThreadListSidebar } from "@/components/assistant-ui/threadlist-sidebar";
13
+ import { NotificationToaster } from "@/components/ui/notification";
12
14
  import {
13
15
  SidebarInset,
14
16
  SidebarProvider,
@@ -19,110 +21,201 @@ import {
19
21
  Breadcrumb,
20
22
  BreadcrumbItem,
21
23
  BreadcrumbList,
22
- BreadcrumbSeparator,
23
24
  } from "@/components/ui/breadcrumb";
25
+ import { ControlBar, type ControlBarProps } from "@/components/control-bar";
26
+
27
+ // =============================================================================
28
+ // Composer Control Context - signals Thread to show inline controls
29
+ // =============================================================================
30
+
31
+ const ComposerControlContext = createContext<boolean>(false);
32
+
33
+ export const useComposerControl = () => useContext(ComposerControlContext);
24
34
 
25
35
  // =============================================================================
26
36
  // Types
27
37
  // =============================================================================
28
38
 
29
- type AomiFrameProps = {
39
+ type RootProps = {
40
+ children?: ReactNode;
30
41
  width?: CSSProperties["width"];
31
42
  height?: CSSProperties["height"];
32
43
  className?: string;
33
44
  style?: CSSProperties;
34
- /** Render prop for wallet footer - receives user state and setter from UserContext */
35
- walletFooter?: (props: UserConfig) => ReactNode;
36
- /** Additional content to render inside the frame */
45
+ /** Position of the wallet button in the sidebar */
46
+ walletPosition?: "header" | "footer" | null;
47
+ /** Backend URL for the Aomi runtime */
48
+ backendUrl?: string;
49
+ };
50
+
51
+ type HeaderProps = {
37
52
  children?: ReactNode;
53
+ /** Show the control bar in the header */
54
+ withControl?: boolean;
55
+ /** Props to pass to the ControlBar when withControl is true */
56
+ controlBarProps?: Omit<ControlBarProps, "children">;
57
+ className?: string;
38
58
  };
39
59
 
60
+ type ComposerProps = {
61
+ children?: ReactNode;
62
+ /** Show inline controls in the composer input area */
63
+ withControl?: boolean;
64
+ className?: string;
65
+ };
66
+
67
+ type FrameControlBarProps = ControlBarProps;
68
+
40
69
  // =============================================================================
41
- // AomiFrame Component
70
+ // Compound Components
42
71
  // =============================================================================
43
72
 
44
- export const AomiFrame = ({
73
+ /**
74
+ * Root component - provides all context and layout container
75
+ */
76
+ const Root: FC<RootProps> = ({
77
+ children,
45
78
  width = "100%",
46
79
  height = "80vh",
47
80
  className,
48
81
  style,
49
- walletFooter,
50
- children,
51
- }: AomiFrameProps) => {
52
- const backendUrl =
53
- process.env.NEXT_PUBLIC_BACKEND_URL ?? "http://localhost:8080";
82
+ walletPosition = "footer",
83
+ backendUrl,
84
+ }) => {
85
+ const resolvedBackendUrl =
86
+ backendUrl ??
87
+ process.env.NEXT_PUBLIC_BACKEND_URL ??
88
+ "http://localhost:8080";
89
+ const frameStyle: CSSProperties = { width, height, ...style };
54
90
 
55
91
  return (
56
- <AomiRuntimeProvider backendUrl={backendUrl}>
57
- <AomiFrameShell
58
- width={width}
59
- height={height}
60
- className={className}
61
- style={style}
62
- walletFooter={walletFooter}
63
- >
64
- {children}
65
- </AomiFrameShell>
92
+ <AomiRuntimeProvider backendUrl={resolvedBackendUrl}>
93
+ <SidebarProvider>
94
+ <div
95
+ className={cn(
96
+ "rounded-4xl flex h-full w-full overflow-hidden bg-white shadow-2xl dark:bg-neutral-950",
97
+ className,
98
+ )}
99
+ style={frameStyle}
100
+ >
101
+ <ThreadListSidebar walletPosition={walletPosition} />
102
+ <SidebarInset className="relative flex flex-col">
103
+ {children}
104
+ </SidebarInset>
105
+ <NotificationToaster />
106
+ </div>
107
+ </SidebarProvider>
66
108
  </AomiRuntimeProvider>
67
109
  );
68
110
  };
69
111
 
70
- // =============================================================================
71
- // Internal Shell Component (uses hooks from providers)
72
- // =============================================================================
112
+ /**
113
+ * Header component - renders the header with optional control bar
114
+ */
115
+ const Header: FC<HeaderProps> = ({
116
+ children,
117
+ withControl,
118
+ controlBarProps,
119
+ className,
120
+ }) => {
121
+ const { currentThreadId, getThreadMetadata } = useAomiRuntime();
122
+ const currentTitle = getThreadMetadata(currentThreadId)?.title ?? "New Chat";
73
123
 
74
- type AomiFrameShellProps = {
75
- width: CSSProperties["width"];
76
- height: CSSProperties["height"];
77
- className?: string;
78
- style?: CSSProperties;
79
- walletFooter?: (props: UserConfig) => ReactNode;
80
- children?: ReactNode;
124
+ return (
125
+ <header
126
+ className={cn(
127
+ "mt-1 flex h-14 shrink-0 items-center gap-2 px-3",
128
+ className,
129
+ )}
130
+ >
131
+ <SidebarTrigger />
132
+ <Separator orientation="vertical" className="mr-2 h-4" />
133
+ <Breadcrumb>
134
+ <BreadcrumbList>
135
+ <BreadcrumbItem className="hidden md:block">
136
+ {currentTitle}
137
+ </BreadcrumbItem>
138
+ </BreadcrumbList>
139
+ </Breadcrumb>
140
+ <div className="ml-auto flex items-center gap-2">
141
+ {withControl && <ControlBar {...controlBarProps} />}
142
+ {children}
143
+ </div>
144
+ </header>
145
+ );
81
146
  };
82
147
 
83
- const AomiFrameShell = ({
84
- width,
85
- height,
86
- className,
87
- style,
88
- walletFooter,
148
+ /**
149
+ * Composer component - renders the thread with optional inline controls
150
+ * When withControl={true}, controls appear inline in the composer input area
151
+ */
152
+ const Composer: FC<ComposerProps> = ({
89
153
  children,
90
- }: AomiFrameShellProps) => {
91
- const { user, setUser, currentThreadId, threadViewKey, getThreadMetadata } =
92
- useAomiRuntime();
93
- const currentTitle = getThreadMetadata(currentThreadId)?.title ?? "New Chat";
94
-
95
- const frameStyle: CSSProperties = { width, height, ...style };
154
+ withControl = false,
155
+ className,
156
+ }) => {
157
+ const { currentThreadId, threadViewKey } = useAomiRuntime();
96
158
 
97
159
  return (
98
- <SidebarProvider>
99
- {children}
100
- <div
101
- className={cn(
102
- "flex h-full w-full overflow-hidden rounded-2xl bg-white shadow-2xl dark:bg-neutral-950",
103
- className,
104
- )}
105
- style={frameStyle}
106
- >
107
- <ThreadListSidebar footer={walletFooter?.({ user, setUser })} />
108
- <SidebarInset className="relative">
109
- <header className="mt-1 flex h-14 shrink-0 items-center gap-2 border-b px-3">
110
- <SidebarTrigger />
111
- <Separator orientation="vertical" className="mr-2 h-4" />
112
- <Breadcrumb>
113
- <BreadcrumbList>
114
- <BreadcrumbItem className="hidden md:block">
115
- {currentTitle}
116
- </BreadcrumbItem>
117
- <BreadcrumbSeparator className="hidden md:block" />
118
- </BreadcrumbList>
119
- </Breadcrumb>
120
- </header>
121
- <div className="flex-1 overflow-hidden">
122
- <Thread key={`${currentThreadId}-${threadViewKey}`} />
123
- </div>
124
- </SidebarInset>
160
+ <ComposerControlContext.Provider value={withControl}>
161
+ <div className={cn("flex flex-1 flex-col overflow-hidden", className)}>
162
+ <Thread key={`${currentThreadId}-${threadViewKey}`} />
163
+ {children}
125
164
  </div>
126
- </SidebarProvider>
165
+ </ComposerControlContext.Provider>
127
166
  );
128
167
  };
168
+
169
+ /**
170
+ * ControlBar component - wrapper for the control bar with frame styling
171
+ */
172
+ const FrameControlBar: FC<FrameControlBarProps> = (props) => {
173
+ return <ControlBar {...props} />;
174
+ };
175
+
176
+ // =============================================================================
177
+ // Default Layout Component (Simple API)
178
+ // =============================================================================
179
+
180
+ type DefaultLayoutProps = Omit<RootProps, "children">;
181
+
182
+ /**
183
+ * Default layout - controls are inline in the composer input area
184
+ * Usage: <AomiFrame /> or <AomiFrame walletPosition="header" />
185
+ */
186
+ const DefaultLayout: FC<DefaultLayoutProps> = ({
187
+ walletPosition = "footer",
188
+ ...props
189
+ }) => {
190
+ // Hide wallet in ControlBar when it's shown in sidebar
191
+ const hideWalletInControlBar = walletPosition !== null;
192
+
193
+ return (
194
+ <Root walletPosition={walletPosition} {...props}>
195
+ <Header
196
+ withControl
197
+ controlBarProps={{ hideWallet: hideWalletInControlBar }}
198
+ />
199
+ <Composer />
200
+ </Root>
201
+ );
202
+ };
203
+
204
+ // =============================================================================
205
+ // Export Compound Component
206
+ // =============================================================================
207
+
208
+ export const AomiFrame = Object.assign(DefaultLayout, {
209
+ Root,
210
+ Header,
211
+ Composer,
212
+ ControlBar: FrameControlBar,
213
+ });
214
+
215
+ // Re-export types for consumers
216
+ export type {
217
+ RootProps as AomiFrameRootProps,
218
+ HeaderProps as AomiFrameHeaderProps,
219
+ ComposerProps as AomiFrameComposerProps,
220
+ FrameControlBarProps as AomiFrameControlBarProps,
221
+ };
@@ -14,7 +14,7 @@ import { Skeleton } from "@/components/ui/skeleton";
14
14
 
15
15
  export const ThreadList: FC = () => {
16
16
  return (
17
- <ThreadListPrimitive.Root className="aui-root aui-thread-list-root flex flex-col items-stretch gap-1.5">
17
+ <ThreadListPrimitive.Root className="aui-root aui-thread-list-root flex list-none flex-col items-stretch gap-1 pl-2">
18
18
  <ThreadListNew />
19
19
  <ThreadListItems />
20
20
  </ThreadListPrimitive.Root>
@@ -25,10 +25,10 @@ const ThreadListNew: FC = () => {
25
25
  return (
26
26
  <ThreadListPrimitive.New asChild>
27
27
  <Button
28
- className="aui-thread-list-new hover:bg-muted data-active:bg-muted flex items-center justify-start gap-1 rounded-lg px-2.5 py-2 text-start"
28
+ className="aui-thread-list-new hover:bg-accent data-active:bg-accent flex items-center justify-start gap-2 rounded-full px-4 py-2 text-start"
29
29
  variant="ghost"
30
30
  >
31
- <PlusIcon />
31
+ <PlusIcon className="size-4" />
32
32
  New Chat
33
33
  </Button>
34
34
  </ThreadListPrimitive.New>
@@ -65,8 +65,8 @@ const ThreadListSkeleton: FC = () => {
65
65
 
66
66
  const ThreadListItem: FC = () => {
67
67
  return (
68
- <ThreadListItemPrimitive.Root className="aui-thread-list-item hover:bg-muted focus-visible:bg-muted focus-visible:ring-ring data-active:bg-muted flex items-center gap-2 rounded-lg transition-all focus-visible:outline-none focus-visible:ring-2">
69
- <ThreadListItemPrimitive.Trigger className="aui-thread-list-item-trigger flex-grow px-3 py-2 text-start">
68
+ <ThreadListItemPrimitive.Root className="aui-thread-list-item hover:bg-accent focus-visible:bg-accent data-active:bg-accent flex items-center gap-2 rounded-full pl-4 transition-all focus-visible:outline-none">
69
+ <ThreadListItemPrimitive.Trigger className="aui-thread-list-item-trigger flex-grow py-2 text-start">
70
70
  <ThreadListItemTitle />
71
71
  </ThreadListItemPrimitive.Trigger>
72
72
  <ThreadListItemDelete />
@@ -37,6 +37,10 @@ import {
37
37
  } from "@/components/assistant-ui/attachment";
38
38
 
39
39
  import { cn, useNotification, useThreadContext } from "@aomi-labs/react";
40
+ import { useComposerControl } from "@/components/aomi-frame";
41
+ import { ModelSelect } from "@/components/control-bar/model-select";
42
+ import { NamespaceSelect } from "@/components/control-bar/namespace-select";
43
+ import { ApiKeyInput } from "@/components/control-bar/api-key-input";
40
44
  import { useAssistantApi, useMessage } from "@assistant-ui/react";
41
45
 
42
46
  const seenSystemMessages = new Set<string>();
@@ -64,7 +68,7 @@ export const Thread: FC = () => {
64
68
  ["--thread-max-width" as string]: "44rem",
65
69
  }}
66
70
  >
67
- <ThreadPrimitive.Viewport className="aui-thread-viewport relative flex flex-1 flex-col overflow-x-auto overflow-y-scroll px-4">
71
+ <ThreadPrimitive.Viewport className="aui-thread-viewport relative flex flex-1 flex-col overflow-x-auto overflow-y-scroll px-2">
68
72
  <ThreadPrimitive.If empty>
69
73
  <ThreadWelcome />
70
74
  </ThreadPrimitive.If>
@@ -173,10 +177,10 @@ const ThreadSuggestions: FC = () => {
173
177
  >
174
178
  <Button
175
179
  variant="ghost"
176
- className="aui-thread-welcome-suggestion @md:flex-col dark:hover:bg-accent/60 h-auto w-full flex-1 flex-wrap items-start justify-start gap-1 rounded-3xl border px-5 py-4 text-left text-sm"
180
+ className="aui-thread-welcome-suggestion @md:flex-col dark:hover:bg-accent/60 h-auto w-full flex-1 flex-wrap items-start justify-start gap-1 rounded-3xl border px-5 py-4 text-left text-sm font-normal"
177
181
  aria-label={suggestedAction.action}
178
182
  >
179
- <span className="aui-thread-welcome-suggestion-text-1 font-medium">
183
+ <span className="aui-thread-welcome-suggestion-text-1">
180
184
  {suggestedAction.title}
181
185
  </span>
182
186
  <span className="aui-thread-welcome-suggestion-text-2 text-muted-foreground">
@@ -194,11 +198,11 @@ const Composer: FC = () => {
194
198
  return (
195
199
  <div className="aui-composer-wrapper bg-background sticky bottom-0 mx-auto flex w-full max-w-[var(--thread-max-width)] flex-col gap-4 overflow-visible rounded-t-3xl pb-4 md:pb-6">
196
200
  <ThreadScrollToBottom />
197
- <ComposerPrimitive.Root className="aui-composer-root rounded-4xl dark:border-muted-foreground/15 relative flex w-full flex-col border bg-white px-1 pt-2 shadow-[0_9px_9px_0px_rgba(0,0,0,0.01),0_2px_5px_0px_rgba(0,0,0,0.06)]">
201
+ <ComposerPrimitive.Root className="aui-composer-root rounded-4xl bg-sidebar text-card-foreground relative flex w-full flex-col px-1 pt-2">
198
202
  <ComposerAttachments />
199
203
  <ComposerPrimitive.Input
200
204
  placeholder="Send a message..."
201
- className="aui-composer-input placeholder:text-muted-foreground focus:outline-primary ml-3 mt-2 max-h-32 min-h-16 w-full resize-none bg-transparent px-3.5 pb-3 pt-1.5 text-sm text-stone-800 outline-none"
205
+ className="aui-composer-input text-foreground dark:text-white placeholder:text-muted-foreground focus:outline-primary ml-3 mt-2 max-h-32 min-h-16 w-full resize-none bg-transparent px-3.5 pb-3 pt-1.5 text-sm outline-none"
202
206
  rows={1}
203
207
  autoFocus
204
208
  aria-label="Message input"
@@ -210,9 +214,24 @@ const Composer: FC = () => {
210
214
  };
211
215
 
212
216
  const ComposerAction: FC = () => {
217
+ const showInlineControls = useComposerControl();
218
+
213
219
  return (
214
- <div className="aui-composer-action-wrapper relative mx-1 mb-2 mt-2 flex items-center justify-between">
215
- <ComposerAddAttachment />
220
+ <div className="aui-composer-action-wrapper relative mx-1 mb-2 mt-2 flex items-center">
221
+ {/* Show attachment button only when inline controls are hidden */}
222
+ {!showInlineControls && <ComposerAddAttachment />}
223
+
224
+ {/* Inline controls: [Model ▾] [Agent ▾] [🔑] */}
225
+ {showInlineControls && (
226
+ <div className="ml-2 flex items-center gap-2">
227
+ <ModelSelect />
228
+ <NamespaceSelect />
229
+ <ApiKeyInput />
230
+ </div>
231
+ )}
232
+
233
+ {/* Spacer */}
234
+ <div className="flex-1" />
216
235
 
217
236
  <ThreadPrimitive.If running={false}>
218
237
  <ComposerPrimitive.Send asChild>
@@ -355,7 +374,7 @@ const EditComposer: FC = () => {
355
374
  <div className="aui-edit-composer-wrapper mx-auto flex w-full max-w-[var(--thread-max-width)] flex-col gap-4 px-2 first:mt-4">
356
375
  <ComposerPrimitive.Root className="aui-edit-composer-root max-w-7/8 bg-muted ml-auto flex w-full flex-col rounded-xl">
357
376
  <ComposerPrimitive.Input
358
- className="aui-edit-composer-input text-foreground flex min-h-[60px] w-full resize-none bg-transparent p-4 outline-none"
377
+ className="aui-edit-composer-input text-foreground dark:text-white flex min-h-[60px] w-full resize-none bg-transparent p-4 outline-none"
359
378
  autoFocus
360
379
  />
361
380
 
@@ -14,14 +14,15 @@ import {
14
14
  SidebarRail,
15
15
  } from "@/components/ui/sidebar";
16
16
  import { ThreadList } from "@/components/assistant-ui/thread-list";
17
+ import { WalletConnect } from "@/components/control-bar/wallet-connect";
17
18
 
18
19
  type ThreadListSidebarProps = React.ComponentProps<typeof Sidebar> & {
19
- /** Optional footer component (e.g., WalletFooter from consumer app) */
20
- footer?: React.ReactNode;
20
+ /** Position of the wallet button: "header" (top), "footer" (bottom), or null (hidden) */
21
+ walletPosition?: "header" | "footer" | null;
21
22
  };
22
23
 
23
24
  export function ThreadListSidebar({
24
- footer,
25
+ walletPosition = "footer",
25
26
  ...props
26
27
  }: ThreadListSidebarProps) {
27
28
  return (
@@ -32,38 +33,32 @@ export function ThreadListSidebar({
32
33
  {...props}
33
34
  >
34
35
  <SidebarHeader className="aomi-sidebar-header">
35
- <div className="aomi-sidebar-header-content flex items-center justify-between">
36
- <SidebarMenu>
37
- <SidebarMenuItem>
38
- <SidebarMenuButton size="lg" asChild>
39
- <Link
40
- href="https://aomi.dev"
41
- target="_blank"
42
- rel="noopener noreferrer"
43
- >
44
- <div className="aomi-sidebar-header-icon-wrapper flex aspect-square size-8 items-center justify-center rounded-lg bg-white">
45
- <Image
46
- src="/assets/images/bubble.svg"
47
- alt="Logo"
48
- width={28}
49
- height={28}
50
- className="aomi-sidebar-header-icon ml-3 size-7"
51
- priority
52
- />
53
- </div>
54
- </Link>
55
- </SidebarMenuButton>
56
- </SidebarMenuItem>
57
- </SidebarMenu>
36
+ <div className="aomi-sidebar-header-content mt-5 mb-5 ml-5 flex items-center justify-between">
37
+ <Link
38
+ href="https://aomi.dev"
39
+ target="_blank"
40
+ rel="noopener noreferrer"
41
+ className="flex items-center justify-center"
42
+ >
43
+ <Image
44
+ src="/assets/images/bubble.svg"
45
+ alt="Logo"
46
+ width={25}
47
+ height={25}
48
+ className="aomi-sidebar-header-icon size-6"
49
+ priority
50
+ />
51
+ </Link>
52
+ {walletPosition === "header" && <WalletConnect />}
58
53
  </div>
59
54
  </SidebarHeader>
60
55
  <SidebarContent className="aomi-sidebar-content">
61
56
  <ThreadList />
62
57
  </SidebarContent>
63
58
  <SidebarRail />
64
- {footer && (
65
- <SidebarFooter className="aomi-sidebar-footer border-sm border-t py-4">
66
- {footer}
59
+ {walletPosition === "footer" && (
60
+ <SidebarFooter className="aomi-sidebar-footer border-0 mx-5 mb-5">
61
+ <WalletConnect className="w-full" />
67
62
  </SidebarFooter>
68
63
  )}
69
64
  </Sidebar>
@@ -0,0 +1,122 @@
1
+ "use client";
2
+
3
+ import { useState, type FC } from "react";
4
+ import { KeyIcon, CheckIcon, EyeIcon, EyeOffIcon } from "lucide-react";
5
+ import { useControl, cn } from "@aomi-labs/react";
6
+ import { Button } from "@/components/ui/button";
7
+ import { Input } from "@/components/ui/input";
8
+ import { Label } from "@/components/ui/label";
9
+ import {
10
+ Dialog,
11
+ DialogContent,
12
+ DialogDescription,
13
+ DialogHeader,
14
+ DialogTitle,
15
+ DialogTrigger,
16
+ DialogFooter,
17
+ } from "@/components/ui/dialog";
18
+
19
+ export type ApiKeyInputProps = {
20
+ className?: string;
21
+ title?: string;
22
+ description?: string;
23
+ };
24
+
25
+ export const ApiKeyInput: FC<ApiKeyInputProps> = ({
26
+ className,
27
+ title = "Aomi API Key",
28
+ description = "Enter your API key to authenticate with Aomi services.",
29
+ }) => {
30
+ const { state, setState } = useControl();
31
+ const [open, setOpen] = useState(false);
32
+ const [inputValue, setInputValue] = useState("");
33
+ const [showKey, setShowKey] = useState(false);
34
+
35
+ const hasApiKey = Boolean(state.apiKey);
36
+
37
+ return (
38
+ <Dialog open={open} onOpenChange={setOpen}>
39
+ <DialogTrigger asChild>
40
+ <Button
41
+ variant="ghost"
42
+ size="icon"
43
+ className={cn("relative rounded-full", className)}
44
+ aria-label={hasApiKey ? "API key configured" : "Set API key"}
45
+ >
46
+ <KeyIcon className={cn("h-4 w-4", hasApiKey && "text-green-500")} />
47
+ {hasApiKey && (
48
+ <span className="absolute -right-0.5 -top-0.5 h-2 w-2 rounded-full bg-green-500" />
49
+ )}
50
+ </Button>
51
+ </DialogTrigger>
52
+ <DialogContent className="max-w-[280px] pl-4 rounded-3xl">
53
+ <DialogHeader className="border-0">
54
+ <DialogTitle>{title}</DialogTitle>
55
+ <DialogDescription>{description}</DialogDescription>
56
+ </DialogHeader>
57
+ <div className="grid gap-4 py-4">
58
+ <div className="grid gap-2">
59
+ <Label htmlFor="api-key" className="mb-2">
60
+ API Key
61
+ </Label>
62
+ <div className="relative">
63
+ <Input
64
+ id="api-key"
65
+ type={showKey ? "text" : "password"}
66
+ placeholder={hasApiKey ? "********" : "Enter your API key"}
67
+ value={inputValue}
68
+ onChange={(e) => setInputValue(e.target.value)}
69
+ className="rounded-full pr-10"
70
+ />
71
+ <Button
72
+ type="button"
73
+ variant="ghost"
74
+ size="icon"
75
+ className="absolute right-0 top-0 h-full px-3 hover:bg-transparent"
76
+ onClick={() => setShowKey(!showKey)}
77
+ aria-label={showKey ? "Hide API key" : "Show API key"}
78
+ >
79
+ {showKey ? (
80
+ <EyeIcon className="h-4 w-4" />
81
+ ) : (
82
+ <EyeOffIcon className="h-4 w-4" />
83
+ )}
84
+ </Button>
85
+ </div>
86
+ {hasApiKey && (
87
+ <p className="text-muted-foreground text-xs">
88
+ <CheckIcon className="mr-1 inline h-3 w-3 text-green-500" />
89
+ API key is configured
90
+ </p>
91
+ )}
92
+ </div>
93
+ </div>
94
+ <DialogFooter>
95
+ {hasApiKey && (
96
+ <Button
97
+ variant="outline"
98
+ onClick={() => {
99
+ setState({ apiKey: null });
100
+ setInputValue("");
101
+ }}
102
+ >
103
+ Clear
104
+ </Button>
105
+ )}
106
+ <Button
107
+ onClick={() => {
108
+ if (inputValue.trim()) {
109
+ setState({ apiKey: inputValue.trim() });
110
+ setOpen(false);
111
+ setInputValue("");
112
+ }
113
+ }}
114
+ disabled={!inputValue.trim()}
115
+ >
116
+ Save
117
+ </Button>
118
+ </DialogFooter>
119
+ </DialogContent>
120
+ </Dialog>
121
+ );
122
+ };
@@ -0,0 +1,58 @@
1
+ "use client";
2
+
3
+ import type { ReactNode, FC } from "react";
4
+ import { cn } from "@aomi-labs/react";
5
+ import { ModelSelect } from "./model-select";
6
+ import { NamespaceSelect } from "./namespace-select";
7
+ import { ApiKeyInput } from "./api-key-input";
8
+ import { WalletConnect } from "./wallet-connect";
9
+
10
+ // =============================================================================
11
+ // Types
12
+ // =============================================================================
13
+
14
+ export type ControlBarProps = {
15
+ className?: string;
16
+ /** Custom controls to render alongside built-in ones */
17
+ children?: ReactNode;
18
+ /** Hide the model selector */
19
+ hideModel?: boolean;
20
+ /** Hide the namespace/agent selector */
21
+ hideNamespace?: boolean;
22
+ /** Hide the API key input */
23
+ hideApiKey?: boolean;
24
+ /** Hide the wallet connect button (default: true) */
25
+ hideWallet?: boolean;
26
+ };
27
+
28
+ // =============================================================================
29
+ // Main Component
30
+ // =============================================================================
31
+
32
+ export const ControlBar: FC<ControlBarProps> = ({
33
+ className,
34
+ children,
35
+ hideModel = false,
36
+ hideNamespace = false,
37
+ hideApiKey = false,
38
+ hideWallet = true,
39
+ }) => {
40
+ return (
41
+ <div className={cn("flex items-center gap-2", className)}>
42
+ {!hideModel && <ModelSelect />}
43
+ {!hideNamespace && <NamespaceSelect />}
44
+ {!hideWallet && <WalletConnect />}
45
+ {children}
46
+ {!hideApiKey && <ApiKeyInput />}
47
+ </div>
48
+ );
49
+ };
50
+
51
+ // =============================================================================
52
+ // Re-exports for granular usage
53
+ // =============================================================================
54
+
55
+ export { ModelSelect, type ModelSelectProps } from "./model-select";
56
+ export { NamespaceSelect, type NamespaceSelectProps } from "./namespace-select";
57
+ export { ApiKeyInput, type ApiKeyInputProps } from "./api-key-input";
58
+ export { WalletConnect, type WalletConnectProps } from "./wallet-connect";