@cloudbase/agent-react-ui 0.0.23
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.
- package/README.md +135 -0
- package/components.json +21 -0
- package/dist/index.css +4241 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.mts +59 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +2169 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2182 -0
- package/dist/index.mjs.map +1 -0
- package/example/.env.sample +2 -0
- package/example/App.tsx +368 -0
- package/example/app.css +1 -0
- package/example/index.html +12 -0
- package/example/main.tsx +9 -0
- package/example/vite.config.ts +34 -0
- package/package.json +75 -0
- package/postcss.config.cjs +3 -0
- package/src/components/ai-elements/agent.tsx +140 -0
- package/src/components/ai-elements/artifact.tsx +147 -0
- package/src/components/ai-elements/attachments.tsx +421 -0
- package/src/components/ai-elements/audio-player.tsx +228 -0
- package/src/components/ai-elements/canvas.tsx +22 -0
- package/src/components/ai-elements/chain-of-thought.tsx +228 -0
- package/src/components/ai-elements/checkpoint.tsx +71 -0
- package/src/components/ai-elements/code-block.tsx +532 -0
- package/src/components/ai-elements/commit.tsx +448 -0
- package/src/components/ai-elements/confirmation.tsx +176 -0
- package/src/components/ai-elements/connection.tsx +28 -0
- package/src/components/ai-elements/context.tsx +408 -0
- package/src/components/ai-elements/controls.tsx +18 -0
- package/src/components/ai-elements/conversation.tsx +100 -0
- package/src/components/ai-elements/edge.tsx +140 -0
- package/src/components/ai-elements/environment-variables.tsx +295 -0
- package/src/components/ai-elements/file-tree.tsx +258 -0
- package/src/components/ai-elements/image.tsx +24 -0
- package/src/components/ai-elements/inline-citation.tsx +287 -0
- package/src/components/ai-elements/message.tsx +336 -0
- package/src/components/ai-elements/mic-selector.tsx +370 -0
- package/src/components/ai-elements/model-selector.tsx +211 -0
- package/src/components/ai-elements/node.tsx +71 -0
- package/src/components/ai-elements/open-in-chat.tsx +365 -0
- package/src/components/ai-elements/package-info.tsx +233 -0
- package/src/components/ai-elements/panel.tsx +15 -0
- package/src/components/ai-elements/persona.tsx +270 -0
- package/src/components/ai-elements/plan.tsx +142 -0
- package/src/components/ai-elements/prompt-input.tsx +1263 -0
- package/src/components/ai-elements/queue.tsx +274 -0
- package/src/components/ai-elements/reasoning.tsx +193 -0
- package/src/components/ai-elements/sandbox.tsx +126 -0
- package/src/components/ai-elements/schema-display.tsx +458 -0
- package/src/components/ai-elements/shimmer.tsx +64 -0
- package/src/components/ai-elements/snippet.tsx +139 -0
- package/src/components/ai-elements/sources.tsx +77 -0
- package/src/components/ai-elements/speech-input.tsx +301 -0
- package/src/components/ai-elements/stack-trace.tsx +482 -0
- package/src/components/ai-elements/suggestion.tsx +53 -0
- package/src/components/ai-elements/task.tsx +87 -0
- package/src/components/ai-elements/terminal.tsx +261 -0
- package/src/components/ai-elements/test-results.tsx +485 -0
- package/src/components/ai-elements/tool.tsx +174 -0
- package/src/components/ai-elements/toolbar.tsx +16 -0
- package/src/components/ai-elements/transcription.tsx +124 -0
- package/src/components/ai-elements/voice-selector.tsx +479 -0
- package/src/components/ai-elements/web-preview.tsx +263 -0
- package/src/components/chat/Chat.tsx +178 -0
- package/src/components/chat/Input.tsx +98 -0
- package/src/components/chat/Message.tsx +276 -0
- package/src/components/chat/index.ts +2 -0
- package/src/components/index.ts +1 -0
- package/src/components/ui/accordion.tsx +64 -0
- package/src/components/ui/alert.tsx +66 -0
- package/src/components/ui/avatar.tsx +107 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/button-group.tsx +83 -0
- package/src/components/ui/button.tsx +64 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/carousel.tsx +239 -0
- package/src/components/ui/collapsible.tsx +31 -0
- package/src/components/ui/command.tsx +184 -0
- package/src/components/ui/dialog.tsx +158 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/hover-card.tsx +42 -0
- package/src/components/ui/input-group.tsx +168 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/popover.tsx +87 -0
- package/src/components/ui/progress.tsx +31 -0
- package/src/components/ui/scroll-area.tsx +56 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/switch.tsx +33 -0
- package/src/components/ui/tabs.tsx +91 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +61 -0
- package/src/css/global.css +123 -0
- package/src/css/index.css +1 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/use-copy-to-clipboard.ts +31 -0
- package/src/index.ts +4 -0
- package/src/lib/utils.ts +6 -0
- package/src/locales/context.ts +8 -0
- package/src/locales/hooks.ts +20 -0
- package/src/locales/index.ts +3 -0
- package/src/locales/langs/en.ts +17 -0
- package/src/locales/langs/index.ts +12 -0
- package/src/locales/langs/zh-cn.ts +18 -0
- package/tsconfig.json +21 -0
- package/tsup.config.ts +21 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import {
|
|
5
|
+
Collapsible,
|
|
6
|
+
CollapsibleContent,
|
|
7
|
+
CollapsibleTrigger,
|
|
8
|
+
} from "@/components/ui/collapsible";
|
|
9
|
+
import { Input } from "@/components/ui/input";
|
|
10
|
+
import {
|
|
11
|
+
Tooltip,
|
|
12
|
+
TooltipContent,
|
|
13
|
+
TooltipProvider,
|
|
14
|
+
TooltipTrigger,
|
|
15
|
+
} from "@/components/ui/tooltip";
|
|
16
|
+
import { cn } from "@/lib/utils";
|
|
17
|
+
import { ChevronDownIcon } from "lucide-react";
|
|
18
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
19
|
+
import { createContext, useContext, useEffect, useState } from "react";
|
|
20
|
+
|
|
21
|
+
export interface WebPreviewContextValue {
|
|
22
|
+
url: string;
|
|
23
|
+
setUrl: (url: string) => void;
|
|
24
|
+
consoleOpen: boolean;
|
|
25
|
+
setConsoleOpen: (open: boolean) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const WebPreviewContext = createContext<WebPreviewContextValue | null>(null);
|
|
29
|
+
|
|
30
|
+
const useWebPreview = () => {
|
|
31
|
+
const context = useContext(WebPreviewContext);
|
|
32
|
+
if (!context) {
|
|
33
|
+
throw new Error("WebPreview components must be used within a WebPreview");
|
|
34
|
+
}
|
|
35
|
+
return context;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type WebPreviewProps = ComponentProps<"div"> & {
|
|
39
|
+
defaultUrl?: string;
|
|
40
|
+
onUrlChange?: (url: string) => void;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const WebPreview = ({
|
|
44
|
+
className,
|
|
45
|
+
children,
|
|
46
|
+
defaultUrl = "",
|
|
47
|
+
onUrlChange,
|
|
48
|
+
...props
|
|
49
|
+
}: WebPreviewProps) => {
|
|
50
|
+
const [url, setUrl] = useState(defaultUrl);
|
|
51
|
+
const [consoleOpen, setConsoleOpen] = useState(false);
|
|
52
|
+
|
|
53
|
+
const handleUrlChange = (newUrl: string) => {
|
|
54
|
+
setUrl(newUrl);
|
|
55
|
+
onUrlChange?.(newUrl);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const contextValue: WebPreviewContextValue = {
|
|
59
|
+
url,
|
|
60
|
+
setUrl: handleUrlChange,
|
|
61
|
+
consoleOpen,
|
|
62
|
+
setConsoleOpen,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<WebPreviewContext.Provider value={contextValue}>
|
|
67
|
+
<div
|
|
68
|
+
className={cn(
|
|
69
|
+
"flex size-full flex-col rounded-lg border bg-card",
|
|
70
|
+
className
|
|
71
|
+
)}
|
|
72
|
+
{...props}
|
|
73
|
+
>
|
|
74
|
+
{children}
|
|
75
|
+
</div>
|
|
76
|
+
</WebPreviewContext.Provider>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export type WebPreviewNavigationProps = ComponentProps<"div">;
|
|
81
|
+
|
|
82
|
+
export const WebPreviewNavigation = ({
|
|
83
|
+
className,
|
|
84
|
+
children,
|
|
85
|
+
...props
|
|
86
|
+
}: WebPreviewNavigationProps) => (
|
|
87
|
+
<div
|
|
88
|
+
className={cn("flex items-center gap-1 border-b p-2", className)}
|
|
89
|
+
{...props}
|
|
90
|
+
>
|
|
91
|
+
{children}
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
export type WebPreviewNavigationButtonProps = ComponentProps<typeof Button> & {
|
|
96
|
+
tooltip?: string;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const WebPreviewNavigationButton = ({
|
|
100
|
+
onClick,
|
|
101
|
+
disabled,
|
|
102
|
+
tooltip,
|
|
103
|
+
children,
|
|
104
|
+
...props
|
|
105
|
+
}: WebPreviewNavigationButtonProps) => (
|
|
106
|
+
<TooltipProvider>
|
|
107
|
+
<Tooltip>
|
|
108
|
+
<TooltipTrigger asChild>
|
|
109
|
+
<Button
|
|
110
|
+
className="h-8 w-8 p-0 hover:text-foreground"
|
|
111
|
+
disabled={disabled}
|
|
112
|
+
onClick={onClick}
|
|
113
|
+
size="sm"
|
|
114
|
+
variant="ghost"
|
|
115
|
+
{...props}
|
|
116
|
+
>
|
|
117
|
+
{children}
|
|
118
|
+
</Button>
|
|
119
|
+
</TooltipTrigger>
|
|
120
|
+
<TooltipContent>
|
|
121
|
+
<p>{tooltip}</p>
|
|
122
|
+
</TooltipContent>
|
|
123
|
+
</Tooltip>
|
|
124
|
+
</TooltipProvider>
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
export type WebPreviewUrlProps = ComponentProps<typeof Input>;
|
|
128
|
+
|
|
129
|
+
export const WebPreviewUrl = ({
|
|
130
|
+
value,
|
|
131
|
+
onChange,
|
|
132
|
+
onKeyDown,
|
|
133
|
+
...props
|
|
134
|
+
}: WebPreviewUrlProps) => {
|
|
135
|
+
const { url, setUrl } = useWebPreview();
|
|
136
|
+
const [inputValue, setInputValue] = useState(url);
|
|
137
|
+
|
|
138
|
+
// Sync input value with context URL when it changes externally
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
setInputValue(url);
|
|
141
|
+
}, [url]);
|
|
142
|
+
|
|
143
|
+
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
144
|
+
setInputValue(event.target.value);
|
|
145
|
+
onChange?.(event);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
149
|
+
if (event.key === "Enter") {
|
|
150
|
+
const target = event.target as HTMLInputElement;
|
|
151
|
+
setUrl(target.value);
|
|
152
|
+
}
|
|
153
|
+
onKeyDown?.(event);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<Input
|
|
158
|
+
className="h-8 flex-1 text-sm"
|
|
159
|
+
onChange={onChange ?? handleChange}
|
|
160
|
+
onKeyDown={handleKeyDown}
|
|
161
|
+
placeholder="Enter URL..."
|
|
162
|
+
value={value ?? inputValue}
|
|
163
|
+
{...props}
|
|
164
|
+
/>
|
|
165
|
+
);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export type WebPreviewBodyProps = ComponentProps<"iframe"> & {
|
|
169
|
+
loading?: ReactNode;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export const WebPreviewBody = ({
|
|
173
|
+
className,
|
|
174
|
+
loading,
|
|
175
|
+
src,
|
|
176
|
+
...props
|
|
177
|
+
}: WebPreviewBodyProps) => {
|
|
178
|
+
const { url } = useWebPreview();
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<div className="flex-1">
|
|
182
|
+
<iframe
|
|
183
|
+
className={cn("size-full", className)}
|
|
184
|
+
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-presentation"
|
|
185
|
+
src={(src ?? url) || undefined}
|
|
186
|
+
title="Preview"
|
|
187
|
+
{...props}
|
|
188
|
+
/>
|
|
189
|
+
{loading}
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export type WebPreviewConsoleProps = ComponentProps<"div"> & {
|
|
195
|
+
logs?: Array<{
|
|
196
|
+
level: "log" | "warn" | "error";
|
|
197
|
+
message: string;
|
|
198
|
+
timestamp: Date;
|
|
199
|
+
}>;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export const WebPreviewConsole = ({
|
|
203
|
+
className,
|
|
204
|
+
logs = [],
|
|
205
|
+
children,
|
|
206
|
+
...props
|
|
207
|
+
}: WebPreviewConsoleProps) => {
|
|
208
|
+
const { consoleOpen, setConsoleOpen } = useWebPreview();
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<Collapsible
|
|
212
|
+
className={cn("border-t bg-muted/50 font-mono text-sm", className)}
|
|
213
|
+
onOpenChange={setConsoleOpen}
|
|
214
|
+
open={consoleOpen}
|
|
215
|
+
{...props}
|
|
216
|
+
>
|
|
217
|
+
<CollapsibleTrigger asChild>
|
|
218
|
+
<Button
|
|
219
|
+
className="flex w-full items-center justify-between p-4 text-left font-medium hover:bg-muted/50"
|
|
220
|
+
variant="ghost"
|
|
221
|
+
>
|
|
222
|
+
Console
|
|
223
|
+
<ChevronDownIcon
|
|
224
|
+
className={cn(
|
|
225
|
+
"h-4 w-4 transition-transform duration-200",
|
|
226
|
+
consoleOpen && "rotate-180"
|
|
227
|
+
)}
|
|
228
|
+
/>
|
|
229
|
+
</Button>
|
|
230
|
+
</CollapsibleTrigger>
|
|
231
|
+
<CollapsibleContent
|
|
232
|
+
className={cn(
|
|
233
|
+
"px-4 pb-4",
|
|
234
|
+
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 outline-none data-[state=closed]:animate-out data-[state=open]:animate-in"
|
|
235
|
+
)}
|
|
236
|
+
>
|
|
237
|
+
<div className="max-h-48 space-y-1 overflow-y-auto">
|
|
238
|
+
{logs.length === 0 ? (
|
|
239
|
+
<p className="text-muted-foreground">No console output</p>
|
|
240
|
+
) : (
|
|
241
|
+
logs.map((log, index) => (
|
|
242
|
+
<div
|
|
243
|
+
className={cn(
|
|
244
|
+
"text-xs",
|
|
245
|
+
log.level === "error" && "text-destructive",
|
|
246
|
+
log.level === "warn" && "text-yellow-600",
|
|
247
|
+
log.level === "log" && "text-foreground"
|
|
248
|
+
)}
|
|
249
|
+
key={`${log.timestamp.getTime()}-${index}`}
|
|
250
|
+
>
|
|
251
|
+
<span className="text-muted-foreground">
|
|
252
|
+
{log.timestamp.toLocaleTimeString()}
|
|
253
|
+
</span>{" "}
|
|
254
|
+
{log.message}
|
|
255
|
+
</div>
|
|
256
|
+
))
|
|
257
|
+
)}
|
|
258
|
+
{children}
|
|
259
|
+
</div>
|
|
260
|
+
</CollapsibleContent>
|
|
261
|
+
</Collapsible>
|
|
262
|
+
);
|
|
263
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { Input } from "./Input";
|
|
2
|
+
import { useChat } from "@cloudbase/agent-react-core";
|
|
3
|
+
import { ChatMessage } from "./Message";
|
|
4
|
+
import { v4 as uuidv4 } from "uuid";
|
|
5
|
+
import { LocaleContext, type LocaleCode, useLocale } from "@/locales";
|
|
6
|
+
import { useMemo } from "react";
|
|
7
|
+
import { cn } from "@/lib/utils";
|
|
8
|
+
import { Suggestion, Suggestions } from "@/components/ai-elements/suggestion";
|
|
9
|
+
|
|
10
|
+
export interface AgKitUIProps {
|
|
11
|
+
/** Optional class name for the container */
|
|
12
|
+
className?: string;
|
|
13
|
+
/** Optional extra class name for the root container */
|
|
14
|
+
containerClassName?: string;
|
|
15
|
+
/** Optional class name for the message list container */
|
|
16
|
+
messagesClassName?: string;
|
|
17
|
+
/** Optional class name for the input wrapper */
|
|
18
|
+
inputClassName?: string;
|
|
19
|
+
/** Optional class name for the empty title */
|
|
20
|
+
emptyTitleClassName?: string;
|
|
21
|
+
/** Optional placeholder for the input */
|
|
22
|
+
inputPlaceholder?: string;
|
|
23
|
+
/** Optional suggested prompts shown above input when chat is empty */
|
|
24
|
+
suggestions?: string[];
|
|
25
|
+
/** Optional locale code for built-in UI copy */
|
|
26
|
+
locale?: LocaleCode;
|
|
27
|
+
/** Optional callback when a suggestion is clicked */
|
|
28
|
+
onSuggestionClick?: (suggestion: string) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function AgKitUI({
|
|
32
|
+
className = "h-full min-h-0 mx-auto max-w-225 flex flex-col overflow-hidden px-4",
|
|
33
|
+
containerClassName,
|
|
34
|
+
messagesClassName,
|
|
35
|
+
inputClassName,
|
|
36
|
+
emptyTitleClassName,
|
|
37
|
+
inputPlaceholder,
|
|
38
|
+
suggestions,
|
|
39
|
+
onSuggestionClick,
|
|
40
|
+
locale = "zh-CN",
|
|
41
|
+
}: AgKitUIProps) {
|
|
42
|
+
const contextValue = useMemo(() => ({ locale }), [locale]);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<LocaleContext.Provider value={contextValue}>
|
|
46
|
+
<AgKitUIContent
|
|
47
|
+
className={className}
|
|
48
|
+
containerClassName={containerClassName}
|
|
49
|
+
messagesClassName={messagesClassName}
|
|
50
|
+
inputClassName={inputClassName}
|
|
51
|
+
emptyTitleClassName={emptyTitleClassName}
|
|
52
|
+
inputPlaceholder={inputPlaceholder}
|
|
53
|
+
suggestions={suggestions}
|
|
54
|
+
onSuggestionClick={onSuggestionClick}
|
|
55
|
+
/>
|
|
56
|
+
</LocaleContext.Provider>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface AgKitUIContentProps {
|
|
61
|
+
className?: string;
|
|
62
|
+
containerClassName?: string;
|
|
63
|
+
messagesClassName?: string;
|
|
64
|
+
inputClassName?: string;
|
|
65
|
+
emptyTitleClassName?: string;
|
|
66
|
+
inputPlaceholder?: string;
|
|
67
|
+
suggestions?: string[];
|
|
68
|
+
onSuggestionClick?: (suggestion: string) => void;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function AgKitUIContent({
|
|
72
|
+
className,
|
|
73
|
+
containerClassName,
|
|
74
|
+
messagesClassName,
|
|
75
|
+
inputClassName,
|
|
76
|
+
emptyTitleClassName,
|
|
77
|
+
inputPlaceholder,
|
|
78
|
+
suggestions,
|
|
79
|
+
onSuggestionClick,
|
|
80
|
+
}: AgKitUIContentProps) {
|
|
81
|
+
const { locale } = useLocale("chat");
|
|
82
|
+
const { uiMessages, streaming, sendMessage, actions, abort } = useChat();
|
|
83
|
+
|
|
84
|
+
// Handle tool response
|
|
85
|
+
const handleRespond = (toolCallId: string, result: string) => {
|
|
86
|
+
sendMessage({
|
|
87
|
+
id: uuidv4(),
|
|
88
|
+
role: "tool",
|
|
89
|
+
content: result,
|
|
90
|
+
toolCallId,
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const visibleMessages = uiMessages.filter(
|
|
95
|
+
(message) => message.role === "user" || message.role === "assistant"
|
|
96
|
+
);
|
|
97
|
+
const isEmpty = visibleMessages.length === 0;
|
|
98
|
+
|
|
99
|
+
const inputNode = (
|
|
100
|
+
<div className={inputClassName}>
|
|
101
|
+
{isEmpty && suggestions && suggestions.length > 0 && (
|
|
102
|
+
<Suggestions className="mb-3">
|
|
103
|
+
{suggestions
|
|
104
|
+
.map((suggestion) => suggestion.trim())
|
|
105
|
+
.filter(Boolean)
|
|
106
|
+
.map((suggestion, index) => (
|
|
107
|
+
<Suggestion
|
|
108
|
+
key={`${suggestion}-${index}`}
|
|
109
|
+
suggestion={suggestion}
|
|
110
|
+
variant="ghost"
|
|
111
|
+
className="max-w-full border border-border/80 text-muted-foreground"
|
|
112
|
+
disabled={streaming}
|
|
113
|
+
onClick={(value) => {
|
|
114
|
+
onSuggestionClick?.(value);
|
|
115
|
+
sendMessage(value);
|
|
116
|
+
}}
|
|
117
|
+
/>
|
|
118
|
+
))}
|
|
119
|
+
</Suggestions>
|
|
120
|
+
)}
|
|
121
|
+
<Input
|
|
122
|
+
placeholder={inputPlaceholder}
|
|
123
|
+
streaming={streaming}
|
|
124
|
+
onStop={() => {
|
|
125
|
+
if (!streaming) return;
|
|
126
|
+
abort();
|
|
127
|
+
}}
|
|
128
|
+
onSend={(message) => {
|
|
129
|
+
sendMessage(message.text);
|
|
130
|
+
}}
|
|
131
|
+
/>
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div
|
|
137
|
+
className={cn(
|
|
138
|
+
"bg-background text-foreground h-full min-h-0 w-full",
|
|
139
|
+
containerClassName
|
|
140
|
+
)}
|
|
141
|
+
>
|
|
142
|
+
<div className={cn(className)}>
|
|
143
|
+
{isEmpty ? (
|
|
144
|
+
<div className="flex min-h-0 flex-1 items-center justify-center">
|
|
145
|
+
<div className="w-full">
|
|
146
|
+
<div
|
|
147
|
+
className={cn(
|
|
148
|
+
"mb-5 text-center text-xl font-medium text-foreground",
|
|
149
|
+
emptyTitleClassName
|
|
150
|
+
)}
|
|
151
|
+
>
|
|
152
|
+
{locale.emptyTitle}
|
|
153
|
+
</div>
|
|
154
|
+
{inputNode}
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
) : (
|
|
158
|
+
<>
|
|
159
|
+
<div
|
|
160
|
+
className={cn(
|
|
161
|
+
"min-h-0 flex-1 overflow-y-auto py-4",
|
|
162
|
+
messagesClassName
|
|
163
|
+
)}
|
|
164
|
+
>
|
|
165
|
+
<ChatMessage
|
|
166
|
+
messages={uiMessages}
|
|
167
|
+
isLoading={streaming}
|
|
168
|
+
actions={actions}
|
|
169
|
+
onRespond={handleRespond}
|
|
170
|
+
/>
|
|
171
|
+
</div>
|
|
172
|
+
{inputNode}
|
|
173
|
+
</>
|
|
174
|
+
)}
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Attachment,
|
|
3
|
+
AttachmentPreview,
|
|
4
|
+
AttachmentRemove,
|
|
5
|
+
Attachments,
|
|
6
|
+
} from "@/components/ai-elements/attachments";
|
|
7
|
+
import {
|
|
8
|
+
PromptInput,
|
|
9
|
+
PromptInputBody,
|
|
10
|
+
PromptInputFooter,
|
|
11
|
+
type PromptInputMessage,
|
|
12
|
+
PromptInputProvider,
|
|
13
|
+
PromptInputSubmit,
|
|
14
|
+
PromptInputTextarea,
|
|
15
|
+
PromptInputTools,
|
|
16
|
+
usePromptInputAttachments,
|
|
17
|
+
} from "@/components/ai-elements/prompt-input";
|
|
18
|
+
import { useLocale } from "@/locales";
|
|
19
|
+
|
|
20
|
+
export interface InputProps {
|
|
21
|
+
onSend?: (message: PromptInputMessage) => void;
|
|
22
|
+
onStop?: () => void;
|
|
23
|
+
placeholder?: string;
|
|
24
|
+
streaming?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const PromptInputAttachmentsDisplay = () => {
|
|
28
|
+
const attachments = usePromptInputAttachments();
|
|
29
|
+
|
|
30
|
+
if (attachments.files.length === 0) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<Attachments variant="inline">
|
|
36
|
+
{attachments.files.map((attachment) => (
|
|
37
|
+
<Attachment
|
|
38
|
+
data={attachment}
|
|
39
|
+
key={attachment.id}
|
|
40
|
+
onRemove={() => attachments.remove(attachment.id)}
|
|
41
|
+
>
|
|
42
|
+
<AttachmentPreview />
|
|
43
|
+
<AttachmentRemove />
|
|
44
|
+
</Attachment>
|
|
45
|
+
))}
|
|
46
|
+
</Attachments>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export function Input({
|
|
51
|
+
onSend,
|
|
52
|
+
onStop,
|
|
53
|
+
placeholder,
|
|
54
|
+
streaming = false,
|
|
55
|
+
}: InputProps) {
|
|
56
|
+
const { locale } = useLocale("input");
|
|
57
|
+
const status: "submitted" | "streaming" | "ready" | "error" = streaming
|
|
58
|
+
? "streaming"
|
|
59
|
+
: "ready";
|
|
60
|
+
|
|
61
|
+
const handleSubmit = (message: PromptInputMessage) => {
|
|
62
|
+
const text = message.text.trim();
|
|
63
|
+
const hasText = Boolean(text);
|
|
64
|
+
const hasAttachments = Boolean(message.files?.length);
|
|
65
|
+
|
|
66
|
+
if (!(hasText || hasAttachments)) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Trigger onSend callback when submitting message
|
|
71
|
+
onSend?.({
|
|
72
|
+
...message,
|
|
73
|
+
text,
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className="pb-4">
|
|
79
|
+
<PromptInputProvider>
|
|
80
|
+
<PromptInput globalDrop multiple onSubmit={handleSubmit}>
|
|
81
|
+
<PromptInputAttachmentsDisplay />
|
|
82
|
+
<PromptInputBody>
|
|
83
|
+
<PromptInputTextarea
|
|
84
|
+
placeholder={placeholder ?? locale.placeholder}
|
|
85
|
+
/>
|
|
86
|
+
</PromptInputBody>
|
|
87
|
+
<PromptInputFooter>
|
|
88
|
+
<PromptInputTools></PromptInputTools>
|
|
89
|
+
<PromptInputSubmit status={status} onStop={onStop} />
|
|
90
|
+
</PromptInputFooter>
|
|
91
|
+
</PromptInput>
|
|
92
|
+
<div className="text-center text-xs opacity-50 mt-3">
|
|
93
|
+
{locale.disclaimer}
|
|
94
|
+
</div>
|
|
95
|
+
</PromptInputProvider>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|